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