diff --git a/README.md b/README.md index 0190a02..24940c8 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,10 @@ # Fosskey CLI +Fosskey CLI (command-line interface) is the simplest way to start using an [encrypted vault][vault-repo] to store your secrets and passwords. + +## What is Fosskey? + Fosskey is a [**F**]ree, [**O**]pen-source, [**S**]ecure, and [**S**]elf-custodial keychain. ## How do "they" store our passwords? @@ -145,6 +149,7 @@ Fosskey does not store the master key. Instead, it uses the Argon2id key-derivat While using the recommended parameters specified in [RFC 9106][rfc9106-params], the encryption/decryption method took about 0.8 seconds to process on a quad-core Intel processor with 16 GiB of memory. If a master key is composed of 8 characters of upper-case (A-Z), lower-case (a-z) letters and numbers (0-9), and symbols (32), there will be a total of 94 possible characters. Therefore, at least a total of B=nP(r-1) brute-force attacks is required to guess the correct master key. Here "B" is the permutation of (n, r-1). Thus, with the target hardware configuration (quad-core, 16 GiB memory), it will take about 1.3 million computation years to brute-force the 8-character long master key. +[vault-repo]: https://github.com/fosskey/vault [chacha20-poly1305]: https://en.wikipedia.org/wiki/ChaCha20-Poly1305 [argon2]: https://en.wikipedia.org/wiki/Argon2 [rfc9106-params]: https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice diff --git a/cmd/delete.go b/cmd/delete.go index 2ca7af8..f534de9 100644 --- a/cmd/delete.go +++ b/cmd/delete.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/fosskey/cli/internal/util" - "github.com/fosskey/cli/internal/vault" + "github.com/fosskey/vault" "github.com/spf13/cobra" ) diff --git a/cmd/fetch.go b/cmd/fetch.go index b468978..b7246c3 100644 --- a/cmd/fetch.go +++ b/cmd/fetch.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/fosskey/cli/internal/util" - "github.com/fosskey/cli/internal/vault" + "github.com/fosskey/vault" "github.com/spf13/cobra" ) diff --git a/cmd/insert.go b/cmd/insert.go index d7220fd..6a5fab8 100644 --- a/cmd/insert.go +++ b/cmd/insert.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/fosskey/cli/internal/util" - "github.com/fosskey/cli/internal/vault" + "github.com/fosskey/vault" "github.com/spf13/cobra" ) diff --git a/cmd/list.go b/cmd/list.go index 52933ac..3eb6499 100644 --- a/cmd/list.go +++ b/cmd/list.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/fosskey/cli/internal/util" - "github.com/fosskey/cli/internal/vault" + "github.com/fosskey/vault" "github.com/spf13/cobra" ) diff --git a/cmd/rekey.go b/cmd/rekey.go index 043a09b..b65ea8b 100644 --- a/cmd/rekey.go +++ b/cmd/rekey.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/fosskey/cli/internal/util" - "github.com/fosskey/cli/internal/vault" + "github.com/fosskey/vault" "github.com/spf13/cobra" ) diff --git a/cmd/root.go b/cmd/root.go index 6a784b6..706f784 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -11,7 +11,7 @@ import ( var rootCmd = &cobra.Command{ Use: "foss", Short: "A free, open-source, secure, and self-custodial keychain", - Version: "0.0.0", + Version: "0.1.0", CompletionOptions: cobra.CompletionOptions{ DisableDefaultCmd: false, HiddenDefaultCmd: true, diff --git a/cmd/update.go b/cmd/update.go index 3c5e761..cd8fdbc 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/fosskey/cli/internal/util" - "github.com/fosskey/cli/internal/vault" + "github.com/fosskey/vault" "github.com/spf13/cobra" ) diff --git a/go.mod b/go.mod index bd05dbd..97563d3 100644 --- a/go.mod +++ b/go.mod @@ -3,13 +3,14 @@ module github.com/fosskey/cli go 1.19 require ( + github.com/fosskey/vault v0.1.0 github.com/spf13/cobra v1.6.1 - golang.org/x/crypto v0.4.0 golang.org/x/term v0.3.0 ) require ( github.com/inconshreveable/mousetrap v1.0.1 // indirect github.com/spf13/pflag v1.0.5 // indirect + golang.org/x/crypto v0.4.0 // indirect golang.org/x/sys v0.3.0 // indirect ) diff --git a/go.sum b/go.sum index 8b4f96f..9332e21 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,6 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/fosskey/vault v0.1.0 h1:Ii7S0SfsesL2wt2QSn9dJn/OXR0mqNL4ABusQ6OHvLg= +github.com/fosskey/vault v0.1.0/go.mod h1:RVYylQ3n2b9U/yzOkAMMhKPJthRfLLzRxdtK6ySKAWM= github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= diff --git a/internal/cipher/cipher.go b/internal/cipher/cipher.go deleted file mode 100644 index 6cd328d..0000000 --- a/internal/cipher/cipher.go +++ /dev/null @@ -1,104 +0,0 @@ -package cipher - -import ( - "crypto/rand" - "errors" - - "golang.org/x/crypto/argon2" - "golang.org/x/crypto/chacha20poly1305" -) - -// Argon2id password hashing config. -// Based on the recommended parameters from RFC 9106: -// https://www.rfc-editor.org/rfc/rfc9106.html#name-parameter-choice -var config = struct { - time uint32 // number of passes - memory uint32 // memory size in KiB - threads uint8 // degree of parallelism - taglen uint32 // tag length in bytes - saltlen int // salt length in bytes -}{ - time: 1, // 1 pass - memory: 2 * 1024 * 1024, // 2 GiB - threads: 4, // 4 lanes - taglen: 32, // 256-bit tag - saltlen: 16, // 128-bit salt -} - -func Encrypt(password, plaintext []byte) ([]byte, error) { - - // Generate random salt and Argon2id key from the password - key, salt, err := deriveKey(password, nil) - if err != nil { - return nil, err - } - - // Get XChaCha20-Poly1305 AEAD from the key - aead, err := chacha20poly1305.NewX(key) - if err != nil { - return nil, err - } - - // Select a random nonce, and leave capacity for the ciphertext - nonce := make([]byte, aead.NonceSize(), aead.NonceSize()+len(plaintext)+aead.Overhead()) - if _, err := rand.Read(nonce); err != nil { - return nil, err - } - - // Encrypt the message and append the ciphertext to the nonce - ciphertext := aead.Seal(nonce, nonce, plaintext, nil) - - // Append the salt - ciphertext = append(ciphertext, salt...) - - return ciphertext, nil -} - -func Decrypt(password, ciphertext []byte) ([]byte, error) { - - // Split salt and ciphertext - salt, ciphertext := ciphertext[len(ciphertext)-config.saltlen:], ciphertext[:len(ciphertext)-config.saltlen] - - // Derive key from salt - key, _, err := deriveKey(password, salt) - if err != nil { - return nil, err - } - - // Get XChaCha20-Poly1305 AEAD from the key - aead, err := chacha20poly1305.NewX(key) - if err != nil { - return nil, err - } - - if len(ciphertext) < aead.NonceSize() { - return nil, errors.New("cipher text too short") - } - - // Split nonce and ciphertext - nonce, ciphertext := ciphertext[:aead.NonceSize()], ciphertext[aead.NonceSize():] - - // Decrypt the message and check it wasn't tampered with - plaintext, err := aead.Open(nil, nonce, ciphertext, nil) - if err != nil { - return nil, err - } - - return plaintext, nil -} - -// Derives a key from the password, salt, and cost parameters using Argon2id -func deriveKey(password, salt []byte) ([]byte, []byte, error) { - - if salt == nil { - // Generate a salt - salt = make([]byte, config.saltlen) - if _, err := rand.Read(salt); err != nil { - return nil, nil, err - } - } - - key := argon2.IDKey(password, salt, config.time, config.memory, config.threads, config.taglen) - - return key, salt, nil -} diff --git a/internal/cipher/cipher_test.go b/internal/cipher/cipher_test.go deleted file mode 100644 index 652bfcf..0000000 --- a/internal/cipher/cipher_test.go +++ /dev/null @@ -1,29 +0,0 @@ -package cipher - -import ( - "bytes" - "testing" -) - -func TestCipher(t *testing.T) { - var password = []byte("MyP@ssw0rd") - var message = []byte("Hello World!") - - ciphertext, err := Encrypt(password, message) - if err != nil { - t.Fatal(err) - } - - if len(ciphertext) <= len(message) { - t.Fatal("Encrypted result must be longer than the original message") - } - - plaintext, err := Decrypt(password, ciphertext) - if err != nil { - t.Fatal(err) - } - - if !bytes.Equal(plaintext, message) { - t.Fatal("Decrypted result did not match with original message") - } -} diff --git a/internal/vault/delete.go b/internal/vault/delete.go deleted file mode 100644 index 34564b7..0000000 --- a/internal/vault/delete.go +++ /dev/null @@ -1,24 +0,0 @@ -package vault - -import "errors" - -func Delete(masterkey, name string) error { - // Read entries - entries, err := read(masterkey) - if err != nil { - return err - } - - if _, exists := entries[name]; !exists { - return errors.New("NotFound") - } - - delete(entries, name) - - // Write to the vault - if err := write(masterkey, entries); err != nil { - return err - } - - return nil -} diff --git a/internal/vault/delete_test.go b/internal/vault/delete_test.go deleted file mode 100644 index 7dceeab..0000000 --- a/internal/vault/delete_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package vault - -import ( - "reflect" - "testing" -) - -func TestDelete(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Delete something from empty vault - if err := Delete(masterkey, "Something"); err == nil || err.Error() != "NotFound" { - t.Fatal("Delete something from an empty vault must return an error(NotFound)") - } - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - "FourthSecretName": "FourthSecretContent", - "FifthSecretName": "FifthSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Delete a non-existent entry from the non-empty vault - if err := Delete(masterkey, "UnicornEgg"); err == nil || err.Error() != "NotFound" { - t.Fatal("Delete of a non-existent entry must return an error(NotFound)") - } - - // Delete an existing entry with an incorrect master key - if err := Delete("AnIncorrectMasterKey", "FirstSecretName"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to delete with an incorrect master key must return an error(AuthFailed)") - } - - // Delete a non-existing entry with an incorrect master key - if err := Delete("AnIncorrectMasterKey", "UnicornEgg"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to delete with an incorrect master key must return an error(AuthFailed)") - } - - // Delete first entry - if err := Delete(masterkey, "FirstSecretName"); err != nil { - t.Fatal(err) - } - - // Read entries - readEntries, err := read(masterkey) - if err != nil || !reflect.DeepEqual(map[string]string{ - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - "FourthSecretName": "FourthSecretContent", - "FifthSecretName": "FifthSecretContent", - }, readEntries) { - t.Fatal("Read result must reflect the deleted entry") - } - - // Delete last entry - if err := Delete(masterkey, "FifthSecretName"); err != nil { - t.Fatal(err) - } - - // Read entries - readEntries, err = read(masterkey) - if err != nil || !reflect.DeepEqual(map[string]string{ - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - "FourthSecretName": "FourthSecretContent", - }, readEntries) { - t.Fatal("Read result must reflect the deleted entry") - } - - // Delete middle entry - if err := Delete(masterkey, "ThirdSecretName"); err != nil { - t.Fatal(err) - } - - // Read entries - readEntries, err = read(masterkey) - if err != nil || !reflect.DeepEqual(map[string]string{ - "SecondSecretName": "SecondSecretContent", - "FourthSecretName": "FourthSecretContent", - }, readEntries) { - t.Fatal("Read result must reflect the deleted entry") - } -} diff --git a/internal/vault/fetch.go b/internal/vault/fetch.go deleted file mode 100644 index b53137a..0000000 --- a/internal/vault/fetch.go +++ /dev/null @@ -1,20 +0,0 @@ -package vault - -import ( - "errors" -) - -func Fetch(masterkey, name string) (string, error) { - - // Read entries - entries, err := read(masterkey) - if err != nil { - return "", err - } - - if _, exists := entries[name]; exists { - return entries[name], nil - } - - return "", errors.New("NotFound") -} diff --git a/internal/vault/fetch_test.go b/internal/vault/fetch_test.go deleted file mode 100644 index 3b124be..0000000 --- a/internal/vault/fetch_test.go +++ /dev/null @@ -1,58 +0,0 @@ -package vault - -import ( - "testing" -) - -func TestFetch(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Fetch something from empty vault - if _, err := Fetch(masterkey, "Something"); err == nil || err.Error() != "NotFound" { - t.Fatal("Fetch something from empty vault must return an error(NotFound)") - } - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Fetch and compare all three entries - for name, secret := range entries { - fetchedSecret, err := Fetch(masterkey, name) - if err != nil { - t.Fatal(err) - } - if fetchedSecret != secret { - t.Fatalf("Expected %q, got %q", secret, fetchedSecret) - } - } - - // Fetch a non-existent entry from the vault - if _, err := Fetch(masterkey, "UnicornEgg"); err == nil || err.Error() != "NotFound" { - t.Fatal("Fetch of a non-existent entry must return an error(NotFound)") - } - - // Fetch an existing entry with an incorrect master key - if _, err := Fetch("AnIncorrectMasterKey", "FirstSecretName"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to fetch with an incorrect master key must return an error(AuthFailed)") - } - - // Fetch a non-existing entry with an incorrect master key - if _, err := Fetch("AnIncorrectMasterKey", "UnicornEgg"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to fetch with an incorrect master key must return an error(AuthFailed)") - } -} diff --git a/internal/vault/insert.go b/internal/vault/insert.go deleted file mode 100644 index 8125141..0000000 --- a/internal/vault/insert.go +++ /dev/null @@ -1,29 +0,0 @@ -package vault - -import ( - "errors" -) - -func Insert(masterkey, name, secret string) error { - - // Read entries - entries, err := read(masterkey) - if err != nil { - return err - } - - // Check if the name already exists - if _, exists := entries[name]; exists { - return errors.New("DuplicateEntry") - } - - // Append the new entry - entries[name] = secret - - // Write to the vault - if err := write(masterkey, entries); err != nil { - return err - } - - return nil -} diff --git a/internal/vault/insert_test.go b/internal/vault/insert_test.go deleted file mode 100644 index e110c64..0000000 --- a/internal/vault/insert_test.go +++ /dev/null @@ -1,44 +0,0 @@ -package vault - -import ( - "reflect" - "testing" -) - -func TestInsert(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Read entries and compare the result - readEntries, err := read(masterkey) - if err != nil || !reflect.DeepEqual(entries, readEntries) { - t.Fatal("Read result must be equal to the inserted entries") - } - - // Insert an entry with an existing name - if err := Insert(masterkey, "FirstSecretName", "Whatever"); err == nil || err.Error() != "DuplicateEntry" { - t.Fatal("Attempt to insert with an existing name must return an error(DuplicateEntry)") - } - - // Insert an entry using an incorrect master key - if err := Insert("AnIncorrectMasterKey", "Something", "Whatever"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to insert with an incorrect master key must return an error(AuthFailed)") - } -} diff --git a/internal/vault/list.go b/internal/vault/list.go deleted file mode 100644 index c7cbed3..0000000 --- a/internal/vault/list.go +++ /dev/null @@ -1,25 +0,0 @@ -package vault - -import "sort" - -// List all names -func List(masterkey string) ([]string, error) { - - // Read entries - entries, err := read(masterkey) - if err != nil { - return nil, err - } - - names := []string{} - - // Iterate thru entries and get the names - for k := range entries { - names = append(names, k) - } - - // Sort the slice - sort.Strings(names) - - return names, nil -} diff --git a/internal/vault/list_test.go b/internal/vault/list_test.go deleted file mode 100644 index 54c6438..0000000 --- a/internal/vault/list_test.go +++ /dev/null @@ -1,53 +0,0 @@ -package vault - -import ( - "reflect" - "sort" - "testing" -) - -func TestList(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // List from empty vault - names, err := List(masterkey) - if err != nil || len(names) != 0 { - t.Fatal("List from empty vault must return an empty slice") - } - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Get the entry names (keys) for future use - entryNames := []string{} - for k := range entries { - entryNames = append(entryNames, k) - } - sort.Strings(entryNames) // Sort for future comparison - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // List from non-empty vault - names, err = List(masterkey) - if err != nil || !reflect.DeepEqual(names, entryNames) { - t.Fatal("List from non-empty vault must return all names") - } - - // List with an incorrect master key - if _, err := List("AnIncorrectMasterKey"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to list with an incorrect master key must return an error(AuthFailed)") - } -} diff --git a/internal/vault/read.go b/internal/vault/read.go deleted file mode 100644 index 43c2784..0000000 --- a/internal/vault/read.go +++ /dev/null @@ -1,43 +0,0 @@ -package vault - -import ( - "errors" - "os" - "strings" - - "github.com/fosskey/cli/internal/cipher" -) - -// Read and decrypt the content of the vault -func read(masterkey string) (map[string]string, error) { - - // Read vault file - encryptedBytes, err := os.ReadFile(vaultPath) - if err != nil { - return nil, err - } - - // Return empty map when vault is empty - if len(encryptedBytes) == 0 { - return make(map[string]string), nil - } - - // Decrypt - decryptedBytes, err := cipher.Decrypt([]byte(masterkey), encryptedBytes) - if err != nil { - return nil, errors.New("AuthFailed") - } - - // Convert to string - plainText := string(decryptedBytes) - - // Map the plain text into key:value entries - entries := make(map[string]string) - lines := strings.Split(plainText, "\n") - for _, line := range lines { - v := strings.Split(line, "\t") - entries[v[0]] = v[1] - } - - return entries, nil -} diff --git a/internal/vault/read_test.go b/internal/vault/read_test.go deleted file mode 100644 index 1274bc3..0000000 --- a/internal/vault/read_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package vault - -import ( - "reflect" - "testing" -) - -func TestRead(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Read entries from empty vault - entries, err := read(masterkey) - if err != nil || len(entries) != 0 { - t.Fatal("Read from empty vault must return an empty map") - } - - // Prepare three entries to be inserted - entries = map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Read and compare the result - readEntries, err := read(masterkey) - if err != nil || !reflect.DeepEqual(entries, readEntries) { - t.Fatal("Read result must be equal to the inserted entries") - } - - // Read with an incorrect master key - if _, err := read("AnIncorrectMasterKey"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to read with an incorrect master key must return an error(AuthFailed)") - } -} diff --git a/internal/vault/rekey.go b/internal/vault/rekey.go deleted file mode 100644 index 293b554..0000000 --- a/internal/vault/rekey.go +++ /dev/null @@ -1,25 +0,0 @@ -package vault - -import "errors" - -// Change the master key -func Rekey(masterkey, newkey string) error { - - // Read entries - entries, err := read(masterkey) - if err != nil { - return err - } - - // Return error when vault is empty - if len(entries) == 0 { - return errors.New("VaultEmpty") - } - - // Write to the vault with new masterkey - if err := write(newkey, entries); err != nil { - return err - } - - return nil -} diff --git a/internal/vault/rekey_test.go b/internal/vault/rekey_test.go deleted file mode 100644 index ab39895..0000000 --- a/internal/vault/rekey_test.go +++ /dev/null @@ -1,55 +0,0 @@ -package vault - -import ( - "reflect" - "testing" -) - -func TestRekey(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Rekey on empty vault - if err := Rekey(masterkey, "Whatever"); err == nil || err.Error() != "VaultEmpty" { - t.Fatal("Rekey on empty vault must return an error(VaultEmpty)") - } - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Rekey on non-empty vault with an incorrect master key - if err := Rekey("AnIncorrectMasterKey", "Whatever"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Rekey on non-empty vault with an incorrect master key must return an error(AuthFailed)") - } - - // Rekey on non-empty vault with the correct master key - newkey := "NewMasterKey!" - if err := Rekey(masterkey, newkey); err != nil { - t.Fatal(err) - } - - // Read with old master key - if _, err := read(masterkey); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to read with old master key must return an error(AuthFailed)") - } - - // Read with new masterkey and compare the result - readEntries, err := read(newkey) - if err != nil || !reflect.DeepEqual(entries, readEntries) { - t.Fatal("Read result must be equal to the inserted entries") - } -} diff --git a/internal/vault/update.go b/internal/vault/update.go deleted file mode 100644 index 697cdd2..0000000 --- a/internal/vault/update.go +++ /dev/null @@ -1,25 +0,0 @@ -package vault - -import "errors" - -func Update(masterkey, name, secret string) error { - - // Read entries - entries, err := read(masterkey) - if err != nil { - return err - } - - if _, exists := entries[name]; !exists { - return errors.New("NotFound") - } - - entries[name] = secret - - // Write to the vault - if err := write(masterkey, entries); err != nil { - return err - } - - return nil -} diff --git a/internal/vault/update_test.go b/internal/vault/update_test.go deleted file mode 100644 index 69f8dce..0000000 --- a/internal/vault/update_test.go +++ /dev/null @@ -1,66 +0,0 @@ -package vault - -import ( - "testing" -) - -func TestUpdate(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Update something from empty vault - if err := Update(masterkey, "Something", "Whatever"); err == nil || err.Error() != "NotFound" { - t.Fatal("Update something in an empty vault must return an error(NotFound)") - } - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Update a non-existent entry in the non-empty vault - if err := Update(masterkey, "UnicornEgg", "Whatever"); err == nil || err.Error() != "NotFound" { - t.Fatal("Update of a non-existent entry must return an error(NotFound)") - } - - // Update an existing entry with an incorrect master key - if err := Update("AnIncorrectMasterKey", "FirstSecretName", "Whatever"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to update with an incorrect master key must return an error(AuthFailed)") - } - - // Update a non-existing entry with an incorrect master key - if err := Update("AnIncorrectMasterKey", "UnicornEgg", "Whatever"); err == nil || err.Error() != "AuthFailed" { - t.Fatal("Attempt to update with an incorrect master key must return an error(AuthFailed)") - } - - // Update all three entries by appending "Updated" to the end - for name, secret := range entries { - err := Update(masterkey, name, secret+"Updated") - if err != nil { - t.Fatal(err) - } - } - - // Fetch and compare all three entries - for name, secret := range entries { - fetchedSecret, err := Fetch(masterkey, name) - if err != nil { - t.Fatal(err) - } - if fetchedSecret != secret+"Updated" { - t.Fatalf("Expected %qUpdated, got %q", secret, fetchedSecret) - } - } -} diff --git a/internal/vault/vault.go b/internal/vault/vault.go deleted file mode 100644 index d1185eb..0000000 --- a/internal/vault/vault.go +++ /dev/null @@ -1,64 +0,0 @@ -package vault - -import ( - "fmt" - "os" - "path" - "path/filepath" - "strings" -) - -var ( - fossPath string - vaultPath string -) - -// Create the vault file inside foss directory if it doesn't exist. -func init() { - - // Set paths - setPaths() - - // Create foss directory if doesn't exist - if _, err := os.Stat(fossPath); err != nil { - err := os.Mkdir(fossPath, os.ModeDir|os.FileMode(0700)) - handleErr(err) - } - - // If the vault file doesn't exist, create it. - if _, err := os.Stat(vaultPath); err != nil { - err := os.WriteFile(vaultPath, nil, os.FileMode(0600)) - handleErr(err) - } -} - -// For tests set fossPath to "/.foss", otherwise "/.foss" -// And set vaultPath to "/vault" -func setPaths() { - if strings.HasSuffix(os.Args[0], ".test") { - // Test case - f, _ := os.Getwd() - rootPath := path.Join(f, "..", "..") - fossPath = filepath.Join(rootPath, ".foss") - } else { - // Run case - homePath, _ := os.UserHomeDir() - fossPath = filepath.Join(homePath, ".foss") - } - vaultPath = filepath.Join(fossPath, "vault") -} - -// Clean up vault file -func cleanup() { - err := os.WriteFile(vaultPath, nil, 0) - handleErr(err) -} - -// Print the error with the prefix "Error:" and exit with error code 1. -// It does nothing if the error is nil. -func handleErr(err error) { - if err != nil { - fmt.Fprintln(os.Stderr, "Error:", err) - os.Exit(1) - } -} diff --git a/internal/vault/vault_test.go b/internal/vault/vault_test.go deleted file mode 100644 index 89546ef..0000000 --- a/internal/vault/vault_test.go +++ /dev/null @@ -1,38 +0,0 @@ -package vault - -import ( - "os" - "testing" -) - -func TestInit(t *testing.T) { - - // There is no need to call init() separately - // Because it will be automatically called before running TestInit - - // Check if foss directory exists - dir, err := os.Stat(fossPath) - if err != nil { - t.Fatal(err) - } - - // Check dir mode - dirmode := dir.Mode() - expected := os.ModeDir | os.FileMode(0700) - if dirmode != expected { - t.Fatalf("Expected dir mode %q but found %q", expected, dirmode) - } - - // Check if vault file exists - file, err := os.Stat(vaultPath) - if err != nil { - t.Fatal(err) - } - - // Check vault file mode - filemode := file.Mode() - expected = os.FileMode(0600) - if filemode != expected { - t.Fatalf("Expected file mode %q but found %q", expected, filemode) - } -} diff --git a/internal/vault/verify.go b/internal/vault/verify.go deleted file mode 100644 index 73b46f2..0000000 --- a/internal/vault/verify.go +++ /dev/null @@ -1,20 +0,0 @@ -package vault - -// Verify the master key, return true if verified -// In case of empty vault, return true -func Verify(masterkey string) (bool, error) { - - // Read entries - entries, err := read(masterkey) - if err != nil { - return false, err - } - - // Return true when vault is empty - if len(entries) == 0 { - return true, nil - } - - // Return true (decryption passed) - return true, nil -} diff --git a/internal/vault/verify_test.go b/internal/vault/verify_test.go deleted file mode 100644 index af8df6c..0000000 --- a/internal/vault/verify_test.go +++ /dev/null @@ -1,45 +0,0 @@ -package vault - -import ( - "testing" -) - -func TestVerify(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Verify master key against empty vault - verified, err := Verify(masterkey) - if err != nil || !verified { - t.Fatal("Verify against empty vault must return true") - } - - // Prepare three entries to be inserted - entries := map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Insert all three entries into the vault - for name, secret := range entries { - if err := Insert(masterkey, name, secret); err != nil { - t.Fatal(err) - } - } - - // Verify master key against non-empty vault - verified, err = Verify(masterkey) - if err != nil || !verified { - t.Fatal("Verify against non-empty vault must return true") - } - - // Verify incorrect master key against non-empty vault - verified, err = Verify("AnIncorrectMasterKey") - if err == nil || verified { - t.Fatal("Verify incorrect master key against non-empty vault must return false, error") - } -} diff --git a/internal/vault/write.go b/internal/vault/write.go deleted file mode 100644 index f0a9034..0000000 --- a/internal/vault/write.go +++ /dev/null @@ -1,39 +0,0 @@ -package vault - -import ( - "os" - "strings" - - "github.com/fosskey/cli/internal/cipher" -) - -// Encrypt entries and write them into the vault -func write(masterkey string, entries map[string]string) error { - - // Return right away if entries is an empty map - if len(entries) == 0 { - return nil - } - - // Flatten all entries into plain text - content := "" - for name, secret := range entries { - content += name + "\t" + secret + "\n" - } - - // Trim the final "\n" - content = strings.Trim(content, "\n") - - // Encrypt - encryptedData, err := cipher.Encrypt([]byte(masterkey), []byte(content)) - if err != nil { - return err - } - - // Write to the vault - if err := os.WriteFile(vaultPath, encryptedData, 0); err != nil { - return err - } - - return nil -} diff --git a/internal/vault/write_test.go b/internal/vault/write_test.go deleted file mode 100644 index d2b9bcc..0000000 --- a/internal/vault/write_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package vault - -import ( - "os" - "reflect" - "testing" -) - -func TestWrite(t *testing.T) { - - // Erase vault content after the test - t.Cleanup(cleanup) - - masterkey := "TheMasterKey!" - - // Write empty entries to the vault - entries := map[string]string{} - if err := write(masterkey, entries); err != nil { - t.Fatal(err) - } - - // Read entries and compare the result - readEntries, err := read(masterkey) - if err != nil || len(readEntries) != 0 { - t.Fatal("Read result must be an empty map") - } - - // Prepare three entries to be inserted - entries = map[string]string{ - "FirstSecretName": "FirstSecretContent", - "SecondSecretName": "SecondSecretContent", - "ThirdSecretName": "ThirdSecretContent", - } - - // Write to the vault - if err := write(masterkey, entries); err != nil { - t.Fatal(err) - } - - // Get vault file info - file, err := os.Stat(vaultPath) - if err != nil { - t.Fatal(err) - } - - // Check vault file size - if file.Size() == 0 { - t.Fatal("Vault file is empty after write") - } - - // Check vault file mode - filemode := file.Mode() - expected := os.FileMode(0600) - if filemode != expected { - t.Fatalf("Expected file mode %q but found %q", expected, filemode) - } - - // Read entries and compare the result - readEntries, err = read(masterkey) - if err != nil || !reflect.DeepEqual(entries, readEntries) { - t.Fatal("Read result must be equal to the inserted entries") - } -}