First cut of profile import/export
This commit is contained in:
parent
8f138b47b0
commit
5a87f835b4
|
@ -27,4 +27,5 @@ tokens.db
|
|||
tokens1.db
|
||||
arch/
|
||||
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 {
|
||||
LoadProfiles(password string)
|
||||
CreateTaggedPeer(name string, password string, tag string)
|
||||
ImportProfile(exportedCwtchFile string, password string) (peer.CwtchPeer, error)
|
||||
DeletePeer(onion string, currentPassword string)
|
||||
AddPeerPlugin(onion string, pluginID plugins.PluginID)
|
||||
LaunchPeers()
|
||||
|
@ -125,6 +126,15 @@ func (app *application) AddPeerPlugin(onion string, pluginID plugins.PluginID) {
|
|||
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
|
||||
func (app *application) LoadProfiles(password string) {
|
||||
count := 0
|
||||
|
|
|
@ -72,6 +72,12 @@ type cwtchPeer struct {
|
|||
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() {
|
||||
cp.mutex.Lock()
|
||||
defer cp.mutex.Unlock()
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"cwtch.im/cwtch/event"
|
||||
"cwtch.im/cwtch/model"
|
||||
"cwtch.im/cwtch/model/attr"
|
||||
|
@ -8,7 +10,11 @@ import (
|
|||
"errors"
|
||||
"fmt"
|
||||
"git.openprivacy.ca/openprivacy/log"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// 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))
|
||||
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)
|
||||
CheckPassword(password string) bool
|
||||
ChangePassword(oldpassword string, newpassword string, newpasswordAgain string) error
|
||||
Export(file string) error
|
||||
Delete()
|
||||
}
|
||||
|
|
150
peer/storage.go
150
peer/storage.go
|
@ -1,6 +1,8 @@
|
|||
package peer
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"crypto/rand"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
@ -13,11 +15,13 @@ import (
|
|||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const versionFile = "VERSION"
|
||||
const version = "2"
|
||||
const saltFile = "SALT"
|
||||
const dbFile = "db"
|
||||
|
||||
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
|
||||
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
|
||||
|
@ -165,3 +169,149 @@ func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer,
|
|||
}
|
||||
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)
|
||||
}
|
||||
|
||||
aliceOnion := alice.GetOnion()
|
||||
alice.PeerWithOnion(bob.GetOnion())
|
||||
|
||||
time.Sleep(time.Second * 40)
|
||||
|
@ -94,7 +95,12 @@ func TestEncryptedStorage(t *testing.T) {
|
|||
|
||||
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)
|
||||
if body != "Hello Bob" || err != nil {
|
||||
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)
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
|
|
Loading…
Reference in New Issue