next-eeze.git

commit f3be91951dcea0fb3b5293a3c4f44ef9978da35e

Author: Adam <git@apiote.tk>

encrypt data on disk

This is the first precaution. Credentials and passwords are encrypted
before storing on disk, and decrypted when retrieved. AES key is
derived from master password by Argon2. Master password is asked for
every time (agent is not used, yet).

 config/init.go | 8 +
 eeze.go | 35 +++++++++-
 fs/fs.go | 165 +++++++++++++++++++++++++++++++++++++++++++++++--
 operation/get.go | 16 +++-
 server/sync.go | 17 ++++


diff --git a/config/init.go b/config/init.go
index 155b0419b0a06d092081d7729f97a395ba25f7ec..536f729503986dbbba1a063f3adf23691f19a1c8 100644
--- a/config/init.go
+++ b/config/init.go
@@ -5,18 +5,22 @@ 	"notabug.org/apiote/next-eeze/fs"
 
 	"fmt"
 	"os"
+
 	"golang.org/x/crypto/ssh/terminal"
 )
 
-func Init() {
+func Init(masterPassword string) {
+	// todo memguard
 	credentials := fs.Credentials{}
 	fmt.Print("Server address: ")
 	fmt.Scanf("%s", &credentials.Server)
 	fmt.Print("Username: ")
 	fmt.Scanf("%s", &credentials.Username)
 	fmt.Print("Password: ")
+	// todo memguard
 	p_b, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
 	credentials.Password = string(p_b)
+	fmt.Print("\n")
 
-	fs.SaveCredentials(credentials)
+	fs.SaveCredentials(credentials, masterPassword)
 }




diff --git a/eeze.go b/eeze.go
index 5f27c5e231ec6ef9350bb9498408ea6f02c1d324..e829383483f1bac1303c56b68883d526a189de13 100644
--- a/eeze.go
+++ b/eeze.go
@@ -1,12 +1,15 @@
 package main
 
 import (
+	"notabug.org/apiote/next-eeze/config"
 	"notabug.org/apiote/next-eeze/operation"
 	"notabug.org/apiote/next-eeze/server"
-	"notabug.org/apiote/next-eeze/config"
 
 	"fmt"
 	"log"
+	"os"
+
+	"golang.org/x/crypto/ssh/terminal"
 
 	"git.sr.ht/~sircmpwn/getopt"
 )
@@ -33,19 +36,41 @@ 		log.Fatal("Error parsing opts. ", err)
 		return
 	}
 
+	masterPassword := ""
+
+	/*
+		todo
+		if exists /tmp/eeze-agent-$username
+		then
+			ask agent for masterPassword
+		else
+			start agent
+		fi
+	*/
+	if masterPassword == "" || (*C && *i) {
+		fmt.Print("Master password: ")
+		// todo memguard
+		masterPass_b, _ := terminal.ReadPassword(int(os.Stdin.Fd()))
+		// todo memguard
+		masterPassword = string(masterPass_b)
+		fmt.Print("\n")
+
+		// todo give masterPassword to agent
+	}
+
 	if *C {
 		if *i {
-			config.Init()
+			config.Init(masterPassword)
 		}
 	} else if *S {
-		err = server.Sync()
+		err = server.Sync(masterPassword)
 	} else if *G {
 		var r string
-		r, err = operation.Get(u, l, s, *f, *p)
+		r, err = operation.Get(u, l, s, *f, *p, masterPassword)
 		fmt.Println(r)
 	} else if *L {
 		var r string
-		r, err = operation.List()
+		r, err = operation.List(masterPassword)
 		fmt.Println(r)
 	} else {
 		getopt.Usage()




diff --git a/fs/fs.go b/fs/fs.go
index fe2773d505f4b99ae55912d8aa4f726bbb67ef19..3efbc6bbdddf77e4a25de910fb4332b3cd85f82f 100644
--- a/fs/fs.go
+++ b/fs/fs.go
@@ -3,15 +3,27 @@
 import (
 	"notabug.org/apiote/next-eeze/password"
 
+	"crypto/aes"
+	"crypto/cipher"
+	"crypto/rand"
+	"errors"
+	"io"
 	"io/ioutil"
 	"log"
 	"os"
 	"os/user"
 	"path/filepath"
 
+	"golang.org/x/crypto/argon2"
+
 	"git.sr.ht/~sircmpwn/go-bare"
 )
 
+type EncryptedContent struct {
+	Salt       []byte
+	CipherText []byte
+}
+
 type Credentials struct {
 	Server   string
 	Username string
@@ -25,8 +37,14 @@ 	path := filepath.Join(dir, "/.local/share/next-eeze/")
 	return path
 }
 
-func Save(passwords []password.NextPassword) error {
+// todo memguard passwords, masterPassword
+func Save(passwords []password.NextPassword, masterPassword string) error {
+	salt := makeSalt()
+	// todo memguard
+	key := deriveKey(masterPassword, salt)
+	// todo memguard
 	barePasswords := []password.BarePassword{}
+	// todo memguard
 	for _, p := range passwords {
 		barePasswords = append(barePasswords, p.ToBarePassword())
 	}
@@ -36,11 +54,32 @@ 		log.Fatal("Error creating passwords file. ", err)
 		return err
 	}
 	defer result.Close()
+	// todo memguard
 	bytes, err := bare.Marshal(&barePasswords)
 	if err != nil {
 		log.Fatal("Error marshalling passwords. ", err)
 		return err
 	}
+	cipherText, err := encrypt(bytes, key)
+	if err != nil {
+		log.Fatal("Error encrypting credentials. ", err)
+		return err
+	}
+
+	enc := EncryptedContent{
+		Salt:       salt,
+		CipherText: cipherText,
+	}
+	bytes, err = bare.Marshal(&enc)
+	if err != nil {
+		log.Fatal("Error marshalling encrypted credentials. ", err)
+		return err
+	}
+	_, err = result.Write(bytes)
+	if err != nil {
+		log.Fatal("Error writing to file. ", err)
+		return err
+	}
 	_, err = result.Write(bytes)
 	if err != nil {
 		log.Fatal("Error writing to file. ", err)
@@ -49,14 +88,30 @@ 	}
 	return nil
 }
 
-func Read() ([]password.BarePassword, error) {
+// todo memguard masterPassword
+func Read(masterPassword string) ([]password.BarePassword, error) {
+	enc := EncryptedContent{}
+	// todo memguard
 	passwords := []password.BarePassword{}
 	content, err := ioutil.ReadFile(getDataLocation() + "/passwords.bare")
 	if err != nil {
 		log.Fatal("Error reading to file. ", err)
 		return nil, err
 	}
-	err = bare.Unmarshal(content, &passwords)
+	err = bare.Unmarshal(content, &enc)
+	if err != nil {
+		log.Fatal("Error unmarshalling encrypted. ", err)
+		return passwords, err
+	}
+	// todo memguard
+	key := deriveKey(masterPassword, enc.Salt)
+	// todo memguard
+	plaintext, err := decrypt(enc.CipherText, key)
+	if err != nil {
+		log.Fatal("Error decrypting passwords. ", err)
+		return passwords, err
+	}
+	err = bare.Unmarshal(plaintext, &passwords)
 	if err != nil {
 		log.Fatal("Error unmarshalling. ", err)
 		return nil, err
@@ -64,18 +119,39 @@ 	}
 	return passwords, nil
 }
 
-func SaveCredentials(credentials Credentials) error {
+// todo memguard credentials, masterPassword
+func SaveCredentials(credentials Credentials, masterPassword string) error {
+	salt := makeSalt()
+	// todo memguard
+	key := deriveKey(masterPassword, salt)
+
 	result, err := os.Create(getDataLocation() + "/credentials.bare")
 	if err != nil {
 		log.Fatal("Error creating credentials file. ", err)
 		return err
 	}
 	defer result.Close()
+	// todo memguard
 	bytes, err := bare.Marshal(&credentials)
 	if err != nil {
 		log.Fatal("Error marshalling credentials. ", err)
 		return err
 	}
+	cipherText, err := encrypt(bytes, key)
+	if err != nil {
+		log.Fatal("Error encrypting credentials. ", err)
+		return err
+	}
+
+	enc := EncryptedContent{
+		Salt:       salt,
+		CipherText: cipherText,
+	}
+	bytes, err = bare.Marshal(&enc)
+	if err != nil {
+		log.Fatal("Error marshalling encrypted credentials. ", err)
+		return err
+	}
 	_, err = result.Write(bytes)
 	if err != nil {
 		log.Fatal("Error writing to file. ", err)
@@ -84,17 +160,92 @@ 	}
 	return nil
 }
 
-func ReadCredentials() (Credentials, error) {
+// todo memguard masterPassword
+func ReadCredentials(masterPassword string) (Credentials, error) {
+	enc := EncryptedContent{}
+	// todo memguard
 	credentials := Credentials{}
 	content, err := ioutil.ReadFile(getDataLocation() + "/credentials.bare")
 	if err != nil {
 		log.Fatal("Error reading to file. ", err)
 		return credentials, err
 	}
-	err = bare.Unmarshal(content, &credentials)
+	err = bare.Unmarshal(content, &enc)
+	if err != nil {
+		log.Fatal("Error unmarshalling encrypted. ", err)
+		return credentials, err
+	}
+	// todo memguard
+	key := deriveKey(masterPassword, enc.Salt)
+	// todo memguard
+	plaintext, err := decrypt(enc.CipherText, key)
 	if err != nil {
-		log.Fatal("Error unmarshalling. ", err)
+		log.Fatal("Error decrypting credentials. ", err)
 		return credentials, err
 	}
+	err = bare.Unmarshal(plaintext, &credentials)
 	return credentials, nil
 }
+
+func makeSalt() []byte {
+	key := [32]byte{}
+	_, err := io.ReadFull(rand.Reader, key[:])
+	if err != nil {
+		panic(err)
+	}
+	return key[:]
+}
+
+// todo memguard password
+func deriveKey(password string, salt []byte) [32]byte {
+	// todo memguard
+	key := argon2.IDKey([]byte(password), salt, 1, 64*1024, 4, 32)
+	// todo memguard
+	var keyArr [32]byte
+	copy(keyArr[:], key)
+	return keyArr
+}
+
+// todo memguard plaintext, key
+func encrypt(plaintext []byte, key [32]byte) (ciphertext []byte, err error) {
+	block, err := aes.NewCipher(key[:])
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	nonce := make([]byte, gcm.NonceSize())
+	_, err = io.ReadFull(rand.Reader, nonce)
+	if err != nil {
+		return nil, err
+	}
+
+	return gcm.Seal(nonce, nonce, plaintext, nil), nil
+}
+
+// todo memguard key
+func decrypt(ciphertext []byte, key [32]byte) (plaintext []byte, err error) {
+	block, err := aes.NewCipher(key[:])
+	if err != nil {
+		return nil, err
+	}
+
+	gcm, err := cipher.NewGCM(block)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(ciphertext) < gcm.NonceSize() {
+		return nil, errors.New("malformed ciphertext")
+	}
+
+	return gcm.Open(nil,
+		ciphertext[:gcm.NonceSize()],
+		ciphertext[gcm.NonceSize():],
+		nil,
+	)
+}




diff --git a/operation/get.go b/operation/get.go
index 26c7a26c70a62f22af60806e0330dd20a93a2129..afb3171aca61985f3150c6745d59e95450b25504 100644
--- a/operation/get.go
+++ b/operation/get.go
@@ -8,17 +8,21 @@ 	"strings"
 	"errors"
 )
 
-func Get(username, label, url string, full, short bool) (string, error) {
+// todo memguard masterPassword
+func Get(username, label, url string, full, short bool, masterPassword string) (string, error) {
 	if full && short {
 		err := errors.New("Full and short specified at the same time")
 		log.Fatal("Error. ", err)
 		return "", err
 	}
-	passwords, err := fs.Read()
+	// todo memguard
+	passwords, err := fs.Read(masterPassword)
 	if err != nil {
 		return "", err
 	}
+	// todo memguard
 	resultPasswords := []string{}
+	// todo memguard
 	for _, p := range passwords {
 		if (username == "" || username == p.Username) &&
 			(label == "" || label == p.Label) &&
@@ -49,12 +53,16 @@ 	}
 	return strings.Join(resultPasswords, "\n\n"), nil
 }
 
-func List() (string, error) {
-	passwords, err := fs.Read()
+// todo memguard masterPassword
+func List(masterPassword string) (string, error) {
+	// todo memguard
+	passwords, err := fs.Read(masterPassword)
 	if err != nil {
 		return "", err
 	}
+	// todo memguard
 	resultPasswords := []string{}
+	// todo memguard
 	for _, p := range passwords {
 		if p.Username != "" {
 			resultPasswords = append(resultPasswords, p.Label+": "+p.Username)




diff --git a/server/sync.go b/server/sync.go
index e0c266cf6764553fa90df7fe75dee7245367d761..89e6da881e2e7626cd3cb3498132ba3bfba0cb4d 100644
--- a/server/sync.go
+++ b/server/sync.go
@@ -19,7 +19,9 @@ 	Success bool
 	Keys    interface{}
 }
 
+// todo memguard credentials
 func open(credentials fs.Credentials) (string, error) {
+	// todo memguard
 	authorization := base64.StdEncoding.EncodeToString([]byte(credentials.Username + ":" + credentials.Password))
 
 	req, err := http.NewRequest("POST",
@@ -60,13 +62,17 @@ 		log.Fatal("Error authenticating body.")
 		return "", errors.New("No success")
 	}
 
+	// todo memguard
 	session := resp.Header.Get("x-api-session")
 	return session, nil
 }
 
+// todo memguard credentials, session
 func list(credentials fs.Credentials, session string) ([]password.NextPassword, error) {
+	// todo memguard
 	authorization := base64.StdEncoding.EncodeToString([]byte(credentials.Username + ":" + credentials.Password))
 
+	// todo memguard
 	passwords := []password.NextPassword{}
 	req, err := http.NewRequest("POST",
 		credentials.Server+"/index.php/apps/passwords/api/1.0/password/list",
@@ -90,6 +96,7 @@ 		return passwords, err
 	}
 	defer resp.Body.Close()
 
+	// todo memguard
 	body, err := ioutil.ReadAll(resp.Body)
 	if err != nil {
 		log.Fatal("Error reading body. ", err)
@@ -105,21 +112,25 @@
 	return passwords, nil
 }
 
-func Sync() error {
-	credentials, err := fs.ReadCredentials()
+// todo memguard masterPassword
+func Sync(masterPassword string) error {
+	// todo memguard
+	credentials, err := fs.ReadCredentials(masterPassword)
 	if err != nil {
 		return err
 	}
 
+	// todo memguard
 	session, err := open(credentials)
 	if err != nil {
 		return err
 	}
+	// todo memguard
 	passwords, err := list(credentials, session)
 	if err != nil {
 		return err
 	}
-	err = fs.Save(passwords)
+	err = fs.Save(passwords, masterPassword)
 	if err != nil {
 		return err
 	}