2021-11-09 23:47:33 +00:00
|
|
|
package peer
|
|
|
|
|
|
|
|
import (
|
2022-03-08 21:45:26 +00:00
|
|
|
"archive/tar"
|
|
|
|
"compress/gzip"
|
2021-11-09 23:47:33 +00:00
|
|
|
"crypto/rand"
|
|
|
|
"database/sql"
|
2022-03-09 23:52:24 +00:00
|
|
|
"encoding/hex"
|
2021-11-18 23:43:58 +00:00
|
|
|
"errors"
|
2021-11-09 23:47:33 +00:00
|
|
|
"fmt"
|
|
|
|
"git.openprivacy.ca/openprivacy/log"
|
|
|
|
"golang.org/x/crypto/pbkdf2"
|
|
|
|
"golang.org/x/crypto/sha3"
|
|
|
|
"io"
|
|
|
|
"io/ioutil"
|
|
|
|
"os"
|
|
|
|
"path"
|
|
|
|
"path/filepath"
|
2022-03-08 21:45:26 +00:00
|
|
|
"strings"
|
2021-11-09 23:47:33 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
const versionFile = "VERSION"
|
|
|
|
const version = "2"
|
|
|
|
const saltFile = "SALT"
|
2022-03-08 21:45:26 +00:00
|
|
|
const dbFile = "db"
|
2021-11-09 23:47:33 +00:00
|
|
|
|
|
|
|
// CreateKeySalt derives a key and salt from a password: returns key, salt, err
|
|
|
|
func CreateKeySalt(password string) ([32]byte, [128]byte, error) {
|
|
|
|
var salt [128]byte
|
|
|
|
if _, err := io.ReadFull(rand.Reader, salt[:]); err != nil {
|
|
|
|
log.Errorf("Cannot read from random: %v\n", err)
|
|
|
|
return [32]byte{}, salt, err
|
|
|
|
}
|
|
|
|
dk := pbkdf2.Key([]byte(password), salt[:], 4096, 32, sha3.New512)
|
|
|
|
|
|
|
|
var dkr [32]byte
|
|
|
|
copy(dkr[:], dk)
|
|
|
|
return dkr, salt, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// createKey derives a key from a password and salt
|
|
|
|
func createKey(password string, salt []byte) [32]byte {
|
|
|
|
dk := pbkdf2.Key([]byte(password), salt, 4096, 32, sha3.New512)
|
|
|
|
|
|
|
|
var dkr [32]byte
|
|
|
|
copy(dkr[:], dk)
|
|
|
|
return dkr
|
|
|
|
}
|
|
|
|
|
|
|
|
func initV2Directory(directory, password string) ([32]byte, [128]byte, error) {
|
|
|
|
os.Mkdir(directory, 0700)
|
|
|
|
|
|
|
|
key, salt, err := CreateKeySalt(password)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("Could not create key for profile store from password: %v\n", err)
|
|
|
|
return [32]byte{}, [128]byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = ioutil.WriteFile(path.Join(directory, versionFile), []byte(version), 0600); err != nil {
|
|
|
|
log.Errorf("Could not write version file: %v", err)
|
|
|
|
return [32]byte{}, [128]byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
if err = ioutil.WriteFile(path.Join(directory, saltFile), salt[:], 0600); err != nil {
|
|
|
|
log.Errorf("Could not write salt file: %v", err)
|
|
|
|
return [32]byte{}, [128]byte{}, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return key, salt, nil
|
|
|
|
}
|
|
|
|
|
2021-11-18 23:43:58 +00:00
|
|
|
func openEncryptedDatabase(profileDirectory string, password string, createIfNotExists bool) (*sql.DB, error) {
|
2021-11-09 23:47:33 +00:00
|
|
|
salt, err := ioutil.ReadFile(path.Join(profileDirectory, saltFile))
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
key := createKey(password, salt)
|
|
|
|
dbPath := filepath.Join(profileDirectory, "db")
|
2021-11-18 23:43:58 +00:00
|
|
|
|
|
|
|
if !createIfNotExists {
|
|
|
|
if _, err := os.Stat(dbPath); errors.Is(err, os.ErrNotExist) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-11-09 23:47:33 +00:00
|
|
|
dbname := fmt.Sprintf("%v?_pragma_key=x'%x'&_pragma_cipher_page_size=8192", dbPath, key)
|
|
|
|
db, err := sql.Open("sqlite3", dbname)
|
|
|
|
if err != nil {
|
|
|
|
log.Errorf("could not open encrypted database", err)
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return db, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// CreateEncryptedStorePeer creates a *new* Cwtch Profile backed by an encrypted datastore
|
|
|
|
func CreateEncryptedStorePeer(profileDirectory string, name string, password string) (CwtchPeer, error) {
|
|
|
|
log.Debugf("Initializing Encrypted Storage Directory")
|
|
|
|
_, _, err := initV2Directory(profileDirectory, password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Opening Encrypted Database")
|
2021-11-18 23:43:58 +00:00
|
|
|
db, err := openEncryptedDatabase(profileDirectory, password, true)
|
2021-11-09 23:47:33 +00:00
|
|
|
if db == nil || err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Initializing Database")
|
|
|
|
err = initializeDatabase(db)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
db.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
|
|
|
|
|
2021-11-19 19:49:04 +00:00
|
|
|
cps, err := NewCwtchProfileStorage(db, profileDirectory)
|
2021-11-09 23:47:33 +00:00
|
|
|
if err != nil {
|
|
|
|
db.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return NewProfileWithEncryptedStorage(name, cps), nil
|
|
|
|
}
|
|
|
|
|
2021-11-17 23:34:14 +00:00
|
|
|
// CreateEncryptedStore creates a encrypted datastore
|
|
|
|
func CreateEncryptedStore(profileDirectory string, password string) (*CwtchProfileStorage, error) {
|
|
|
|
|
2021-11-18 23:43:58 +00:00
|
|
|
log.Debugf("Creating Encrypted Database")
|
|
|
|
db, err := openEncryptedDatabase(profileDirectory, password, true)
|
2021-11-17 23:34:14 +00:00
|
|
|
if db == nil || err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Initializing Database")
|
|
|
|
err = initializeDatabase(db)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
db.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Creating Cwtch Profile Backed By Encrypted Database")
|
|
|
|
|
2021-11-19 19:49:04 +00:00
|
|
|
cps, err := NewCwtchProfileStorage(db, profileDirectory)
|
2021-11-17 23:34:14 +00:00
|
|
|
if err != nil {
|
|
|
|
db.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
return cps, nil
|
|
|
|
}
|
|
|
|
|
2021-11-09 23:47:33 +00:00
|
|
|
// FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database
|
|
|
|
func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) {
|
2021-11-18 23:43:58 +00:00
|
|
|
log.Infof("Loading Encrypted Profile: %v", profileDirectory)
|
|
|
|
db, err := openEncryptedDatabase(profileDirectory, password, false)
|
2021-11-09 23:47:33 +00:00
|
|
|
if db == nil || err != nil {
|
|
|
|
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
log.Debugf("Initializing Profile from Encrypted Storage")
|
2021-11-19 19:49:04 +00:00
|
|
|
cps, err := NewCwtchProfileStorage(db, profileDirectory)
|
2021-11-09 23:47:33 +00:00
|
|
|
if err != nil {
|
|
|
|
db.Close()
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return FromEncryptedStorage(cps), nil
|
|
|
|
}
|
2022-03-08 21:45:26 +00:00
|
|
|
|
|
|
|
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
|
2022-03-09 22:32:21 +00:00
|
|
|
log.Errorf("error importing profile: %v. removing %s", err, profileDir)
|
|
|
|
os.RemoveAll(profileDir)
|
2022-03-08 21:45:26 +00:00
|
|
|
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]
|
|
|
|
|
2022-03-09 23:52:24 +00:00
|
|
|
_, hexErr := hex.DecodeString(dir)
|
2022-03-22 19:45:29 +00:00
|
|
|
if dir == "." || dir == ".." || len(dir) != 32 || hexErr != nil {
|
2022-03-09 23:52:24 +00:00
|
|
|
return "", errors.New("invalid profile name")
|
|
|
|
}
|
|
|
|
|
2022-03-08 21:45:26 +00:00
|
|
|
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]
|
2022-03-09 23:52:24 +00:00
|
|
|
|
|
|
|
_, hexErr := hex.DecodeString(dir)
|
|
|
|
if dir == "." || dir == ".." || len(dir) != 32 || hexErr != nil {
|
|
|
|
return errors.New("invalid profile name")
|
|
|
|
}
|
|
|
|
|
2022-03-08 21:45:26 +00:00
|
|
|
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
|
|
|
|
}
|