Browse Source

First cut of profile import/export

pull/434/head
Sarah Jamie Lewis 4 months ago
parent
commit
5a87f835b4
  1. 3
      .gitignore
  2. 10
      app/app.go
  3. 6
      peer/cwtch_peer.go
  4. 78
      peer/cwtchprofilestorage.go
  5. 1
      peer/profile_interface.go
  6. 150
      peer/storage.go
  7. 27
      testing/encryptedstorage/encrypted_storage_integration_test.go

3
.gitignore

@ -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

@ -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

6
peer/cwtch_peer.go

@ -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()

78
peer/cwtchprofilestorage.go

@ -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
}

1
peer/profile_interface.go

@ -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

@ -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
}

27
testing/encryptedstorage/encrypted_storage_integration_test.go

@ -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…
Cancel
Save