Official cwtch.im peer implementation. https://cwtch.im
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 

330 lines
8.4 KiB

package peer
import (
"archive/tar"
"compress/gzip"
"crypto/rand"
"database/sql"
"encoding/hex"
"errors"
"fmt"
"git.openprivacy.ca/openprivacy/log"
"golang.org/x/crypto/pbkdf2"
"golang.org/x/crypto/sha3"
"io"
"io/ioutil"
"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) {
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
}
func openEncryptedDatabase(profileDirectory string, password string, createIfNotExists bool) (*sql.DB, error) {
salt, err := ioutil.ReadFile(path.Join(profileDirectory, saltFile))
if err != nil {
return nil, err
}
key := createKey(password, salt)
dbPath := filepath.Join(profileDirectory, "db")
if !createIfNotExists {
if _, err := os.Stat(dbPath); errors.Is(err, os.ErrNotExist) {
return nil, err
}
}
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")
db, err := openEncryptedDatabase(profileDirectory, password, true)
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")
cps, err := NewCwtchProfileStorage(db, profileDirectory)
if err != nil {
db.Close()
return nil, err
}
return NewProfileWithEncryptedStorage(name, cps), nil
}
// CreateEncryptedStore creates a encrypted datastore
func CreateEncryptedStore(profileDirectory string, password string) (*CwtchProfileStorage, error) {
log.Debugf("Creating Encrypted Database")
db, err := openEncryptedDatabase(profileDirectory, password, true)
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")
cps, err := NewCwtchProfileStorage(db, profileDirectory)
if err != nil {
db.Close()
return nil, err
}
return cps, nil
}
// FromEncryptedDatabase constructs a Cwtch Profile from an existing Encrypted Database
func FromEncryptedDatabase(profileDirectory string, password string) (CwtchPeer, error) {
log.Infof("Loading Encrypted Profile: %v", profileDirectory)
db, err := openEncryptedDatabase(profileDirectory, password, false)
if db == nil || err != nil {
return nil, fmt.Errorf("unable to open encrypted database: error: %v", err)
}
log.Debugf("Initializing Profile from Encrypted Storage")
cps, err := NewCwtchProfileStorage(db, profileDirectory)
if err != nil {
db.Close()
return nil, err
}
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
log.Errorf("error importing profile: %v. removing %s", err, profileDir)
os.RemoveAll(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]
_, hexErr := hex.DecodeString(dir)
if dir == "." || dir == ".." || len(dir) != 32 || hexErr != nil {
return "", errors.New("invalid profile name")
}
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]
_, hexErr := hex.DecodeString(dir)
if dir == "." || dir == ".." || len(dir) != 32 || hexErr != nil {
return errors.New("invalid profile name")
}
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
}