First cut of profile import/export
This commit is contained in:
parent
8f138b47b0
commit
5a87f835b4
|
@ -27,4 +27,5 @@ tokens.db
|
||||||
tokens1.db
|
tokens1.db
|
||||||
arch/
|
arch/
|
||||||
testing/encryptedstorage/encrypted_storage_profiles
|
testing/encryptedstorage/encrypted_storage_profiles
|
||||||
testing/encryptedstorage/tordir
|
testing/encryptedstorage/tordir
|
||||||
|
*.tar.gz
|
10
app/app.go
10
app/app.go
|
@ -34,6 +34,7 @@ type application struct {
|
||||||
type Application interface {
|
type Application interface {
|
||||||
LoadProfiles(password string)
|
LoadProfiles(password string)
|
||||||
CreateTaggedPeer(name string, password string, tag string)
|
CreateTaggedPeer(name string, password string, tag string)
|
||||||
|
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
|
||||||
DeletePeer(onion string, currentPassword string)
|
DeletePeer(onion string, currentPassword string)
|
||||||
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||||
LaunchPeers()
|
LaunchPeers()
|
||||||
|
@ -125,6 +126,15 @@ func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
|
||||||
app.AddPlugin(onion, pluginID, app.eventBuses[onion], app.acn)
|
app.AddPlugin(onion, pluginID, app.eventBuses[onion], app.acn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (app *application) ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error) {
|
||||||
|
profileDirectory := path.Join(app.directory, "profiles")
|
||||||
|
profile, err := peer.ImportProfile(exportedCwtchFile, profileDirectory, password)
|
||||||
|
if err == nil {
|
||||||
|
app.installProfile(profile)
|
||||||
|
}
|
||||||
|
return profile, err
|
||||||
|
}
|
||||||
|
|
||||||
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
|
// LoadProfiles takes a password and attempts to load any profiles it can from storage with it and create Peers for them
|
||||||
func (app *application) LoadProfiles(password string) {
|
func (app *application) LoadProfiles(password string) {
|
||||||
count := 0
|
count := 0
|
||||||
|
|
|
@ -72,6 +72,12 @@ type cwtchPeer struct {
|
||||||
eventBus event.Manager
|
eventBus event.Manager
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (cp *cwtchPeer) Export(file string) error {
|
||||||
|
cp.mutex.Lock()
|
||||||
|
defer cp.mutex.Unlock()
|
||||||
|
return cp.storage.Export(file)
|
||||||
|
}
|
||||||
|
|
||||||
func (cp *cwtchPeer) Delete() {
|
func (cp *cwtchPeer) Delete() {
|
||||||
cp.mutex.Lock()
|
cp.mutex.Lock()
|
||||||
defer cp.mutex.Unlock()
|
defer cp.mutex.Unlock()
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
"cwtch.im/cwtch/event"
|
"cwtch.im/cwtch/event"
|
||||||
"cwtch.im/cwtch/model"
|
"cwtch.im/cwtch/model"
|
||||||
"cwtch.im/cwtch/model/attr"
|
"cwtch.im/cwtch/model/attr"
|
||||||
|
@ -8,7 +10,11 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.openprivacy.ca/openprivacy/log"
|
"git.openprivacy.ca/openprivacy/log"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StorageKeyType is an interface wrapper around storage key types
|
// StorageKeyType is an interface wrapper around storage key types
|
||||||
|
@ -771,3 +777,75 @@ func (cps *CwtchProfileStorage) Rekey(newkey [32]byte) error {
|
||||||
_, err := cps.db.Exec(fmt.Sprintf(`PRAGMA rekey="x'%x'";`, newkey))
|
_, err := cps.db.Exec(fmt.Sprintf(`PRAGMA rekey="x'%x'";`, newkey))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Export takes in a file name and creates an exported cwtch profile file (which in reality is a compressed tarball).
|
||||||
|
func (cps *CwtchProfileStorage) Export(filename string) error {
|
||||||
|
profileDB := filepath.Join(cps.ProfileDirectory, dbFile)
|
||||||
|
profileSalt := filepath.Join(cps.ProfileDirectory, saltFile)
|
||||||
|
profileVersion := filepath.Join(cps.ProfileDirectory, versionFile)
|
||||||
|
|
||||||
|
file, err := os.Create(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create tarball file '%s', got error '%s'", filename, err.Error())
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
gzipWriter := gzip.NewWriter(file)
|
||||||
|
defer gzipWriter.Close()
|
||||||
|
|
||||||
|
tarWriter := tar.NewWriter(gzipWriter)
|
||||||
|
defer tarWriter.Close()
|
||||||
|
|
||||||
|
// We need to know the base directory so we can import it later (and prevent duplicates)...
|
||||||
|
profilePath := path.Base(cps.ProfileDirectory)
|
||||||
|
|
||||||
|
err = addFileToTarWriter(profilePath, profileDB, tarWriter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not add file '%s', to tarball, got error '%s'", profileDB, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = addFileToTarWriter(profilePath, profileSalt, tarWriter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not add file '%s', to tarball, got error '%s'", profileDB, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
err = addFileToTarWriter(profilePath, profileVersion, tarWriter)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not add file '%s', to tarball, got error '%s'", profileDB, err.Error())
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func addFileToTarWriter(profilePath string, filePath string, tarWriter *tar.Writer) error {
|
||||||
|
file, err := os.Open(filePath)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not open file '%s', got error '%s'", filePath, err.Error())
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
stat, err := file.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not get stat for file '%s', got error '%s'", filePath, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
header := &tar.Header{
|
||||||
|
// Note: we are using strings.Join here deliberately so that we can import the profile
|
||||||
|
// in a cross platform way (e.g. using filepath here would result in different names on Windows v.s Linux)
|
||||||
|
Name: strings.Join([]string{profilePath, stat.Name()}, "/"),
|
||||||
|
Size: stat.Size(),
|
||||||
|
Mode: int64(stat.Mode()),
|
||||||
|
ModTime: stat.ModTime(),
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tarWriter.WriteHeader(header)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not write header for file '%s', got error '%s'", filePath, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(tarWriter, file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not copy the file '%s' data to the tarball, got error '%s'", filePath, err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -116,5 +116,6 @@ type CwtchPeer interface {
|
||||||
ShareFile(fileKey string, serializedManifest string)
|
ShareFile(fileKey string, serializedManifest string)
|
||||||
CheckPassword(password string) bool
|
CheckPassword(password string) bool
|
||||||
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
|
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
|
||||||
|
Export(file string) error
|
||||||
Delete()
|
Delete()
|
||||||
}
|
}
|
||||||
|
|
150
peer/storage.go
150
peer/storage.go
|
@ -1,6 +1,8 @@
|
||||||
package peer
|
package peer
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"archive/tar"
|
||||||
|
"compress/gzip"
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"errors"
|
"errors"
|
||||||
|
@ -13,11 +15,13 @@ import (
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
const versionFile = "VERSION"
|
const versionFile = "VERSION"
|
||||||
const version = "2"
|
const version = "2"
|
||||||
const saltFile = "SALT"
|
const saltFile = "SALT"
|
||||||
|
const dbFile = "db"
|
||||||
|
|
||||||
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
|
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
|
||||||
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
|
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
|
||||||
|
@ -165,3 +169,149 @@ func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer,
|
||||||
}
|
}
|
||||||
return FromEncryptedStorage(cps), nil
|
return FromEncryptedStorage(cps), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ImportProfile(exportedCwtchFile string, profilesDir string, password string) (CwtchPeer, error) {
|
||||||
|
profileID, err := checkCwtchProfileBackupFile(exportedCwtchFile)
|
||||||
|
if profileID == "" || err != nil {
|
||||||
|
log.Errorf("%s is an invalid cwtch backup file: %s", profileID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
log.Infof("%s is a valid cwtch backup file", profileID)
|
||||||
|
|
||||||
|
profileDBFile := filepath.Join(profilesDir, profileID, dbFile)
|
||||||
|
log.Debugf("checking %v", profileDBFile)
|
||||||
|
if _, err := os.Stat(profileDBFile); errors.Is(err, os.ErrNotExist) {
|
||||||
|
// backup is valid and the profile hasn't been imported yet, time to extract and check the password
|
||||||
|
profileDir := filepath.Join(profilesDir, profileID)
|
||||||
|
os.MkdirAll(profileDir, 0700)
|
||||||
|
err := importCwtchProfileBackupFile(exportedCwtchFile, profilesDir)
|
||||||
|
if err == nil {
|
||||||
|
profile, err := FromEncryptedDatabase(profileDir, password)
|
||||||
|
if err == nil {
|
||||||
|
return profile, err
|
||||||
|
}
|
||||||
|
// Otherwise purge
|
||||||
|
os.RemoveAll(filepath.Join(profilesDir, profileDir))
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("%s is already a profile for this app", profileID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkCwtchProfileBackupFile(srcFile string) (string, error) {
|
||||||
|
f, err := os.Open(srcFile)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
gzf, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(gzf)
|
||||||
|
|
||||||
|
profileName := ""
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
return "", errors.New("invalid cwtch backup file")
|
||||||
|
case tar.TypeReg:
|
||||||
|
parts := strings.Split(header.Name, "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return "", errors.New("invalid header name")
|
||||||
|
}
|
||||||
|
dir := parts[0]
|
||||||
|
profileFileType := parts[1]
|
||||||
|
|
||||||
|
if profileName == "" {
|
||||||
|
profileName = dir
|
||||||
|
}
|
||||||
|
if dir != profileName {
|
||||||
|
return "", errors.New("invalid cwtch backup file")
|
||||||
|
}
|
||||||
|
|
||||||
|
if profileFileType != dbFile && profileFileType != saltFile && profileFileType != versionFile {
|
||||||
|
return "", errors.New("invalid cwtch backup file")
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return "", errors.New("invalid cwtch backup file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return profileName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func importCwtchProfileBackupFile(srcFile string, profilesDir string) error {
|
||||||
|
f, err := os.Open(srcFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
gzf, err := gzip.NewReader(f)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tarReader := tar.NewReader(gzf)
|
||||||
|
|
||||||
|
profileName := ""
|
||||||
|
|
||||||
|
for {
|
||||||
|
header, err := tarReader.Next()
|
||||||
|
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch header.Typeflag {
|
||||||
|
case tar.TypeDir:
|
||||||
|
return errors.New("invalid cwtch backup file")
|
||||||
|
case tar.TypeReg:
|
||||||
|
// using split here because we deliberately construct these paths in a cross-platform consistent way
|
||||||
|
parts := strings.Split(header.Name, "/")
|
||||||
|
if len(parts) != 2 {
|
||||||
|
return errors.New("invalid header name")
|
||||||
|
}
|
||||||
|
dir := parts[0]
|
||||||
|
base := parts[1]
|
||||||
|
if profileName == "" {
|
||||||
|
profileName = dir
|
||||||
|
}
|
||||||
|
|
||||||
|
if dir != profileName {
|
||||||
|
return errors.New("invalid cwtch backup file")
|
||||||
|
}
|
||||||
|
|
||||||
|
// here we use filepath.Join to construct a valid directory path
|
||||||
|
outFile, err := os.Create(filepath.Join(profilesDir, dir, base))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error importing cwtch profile file: %s", err)
|
||||||
|
}
|
||||||
|
defer outFile.Close()
|
||||||
|
if _, err := io.Copy(outFile, tarReader); err != nil {
|
||||||
|
return fmt.Errorf("error importing cwtch profile file: %s", err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return errors.New("invalid cwtch backup file")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
|
@ -79,6 +79,7 @@ func TestEncryptedStorage(t *testing.T) {
|
||||||
t.Fatalf("unexpected issue when fetching all of alices conversations. Expected 1 got : %v %v", conversations, err)
|
t.Fatalf("unexpected issue when fetching all of alices conversations. Expected 1 got : %v %v", conversations, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
aliceOnion := alice.GetOnion()
|
||||||
alice.PeerWithOnion(bob.GetOnion())
|
alice.PeerWithOnion(bob.GetOnion())
|
||||||
|
|
||||||
time.Sleep(time.Second * 40)
|
time.Sleep(time.Second * 40)
|
||||||
|
@ -94,7 +95,12 @@ func TestEncryptedStorage(t *testing.T) {
|
||||||
|
|
||||||
time.Sleep(time.Second * 30)
|
time.Sleep(time.Second * 30)
|
||||||
|
|
||||||
ci, _ := bob.FetchConversationInfo(alice.GetOnion())
|
ci, err := bob.FetchConversationInfo(alice.GetOnion())
|
||||||
|
for err != nil {
|
||||||
|
time.Sleep(time.Second * 5)
|
||||||
|
ci, err = bob.FetchConversationInfo(alice.GetOnion())
|
||||||
|
}
|
||||||
|
|
||||||
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
|
body, _, err := bob.GetChannelMessage(ci.ID, 0, 1)
|
||||||
if body != "Hello Bob" || err != nil {
|
if body != "Hello Bob" || err != nil {
|
||||||
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
|
t.Fatalf("unexpected message in conversation channel %v %v", body, err)
|
||||||
|
@ -126,6 +132,25 @@ func TestEncryptedStorage(t *testing.T) {
|
||||||
t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages)
|
t.Fatalf("expeced GetMostRecentMessages to return 1, instead returned: %v %v", len(messages), messages)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = alice.Export("alice.tar.gz")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("could not export profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = app.ImportProfile("alice.tar.gz", "password")
|
||||||
|
if err == nil {
|
||||||
|
t.Fatal("profile is already imported...this should fail")
|
||||||
|
}
|
||||||
|
|
||||||
|
app.DeletePeer(alice.GetOnion(), "password")
|
||||||
|
alice, err = app.ImportProfile("alice.tar.gz", "password")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("profile should have successfully imported: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if alice.GetOnion() != aliceOnion {
|
||||||
|
t.Fatalf("profile is not Alice...%s != %s", aliceOnion, alice.GetOnion())
|
||||||
|
}
|
||||||
|
|
||||||
app.Shutdown()
|
app.Shutdown()
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue