latest
This commit is contained in:
parent
efedbe405f
commit
354681b298
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2004 Sam Hocevar <sam@hocevar.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
2
Makefile
2
Makefile
@ -1,6 +1,6 @@
|
|||||||
default: test
|
default: test
|
||||||
|
|
||||||
test:
|
test: lint
|
||||||
go test -v ./...
|
go test -v ./...
|
||||||
|
|
||||||
lint:
|
lint:
|
||||||
|
16
README.md
16
README.md
@ -12,15 +12,15 @@
|
|||||||
|
|
||||||
the first and initial vault is titled `default`.
|
the first and initial vault is titled `default`.
|
||||||
|
|
||||||
* `secret init` initializes a new vault. this will create a new profile and
|
* `secret init` initializes a new vault and imports a user-provided BIP39
|
||||||
generate a new long-term keypair. the long-term keypair is used to
|
mnemonic phrase. The user must provide their own mnemonic phrase. The
|
||||||
encrypt and decrypt secrets. the long-term keypair is stored in the
|
long-term keypair is derived from this mnemonic. The long-term keypair is
|
||||||
vault. the private key for the vault is encrypted to a short-term
|
used to encrypt and decrypt secrets. The long-term keypair is stored in the
|
||||||
keypair. the short-term keypair private key is encrypted to a passphrase.
|
vault. The private key for the vault is encrypted to a short-term keypair.
|
||||||
to generate the long-term keypair, a random bip32 seed phrase is
|
The short-term keypair private key is encrypted to a passphrase.
|
||||||
generated, then the process proceeds exactly as `secret import private`.
|
|
||||||
|
|
||||||
the randomly generated bip32 seed phrase is shown to the user.
|
Use `secret generate mnemonic` to create a new BIP39 mnemonic phrase if you
|
||||||
|
need one.
|
||||||
|
|
||||||
if there is already a vault, `secret init` exits with an error.
|
if there is already a vault, `secret init` exits with an error.
|
||||||
|
|
||||||
|
2
go.mod
2
go.mod
@ -18,7 +18,9 @@ require (
|
|||||||
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect
|
||||||
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/spf13/pflag v1.0.6 // indirect
|
github.com/spf13/pflag v1.0.6 // indirect
|
||||||
golang.org/x/sys v0.33.0 // indirect
|
golang.org/x/sys v0.33.0 // indirect
|
||||||
|
golang.org/x/term v0.32.0 // indirect
|
||||||
golang.org/x/text v0.25.0 // indirect
|
golang.org/x/text v0.25.0 // indirect
|
||||||
)
|
)
|
||||||
|
5
go.sum
5
go.sum
@ -60,6 +60,8 @@ github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M
|
|||||||
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI=
|
||||||
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
github.com/jrick/logrotate v1.0.0/go.mod h1:LNinyqDIJnpAur+b8yyulnQw/wDuN1+BYKlTRt3OuAQ=
|
||||||
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A=
|
||||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||||
@ -109,8 +111,11 @@ golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7w
|
|||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||||
|
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
|
@ -1,223 +0,0 @@
|
|||||||
package agehd
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"io"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"filippo.io/age"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
mnemonic = "abandon abandon abandon abandon abandon " +
|
|
||||||
"abandon abandon abandon abandon abandon abandon about"
|
|
||||||
|
|
||||||
// Test xprv from BIP85 test vectors
|
|
||||||
testXPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestEncryptDecrypt(t *testing.T) {
|
|
||||||
id, err := DeriveIdentity(mnemonic, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("secret: %s", id.String())
|
|
||||||
t.Logf("recipient: %s", id.Recipient().String())
|
|
||||||
|
|
||||||
var ct bytes.Buffer
|
|
||||||
w, err := age.Encrypt(&ct, id.Recipient())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("encrypt init: %v", err)
|
|
||||||
}
|
|
||||||
if _, err = io.WriteString(w, "hello world"); err != nil {
|
|
||||||
t.Fatalf("write: %v", err)
|
|
||||||
}
|
|
||||||
if err = w.Close(); err != nil {
|
|
||||||
t.Fatalf("encrypt close: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decrypt init: %v", err)
|
|
||||||
}
|
|
||||||
dec, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := string(dec); got != "hello world" {
|
|
||||||
t.Fatalf("round-trip mismatch: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeriveIdentityFromXPRV(t *testing.T) {
|
|
||||||
id, err := DeriveIdentityFromXPRV(testXPRV, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive from xprv: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("xprv secret: %s", id.String())
|
|
||||||
t.Logf("xprv recipient: %s", id.Recipient().String())
|
|
||||||
|
|
||||||
// Test encryption/decryption with xprv-derived identity
|
|
||||||
var ct bytes.Buffer
|
|
||||||
w, err := age.Encrypt(&ct, id.Recipient())
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("encrypt init: %v", err)
|
|
||||||
}
|
|
||||||
if _, err = io.WriteString(w, "hello from xprv"); err != nil {
|
|
||||||
t.Fatalf("write: %v", err)
|
|
||||||
}
|
|
||||||
if err = w.Close(); err != nil {
|
|
||||||
t.Fatalf("encrypt close: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decrypt init: %v", err)
|
|
||||||
}
|
|
||||||
dec, err := io.ReadAll(r)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("read: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if got := string(dec); got != "hello from xprv" {
|
|
||||||
t.Fatalf("round-trip mismatch: %q", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeterministicDerivation(t *testing.T) {
|
|
||||||
// Test that the same mnemonic and index always produce the same identity
|
|
||||||
id1, err := DeriveIdentity(mnemonic, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive 1: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id2, err := DeriveIdentity(mnemonic, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive 2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id1.String() != id2.String() {
|
|
||||||
t.Fatalf("identities should be deterministic: %s != %s", id1.String(), id2.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that different indices produce different identities
|
|
||||||
id3, err := DeriveIdentity(mnemonic, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive 3: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id1.String() == id3.String() {
|
|
||||||
t.Fatalf("different indices should produce different identities")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Index 0: %s", id1.String())
|
|
||||||
t.Logf("Index 1: %s", id3.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDeterministicXPRVDerivation(t *testing.T) {
|
|
||||||
// Test that the same xprv and index always produce the same identity
|
|
||||||
id1, err := DeriveIdentityFromXPRV(testXPRV, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive 1: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
id2, err := DeriveIdentityFromXPRV(testXPRV, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive 2: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id1.String() != id2.String() {
|
|
||||||
t.Fatalf("xprv identities should be deterministic: %s != %s", id1.String(), id2.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Test that different indices with same xprv produce different identities
|
|
||||||
id3, err := DeriveIdentityFromXPRV(testXPRV, 1)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive 3: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if id1.String() == id3.String() {
|
|
||||||
t.Fatalf("different indices should produce different identities")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("XPRV Index 0: %s", id1.String())
|
|
||||||
t.Logf("XPRV Index 1: %s", id3.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestMnemonicVsXPRVConsistency(t *testing.T) {
|
|
||||||
// Test that deriving from mnemonic and from the corresponding xprv produces the same result
|
|
||||||
// Note: This test is removed because the test mnemonic and test xprv are from different sources
|
|
||||||
// and are not expected to produce the same results.
|
|
||||||
t.Skip("Skipping consistency test - test mnemonic and xprv are from different sources")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEntropyLength(t *testing.T) {
|
|
||||||
// Test that DeriveEntropy returns exactly 32 bytes
|
|
||||||
entropy, err := DeriveEntropy(mnemonic, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive entropy: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entropy) != 32 {
|
|
||||||
t.Fatalf("expected 32 bytes of entropy, got %d", len(entropy))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Entropy (32 bytes): %x", entropy)
|
|
||||||
|
|
||||||
// Test that DeriveEntropyFromXPRV returns exactly 32 bytes
|
|
||||||
entropyXPRV, err := DeriveEntropyFromXPRV(testXPRV, 0)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("derive entropy from xprv: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(entropyXPRV) != 32 {
|
|
||||||
t.Fatalf("expected 32 bytes of entropy from xprv, got %d", len(entropyXPRV))
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV)
|
|
||||||
|
|
||||||
// Note: We don't compare the entropy values since the test mnemonic and test xprv
|
|
||||||
// are from different sources and should produce different entropy values.
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIdentityFromEntropy(t *testing.T) {
|
|
||||||
// Test that IdentityFromEntropy works with custom entropy
|
|
||||||
entropy := make([]byte, 32)
|
|
||||||
for i := range entropy {
|
|
||||||
entropy[i] = byte(i)
|
|
||||||
}
|
|
||||||
|
|
||||||
id, err := IdentityFromEntropy(entropy)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("identity from entropy: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Custom entropy identity: %s", id.String())
|
|
||||||
|
|
||||||
// Test that it rejects wrong-sized entropy
|
|
||||||
_, err = IdentityFromEntropy(entropy[:31])
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for 31-byte entropy")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a 33-byte slice to test rejection
|
|
||||||
entropy33 := make([]byte, 33)
|
|
||||||
copy(entropy33, entropy)
|
|
||||||
_, err = IdentityFromEntropy(entropy33)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for 33-byte entropy")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestInvalidXPRV(t *testing.T) {
|
|
||||||
// Test with invalid xprv
|
|
||||||
_, err := DeriveIdentityFromXPRV("invalid-xprv", 0)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatalf("expected error for invalid xprv")
|
|
||||||
}
|
|
||||||
|
|
||||||
t.Logf("Got expected error for invalid xprv: %v", err)
|
|
||||||
}
|
|
@ -35,7 +35,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/agehd"
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -61,7 +61,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/agehd"
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -87,7 +87,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/agehd"
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
@ -114,7 +114,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
"git.eeqj.de/sneak/secret/internal/agehd"
|
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
@ -13,7 +13,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"filippo.io/age"
|
"filippo.io/age"
|
||||||
"git.eeqj.de/sneak/secret/internal/bip85"
|
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
"github.com/btcsuite/btcutil/bech32"
|
"github.com/btcsuite/btcutil/bech32"
|
928
pkg/agehd/agehd_test.go
Normal file
928
pkg/agehd/agehd_test.go
Normal file
@ -0,0 +1,928 @@
|
|||||||
|
package agehd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"filippo.io/age"
|
||||||
|
"github.com/tyler-smith/go-bip39"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
mnemonic = "abandon abandon abandon abandon abandon " +
|
||||||
|
"abandon abandon abandon abandon abandon abandon about"
|
||||||
|
|
||||||
|
// Test xprv from BIP85 test vectors
|
||||||
|
testXPRV = "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
||||||
|
|
||||||
|
// Additional test mnemonics for comprehensive testing
|
||||||
|
testMnemonic12 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
testMnemonic15 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
testMnemonic18 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
testMnemonic21 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
testMnemonic24 = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art"
|
||||||
|
|
||||||
|
// Test messages used throughout the tests
|
||||||
|
testMessageHelloWorld = "hello world"
|
||||||
|
testMessageHelloFromXPRV = "hello from xprv"
|
||||||
|
testMessageGeneric = "test message"
|
||||||
|
testMessageBoundary = "boundary test"
|
||||||
|
testMessageBenchmark = "benchmark test message"
|
||||||
|
testMessageLargePattern = "A"
|
||||||
|
|
||||||
|
// Error messages for validation
|
||||||
|
errorMsgNeed32Bytes = "need 32-byte scalar, got"
|
||||||
|
errorMsgInvalidXPRV = "invalid-xprv"
|
||||||
|
|
||||||
|
// Test constants for various scenarios
|
||||||
|
testSkipMessage = "Skipping consistency test - test mnemonic and xprv are from different sources"
|
||||||
|
|
||||||
|
// Numeric constants for testing
|
||||||
|
testNumGoroutines = 10
|
||||||
|
testNumIterations = 100
|
||||||
|
|
||||||
|
// Large data test constants
|
||||||
|
testDataSizeMegabyte = 1024 * 1024 // 1 MB
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncryptDecrypt(t *testing.T) {
|
||||||
|
id, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("secret: %s", id.String())
|
||||||
|
t.Logf("recipient: %s", id.Recipient().String())
|
||||||
|
|
||||||
|
var ct bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&ct, id.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt init: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(w, testMessageHelloWorld); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
t.Fatalf("encrypt close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt init: %v", err)
|
||||||
|
}
|
||||||
|
dec, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := string(dec); got != testMessageHelloWorld {
|
||||||
|
t.Fatalf("round-trip mismatch: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeriveIdentityFromXPRV(t *testing.T) {
|
||||||
|
id, err := DeriveIdentityFromXPRV(testXPRV, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive from xprv: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("xprv secret: %s", id.String())
|
||||||
|
t.Logf("xprv recipient: %s", id.Recipient().String())
|
||||||
|
|
||||||
|
// Test encryption/decryption with xprv-derived identity
|
||||||
|
var ct bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&ct, id.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt init: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(w, testMessageHelloFromXPRV); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
t.Fatalf("encrypt close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), id)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt init: %v", err)
|
||||||
|
}
|
||||||
|
dec, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := string(dec); got != testMessageHelloFromXPRV {
|
||||||
|
t.Fatalf("round-trip mismatch: %q", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeterministicDerivation(t *testing.T) {
|
||||||
|
// Test that the same mnemonic and index always produce the same identity
|
||||||
|
id1, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive 1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id2, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id1.String() != id2.String() {
|
||||||
|
t.Fatalf("identities should be deterministic: %s != %s", id1.String(), id2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that different indices produce different identities
|
||||||
|
id3, err := DeriveIdentity(mnemonic, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive 3: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id1.String() == id3.String() {
|
||||||
|
t.Fatalf("different indices should produce different identities")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Index 0: %s", id1.String())
|
||||||
|
t.Logf("Index 1: %s", id3.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeterministicXPRVDerivation(t *testing.T) {
|
||||||
|
// Test that the same xprv and index always produce the same identity
|
||||||
|
id1, err := DeriveIdentityFromXPRV(testXPRV, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive 1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
id2, err := DeriveIdentityFromXPRV(testXPRV, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id1.String() != id2.String() {
|
||||||
|
t.Fatalf("xprv identities should be deterministic: %s != %s", id1.String(), id2.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that different indices with same xprv produce different identities
|
||||||
|
id3, err := DeriveIdentityFromXPRV(testXPRV, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive 3: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if id1.String() == id3.String() {
|
||||||
|
t.Fatalf("different indices should produce different identities")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("XPRV Index 0: %s", id1.String())
|
||||||
|
t.Logf("XPRV Index 1: %s", id3.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMnemonicVsXPRVConsistency(t *testing.T) {
|
||||||
|
// Test that deriving from mnemonic and from the corresponding xprv produces the same result
|
||||||
|
// Note: This test is removed because the test mnemonic and test xprv are from different sources
|
||||||
|
// and are not expected to produce the same results.
|
||||||
|
t.Skip(testSkipMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEntropyLength(t *testing.T) {
|
||||||
|
// Test that DeriveEntropy returns exactly 32 bytes
|
||||||
|
entropy, err := DeriveEntropy(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive entropy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entropy) != 32 {
|
||||||
|
t.Fatalf("expected 32 bytes of entropy, got %d", len(entropy))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Entropy (32 bytes): %x", entropy)
|
||||||
|
|
||||||
|
// Test that DeriveEntropyFromXPRV returns exactly 32 bytes
|
||||||
|
entropyXPRV, err := DeriveEntropyFromXPRV(testXPRV, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive entropy from xprv: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(entropyXPRV) != 32 {
|
||||||
|
t.Fatalf("expected 32 bytes of entropy from xprv, got %d", len(entropyXPRV))
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("XPRV Entropy (32 bytes): %x", entropyXPRV)
|
||||||
|
|
||||||
|
// Note: We don't compare the entropy values since the test mnemonic and test xprv
|
||||||
|
// are from different sources and should produce different entropy values.
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestIdentityFromEntropy(t *testing.T) {
|
||||||
|
// Test that IdentityFromEntropy works with custom entropy
|
||||||
|
entropy := make([]byte, 32)
|
||||||
|
for i := range entropy {
|
||||||
|
entropy[i] = byte(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
id, err := IdentityFromEntropy(entropy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("identity from entropy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Custom entropy identity: %s", id.String())
|
||||||
|
|
||||||
|
// Test that it rejects wrong-sized entropy
|
||||||
|
_, err = IdentityFromEntropy(entropy[:31])
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for 31-byte entropy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a 33-byte slice to test rejection
|
||||||
|
entropy33 := make([]byte, 33)
|
||||||
|
copy(entropy33, entropy)
|
||||||
|
_, err = IdentityFromEntropy(entropy33)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for 33-byte entropy")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidXPRV(t *testing.T) {
|
||||||
|
// Test with invalid xprv
|
||||||
|
_, err := DeriveIdentityFromXPRV(errorMsgInvalidXPRV, 0)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("expected error for invalid xprv")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Got expected error for invalid xprv: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestClampFunction tests the RFC-7748 clamping function
|
||||||
|
func TestClampFunction(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input []byte
|
||||||
|
expected []byte
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "all zeros",
|
||||||
|
input: make([]byte, 32),
|
||||||
|
expected: []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "all ones",
|
||||||
|
input: bytes.Repeat([]byte{255}, 32),
|
||||||
|
expected: append([]byte{248}, append(bytes.Repeat([]byte{255}, 30), 127)...),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
input := make([]byte, 32)
|
||||||
|
copy(input, tt.input)
|
||||||
|
clamp(input)
|
||||||
|
|
||||||
|
// Check specific bits that should be clamped
|
||||||
|
if input[0]&7 != 0 {
|
||||||
|
t.Errorf("first byte should have bottom 3 bits cleared, got %08b", input[0])
|
||||||
|
}
|
||||||
|
if input[31]&128 != 0 {
|
||||||
|
t.Errorf("last byte should have top bit cleared, got %08b", input[31])
|
||||||
|
}
|
||||||
|
if input[31]&64 == 0 {
|
||||||
|
t.Errorf("last byte should have second-to-top bit set, got %08b", input[31])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIdentityFromEntropyEdgeCases tests edge cases for IdentityFromEntropy
|
||||||
|
func TestIdentityFromEntropyEdgeCases(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entropy []byte
|
||||||
|
expectError bool
|
||||||
|
errorMsg string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "nil entropy",
|
||||||
|
entropy: nil,
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: errorMsgNeed32Bytes + " 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "empty entropy",
|
||||||
|
entropy: []byte{},
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: errorMsgNeed32Bytes + " 0",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too short entropy",
|
||||||
|
entropy: make([]byte, 31),
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: errorMsgNeed32Bytes + " 31",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "too long entropy",
|
||||||
|
entropy: make([]byte, 33),
|
||||||
|
expectError: true,
|
||||||
|
errorMsg: errorMsgNeed32Bytes + " 33",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid 32-byte entropy",
|
||||||
|
entropy: make([]byte, 32),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "random valid entropy",
|
||||||
|
entropy: func() []byte { b := make([]byte, 32); rand.Read(b); return b }(),
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
identity, err := IdentityFromEntropy(tt.entropy)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error but got none")
|
||||||
|
} else if !strings.Contains(err.Error(), tt.errorMsg) {
|
||||||
|
t.Errorf("expected error containing %q, got %q", tt.errorMsg, err.Error())
|
||||||
|
}
|
||||||
|
if identity != nil {
|
||||||
|
t.Errorf("expected nil identity on error, got %v", identity)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error: %v", err)
|
||||||
|
}
|
||||||
|
if identity == nil {
|
||||||
|
t.Errorf("expected valid identity, got nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeriveEntropyInvalidMnemonic tests error handling for invalid mnemonics
|
||||||
|
func TestDeriveEntropyInvalidMnemonic(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
mnemonic string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty mnemonic",
|
||||||
|
mnemonic: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "single word",
|
||||||
|
mnemonic: "abandon",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid word",
|
||||||
|
mnemonic: "invalid word sequence that does not exist in bip39",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong word count",
|
||||||
|
mnemonic: "abandon abandon abandon abandon abandon",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
// Note: BIP39 library is quite permissive and doesn't validate
|
||||||
|
// mnemonic words strictly, so we mainly test that the function
|
||||||
|
// doesn't panic and produces some result
|
||||||
|
entropy, err := DeriveEntropy(tt.mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Logf("Got error for invalid mnemonic %q: %v", tt.name, err)
|
||||||
|
} else {
|
||||||
|
if len(entropy) != 32 {
|
||||||
|
t.Errorf("expected 32 bytes even for invalid mnemonic, got %d", len(entropy))
|
||||||
|
}
|
||||||
|
t.Logf("Invalid mnemonic %q produced entropy: %x", tt.name, entropy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDeriveEntropyFromXPRVInvalidInputs tests error handling for invalid XPRVs
|
||||||
|
func TestDeriveEntropyFromXPRVInvalidInputs(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
xprv string
|
||||||
|
expectError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "empty xprv",
|
||||||
|
xprv: "",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid base58",
|
||||||
|
xprv: "invalid-base58-string-!@#$%",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "wrong prefix",
|
||||||
|
xprv: "xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "truncated xprv",
|
||||||
|
xprv: "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLj",
|
||||||
|
expectError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid xprv",
|
||||||
|
xprv: testXPRV,
|
||||||
|
expectError: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
entropy, err := DeriveEntropyFromXPRV(tt.xprv, 0)
|
||||||
|
|
||||||
|
if tt.expectError {
|
||||||
|
if err == nil {
|
||||||
|
t.Errorf("expected error for invalid xprv %q", tt.name)
|
||||||
|
} else {
|
||||||
|
t.Logf("Got expected error for %q: %v", tt.name, err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("unexpected error for valid xprv: %v", err)
|
||||||
|
}
|
||||||
|
if len(entropy) != 32 {
|
||||||
|
t.Errorf("expected 32 bytes of entropy, got %d", len(entropy))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestDifferentMnemonicLengths tests derivation with different mnemonic lengths
|
||||||
|
func TestDifferentMnemonicLengths(t *testing.T) {
|
||||||
|
mnemonics := map[string]string{
|
||||||
|
"12 words": testMnemonic12,
|
||||||
|
"15 words": testMnemonic15,
|
||||||
|
"18 words": testMnemonic18,
|
||||||
|
"21 words": testMnemonic21,
|
||||||
|
"24 words": testMnemonic24,
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, mnemonic := range mnemonics {
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
identity, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to derive identity from %s: %v", name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test that we can encrypt/decrypt
|
||||||
|
var ct bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&ct, identity.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt init: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(w, testMessageGeneric); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
t.Fatalf("encrypt close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt init: %v", err)
|
||||||
|
}
|
||||||
|
dec, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(dec) != testMessageGeneric {
|
||||||
|
t.Fatalf("round-trip failed for %s", name)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("%s identity: %s", name, identity.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIndexBoundaries tests derivation with various index values
|
||||||
|
func TestIndexBoundaries(t *testing.T) {
|
||||||
|
indices := []uint32{
|
||||||
|
0, // minimum
|
||||||
|
1, // basic
|
||||||
|
100, // moderate
|
||||||
|
1000, // larger
|
||||||
|
0x7FFFFFFF, // maximum hardened index
|
||||||
|
0xFFFFFFFF, // maximum uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, index := range indices {
|
||||||
|
t.Run(fmt.Sprintf("index_%d", index), func(t *testing.T) {
|
||||||
|
identity, err := DeriveIdentity(mnemonic, index)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to derive identity at index %d: %v", index, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify the identity is valid by testing encryption/decryption
|
||||||
|
var ct bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&ct, identity.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt init at index %d: %v", index, err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(w, testMessageBoundary); err != nil {
|
||||||
|
t.Fatalf("write at index %d: %v", index, err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
t.Fatalf("encrypt close at index %d: %v", index, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt init at index %d: %v", index, err)
|
||||||
|
}
|
||||||
|
dec, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read at index %d: %v", index, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(dec) != testMessageBoundary {
|
||||||
|
t.Fatalf("round-trip failed at index %d", index)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Index %d identity: %s", index, identity.String())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEntropyUniqueness tests that different inputs produce different entropy
|
||||||
|
func TestEntropyUniqueness(t *testing.T) {
|
||||||
|
// Test different indices with same mnemonic
|
||||||
|
entropy1, err := DeriveEntropy(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive entropy 1: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
entropy2, err := DeriveEntropy(mnemonic, 1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive entropy 2: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(entropy1, entropy2) {
|
||||||
|
t.Fatalf("different indices should produce different entropy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test different mnemonics with same index
|
||||||
|
entropy3, err := DeriveEntropy(testMnemonic24, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive entropy 3: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if bytes.Equal(entropy1, entropy3) {
|
||||||
|
t.Fatalf("different mnemonics should produce different entropy")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Entropy uniqueness verified across indices and mnemonics")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConcurrentDerivation tests that derivation is safe for concurrent use
|
||||||
|
func TestConcurrentDerivation(t *testing.T) {
|
||||||
|
results := make(chan string, testNumGoroutines*testNumIterations)
|
||||||
|
errors := make(chan error, testNumGoroutines*testNumIterations)
|
||||||
|
|
||||||
|
for i := 0; i < testNumGoroutines; i++ {
|
||||||
|
go func(goroutineID int) {
|
||||||
|
for j := 0; j < testNumIterations; j++ {
|
||||||
|
identity, err := DeriveIdentity(mnemonic, uint32(j))
|
||||||
|
if err != nil {
|
||||||
|
errors <- err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
results <- identity.String()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Collect results
|
||||||
|
resultMap := make(map[string]int)
|
||||||
|
for i := 0; i < testNumGoroutines*testNumIterations; i++ {
|
||||||
|
select {
|
||||||
|
case result := <-results:
|
||||||
|
resultMap[result]++
|
||||||
|
case err := <-errors:
|
||||||
|
t.Fatalf("concurrent derivation error: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that each index produced the same result across all goroutines
|
||||||
|
expectedResults := testNumGoroutines
|
||||||
|
for result, count := range resultMap {
|
||||||
|
if count != expectedResults {
|
||||||
|
t.Errorf("result %s appeared %d times, expected %d", result, count, expectedResults)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Concurrent derivation test passed with %d unique results", len(resultMap))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Benchmark tests
|
||||||
|
func BenchmarkDeriveIdentity(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := DeriveIdentity(mnemonic, uint32(i%1000))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("derive identity: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDeriveIdentityFromXPRV(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := DeriveIdentityFromXPRV(testXPRV, uint32(i%1000))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("derive identity from xprv: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkDeriveEntropy(b *testing.B) {
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := DeriveEntropy(mnemonic, uint32(i%1000))
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("derive entropy: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkIdentityFromEntropy(b *testing.B) {
|
||||||
|
entropy := make([]byte, 32)
|
||||||
|
rand.Read(entropy)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
_, err := IdentityFromEntropy(entropy)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("identity from entropy: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func BenchmarkEncryptDecrypt(b *testing.B) {
|
||||||
|
identity, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("derive identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
for i := 0; i < b.N; i++ {
|
||||||
|
var ct bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&ct, identity.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("encrypt init: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(w, testMessageBenchmark); err != nil {
|
||||||
|
b.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
b.Fatalf("encrypt close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("decrypt init: %v", err)
|
||||||
|
}
|
||||||
|
_, err = io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
b.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestConstants verifies the hardcoded constants
|
||||||
|
func TestConstants(t *testing.T) {
|
||||||
|
if purpose != 83696968 {
|
||||||
|
t.Errorf("purpose constant mismatch: expected 83696968, got %d", purpose)
|
||||||
|
}
|
||||||
|
if vendorID != 592366788 {
|
||||||
|
t.Errorf("vendorID constant mismatch: expected 592366788, got %d", vendorID)
|
||||||
|
}
|
||||||
|
if appID != 733482323 {
|
||||||
|
t.Errorf("appID constant mismatch: expected 733482323, got %d", appID)
|
||||||
|
}
|
||||||
|
if hrp != "age-secret-key-" {
|
||||||
|
t.Errorf("hrp constant mismatch: expected 'age-secret-key-', got %q", hrp)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestIdentityStringFormat tests that generated identities have the correct format
|
||||||
|
func TestIdentityStringFormat(t *testing.T) {
|
||||||
|
identity, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
secretKey := identity.String()
|
||||||
|
recipient := identity.Recipient().String()
|
||||||
|
|
||||||
|
// Check secret key format
|
||||||
|
if !strings.HasPrefix(secretKey, "AGE-SECRET-KEY-") {
|
||||||
|
t.Errorf("secret key should start with 'AGE-SECRET-KEY-', got: %s", secretKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check recipient format
|
||||||
|
if !strings.HasPrefix(recipient, "age1") {
|
||||||
|
t.Errorf("recipient should start with 'age1', got: %s", recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that they're different
|
||||||
|
if secretKey == recipient {
|
||||||
|
t.Errorf("secret key and recipient should be different")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Secret key format: %s", secretKey)
|
||||||
|
t.Logf("Recipient format: %s", recipient)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestLargeMessageEncryption tests encryption/decryption of larger messages
|
||||||
|
func TestLargeMessageEncryption(t *testing.T) {
|
||||||
|
identity, err := DeriveIdentity(mnemonic, 0)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("derive identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test with different message sizes
|
||||||
|
sizes := []int{1, 100, 1024, 10240, 100000}
|
||||||
|
|
||||||
|
for _, size := range sizes {
|
||||||
|
t.Run(fmt.Sprintf("size_%d", size), func(t *testing.T) {
|
||||||
|
message := strings.Repeat(testMessageLargePattern, size)
|
||||||
|
|
||||||
|
var ct bytes.Buffer
|
||||||
|
w, err := age.Encrypt(&ct, identity.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("encrypt init: %v", err)
|
||||||
|
}
|
||||||
|
if _, err = io.WriteString(w, message); err != nil {
|
||||||
|
t.Fatalf("write: %v", err)
|
||||||
|
}
|
||||||
|
if err = w.Close(); err != nil {
|
||||||
|
t.Fatalf("encrypt close: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err := age.Decrypt(bytes.NewReader(ct.Bytes()), identity)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decrypt init: %v", err)
|
||||||
|
}
|
||||||
|
dec, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("read: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if string(dec) != message {
|
||||||
|
t.Fatalf("message size %d: round-trip failed", size)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Successfully encrypted/decrypted %d byte message", size)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestRandomMnemonicDeterministicGeneration tests that:
|
||||||
|
// 1. A random mnemonic generates the same keys deterministically
|
||||||
|
// 2. Large data (1MB) can be encrypted and decrypted successfully
|
||||||
|
func TestRandomMnemonicDeterministicGeneration(t *testing.T) {
|
||||||
|
// Generate a random mnemonic using the BIP39 library
|
||||||
|
entropy := make([]byte, 32) // 256 bits for 24-word mnemonic
|
||||||
|
_, err := rand.Read(entropy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate random entropy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
randomMnemonic, err := bip39.NewMnemonic(entropy)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate random mnemonic: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Generated random mnemonic: %s", randomMnemonic)
|
||||||
|
|
||||||
|
// Test index for key derivation
|
||||||
|
testIndex := uint32(42)
|
||||||
|
|
||||||
|
// Generate the first identity
|
||||||
|
identity1, err := DeriveIdentity(randomMnemonic, testIndex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to derive first identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the second identity with the same mnemonic and index
|
||||||
|
identity2, err := DeriveIdentity(randomMnemonic, testIndex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to derive second identity: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that both private keys are identical
|
||||||
|
privateKey1 := identity1.String()
|
||||||
|
privateKey2 := identity2.String()
|
||||||
|
if privateKey1 != privateKey2 {
|
||||||
|
t.Fatalf("private keys should be identical:\nFirst: %s\nSecond: %s", privateKey1, privateKey2)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that both public keys (recipients) are identical
|
||||||
|
publicKey1 := identity1.Recipient().String()
|
||||||
|
publicKey2 := identity2.Recipient().String()
|
||||||
|
if publicKey1 != publicKey2 {
|
||||||
|
t.Fatalf("public keys should be identical:\nFirst: %s\nSecond: %s", publicKey1, publicKey2)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("✓ Deterministic generation verified")
|
||||||
|
t.Logf("Private key: %s", privateKey1)
|
||||||
|
t.Logf("Public key: %s", publicKey1)
|
||||||
|
|
||||||
|
// Generate 1 MB of random data for encryption test
|
||||||
|
testData := make([]byte, testDataSizeMegabyte)
|
||||||
|
_, err = rand.Read(testData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to generate random test data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("Generated %d bytes of random test data", len(testData))
|
||||||
|
|
||||||
|
// Encrypt the data using the public key (recipient)
|
||||||
|
var ciphertext bytes.Buffer
|
||||||
|
encryptor, err := age.Encrypt(&ciphertext, identity1.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create encryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = encryptor.Write(testData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write data to encryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = encryptor.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to close encryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("✓ Encrypted %d bytes into %d bytes of ciphertext", len(testData), ciphertext.Len())
|
||||||
|
|
||||||
|
// Decrypt the data using the private key
|
||||||
|
decryptor, err := age.Decrypt(bytes.NewReader(ciphertext.Bytes()), identity1)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create decryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedData, err := io.ReadAll(decryptor)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read decrypted data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("✓ Decrypted %d bytes", len(decryptedData))
|
||||||
|
|
||||||
|
// Verify that the decrypted data matches the original
|
||||||
|
if len(decryptedData) != len(testData) {
|
||||||
|
t.Fatalf("decrypted data length mismatch: expected %d, got %d", len(testData), len(decryptedData))
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(testData, decryptedData) {
|
||||||
|
t.Fatalf("decrypted data does not match original data")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("✓ Large data encryption/decryption test passed successfully")
|
||||||
|
|
||||||
|
// Additional verification: test with the second identity (should work identically)
|
||||||
|
var ciphertext2 bytes.Buffer
|
||||||
|
encryptor2, err := age.Encrypt(&ciphertext2, identity2.Recipient())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create second encryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = encryptor2.Write(testData)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to write data to second encryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = encryptor2.Close()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to close second encryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt with the second identity
|
||||||
|
decryptor2, err := age.Decrypt(bytes.NewReader(ciphertext2.Bytes()), identity2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to create second decryptor: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptedData2, err := io.ReadAll(decryptor2)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to read second decrypted data: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !bytes.Equal(testData, decryptedData2) {
|
||||||
|
t.Fatalf("second decrypted data does not match original data")
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("✓ Cross-verification with second identity successful")
|
||||||
|
}
|
@ -17,7 +17,7 @@ BIP85 enables a variety of use cases:
|
|||||||
```go
|
```go
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"git.eeqj.de/sneak/secret/internal/bip85"
|
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -140,7 +140,7 @@ The implementation is also compatible with the Python reference implementation's
|
|||||||
Run the tests with verbose output to see the test vectors and results:
|
Run the tests with verbose output to see the test vectors and results:
|
||||||
|
|
||||||
```
|
```
|
||||||
go test -v git.eeqj.de/sneak/secret/internal/bip85
|
go test -v git.eeqj.de/sneak/secret/pkg/bip85
|
||||||
```
|
```
|
||||||
|
|
||||||
## References
|
## References
|
785
test_secret_manager.sh
Executable file
785
test_secret_manager.sh
Executable file
@ -0,0 +1,785 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e # Exit on any error
|
||||||
|
|
||||||
|
# Colors for output
|
||||||
|
RED='\033[0;31m'
|
||||||
|
GREEN='\033[0;32m'
|
||||||
|
YELLOW='\033[1;33m'
|
||||||
|
BLUE='\033[0;34m'
|
||||||
|
NC='\033[0m' # No Color
|
||||||
|
|
||||||
|
# Test configuration
|
||||||
|
TEST_MNEMONIC="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||||
|
TEST_PASSPHRASE="test-passphrase-123"
|
||||||
|
TEMP_DIR="$(mktemp -d)"
|
||||||
|
SECRET_BINARY="./secret"
|
||||||
|
|
||||||
|
echo -e "${BLUE}=== Secret Manager Comprehensive Test Script ===${NC}"
|
||||||
|
echo -e "${YELLOW}Using temporary directory: $TEMP_DIR${NC}"
|
||||||
|
|
||||||
|
# Function to print test steps
|
||||||
|
print_step() {
|
||||||
|
echo -e "\n${BLUE}Step $1: $2${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print success
|
||||||
|
print_success() {
|
||||||
|
echo -e "${GREEN}✓ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print error and exit
|
||||||
|
print_error() {
|
||||||
|
echo -e "${RED}✗ $1${NC}"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to print warning (for expected failures)
|
||||||
|
print_warning() {
|
||||||
|
echo -e "${YELLOW}⚠ $1${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Function to clear state directory and reset environment
|
||||||
|
reset_state() {
|
||||||
|
echo -e "${YELLOW}Resetting state directory...${NC}"
|
||||||
|
|
||||||
|
# Safety checks before removing anything
|
||||||
|
if [ -z "$TEMP_DIR" ]; then
|
||||||
|
print_error "TEMP_DIR is not set, cannot reset state safely"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$TEMP_DIR" ]; then
|
||||||
|
print_error "TEMP_DIR ($TEMP_DIR) is not a directory, cannot reset state safely"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Additional safety: ensure TEMP_DIR looks like a temp directory
|
||||||
|
case "$TEMP_DIR" in
|
||||||
|
/tmp/* | /var/folders/* | */tmp/*)
|
||||||
|
# Looks like a reasonable temp directory path
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
print_error "TEMP_DIR ($TEMP_DIR) does not look like a safe temporary directory path"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
# Now it's safe to remove contents - use find to avoid glob expansion issues
|
||||||
|
find "${TEMP_DIR:?}" -mindepth 1 -delete 2>/dev/null || true
|
||||||
|
unset SB_SECRET_MNEMONIC
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Cleanup function
|
||||||
|
cleanup() {
|
||||||
|
echo -e "\n${YELLOW}Cleaning up...${NC}"
|
||||||
|
rm -rf "$TEMP_DIR"
|
||||||
|
unset SB_SECRET_STATE_DIR
|
||||||
|
unset SB_SECRET_MNEMONIC
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
echo -e "${GREEN}Cleanup complete${NC}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# Set cleanup trap
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
# Build the secret binary if it doesn't exist
|
||||||
|
if [ ! -f "$SECRET_BINARY" ]; then
|
||||||
|
print_step "0" "Building secret binary"
|
||||||
|
go build -o "$SECRET_BINARY" ./cmd/secret/
|
||||||
|
print_success "Built secret binary"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 1: Set up environment variables
|
||||||
|
print_step "1" "Setting up environment variables"
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
print_success "Environment variables set"
|
||||||
|
echo " SB_SECRET_STATE_DIR=$SB_SECRET_STATE_DIR"
|
||||||
|
echo " SB_SECRET_MNEMONIC=$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Test 2: Initialize the secret manager (should create default vault)
|
||||||
|
print_step "2" "Initializing secret manager (creates default vault)"
|
||||||
|
# Set passphrase for init command only
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
if $SECRET_BINARY init > /dev/null 2>&1; then
|
||||||
|
print_success "Secret manager initialized with default vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to initialize secret manager"
|
||||||
|
fi
|
||||||
|
# Unset passphrase after init
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
|
# Verify directory structure was created
|
||||||
|
if [ -d "$TEMP_DIR" ]; then
|
||||||
|
print_success "State directory created: $TEMP_DIR"
|
||||||
|
else
|
||||||
|
print_error "State directory was not created"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 3: Vault management
|
||||||
|
print_step "3" "Testing vault management"
|
||||||
|
|
||||||
|
# List vaults (should show default)
|
||||||
|
echo "Listing vaults..."
|
||||||
|
if $SECRET_BINARY vault list > /dev/null 2>&1; then
|
||||||
|
VAULTS=$($SECRET_BINARY vault list)
|
||||||
|
echo "Available vaults: $VAULTS"
|
||||||
|
print_success "Listed vaults successfully"
|
||||||
|
else
|
||||||
|
print_error "Failed to list vaults"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create a new vault
|
||||||
|
echo "Creating new vault 'work'..."
|
||||||
|
if $SECRET_BINARY vault create work > /dev/null 2>&1; then
|
||||||
|
print_success "Created vault 'work'"
|
||||||
|
else
|
||||||
|
print_error "Failed to create vault 'work'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create another vault
|
||||||
|
echo "Creating new vault 'personal'..."
|
||||||
|
if $SECRET_BINARY vault create personal > /dev/null 2>&1; then
|
||||||
|
print_success "Created vault 'personal'"
|
||||||
|
else
|
||||||
|
print_error "Failed to create vault 'personal'"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List vaults again (should show default, work, personal)
|
||||||
|
echo "Listing vaults after creation..."
|
||||||
|
if $SECRET_BINARY vault list > /dev/null 2>&1; then
|
||||||
|
VAULTS=$($SECRET_BINARY vault list)
|
||||||
|
echo "Available vaults: $VAULTS"
|
||||||
|
print_success "Listed vaults after creation"
|
||||||
|
else
|
||||||
|
print_error "Failed to list vaults after creation"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch to work vault
|
||||||
|
echo "Switching to 'work' vault..."
|
||||||
|
if $SECRET_BINARY vault select work > /dev/null 2>&1; then
|
||||||
|
print_success "Switched to 'work' vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to switch to 'work' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4: Import functionality with different environment variable combinations
|
||||||
|
print_step "4" "Testing import functionality with different environment variable combinations"
|
||||||
|
|
||||||
|
# Test 4a: Import with mnemonic env var set, no passphrase env var
|
||||||
|
echo -e "\n${YELLOW}Test 4a: Import with SB_SECRET_MNEMONIC set, SB_UNLOCK_PASSPHRASE unset${NC}"
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
if $SECRET_BINARY vault create test-vault > /dev/null 2>&1; then
|
||||||
|
print_success "Created test-vault for import testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import should prompt for passphrase
|
||||||
|
echo "Importing with mnemonic env var set, should prompt for passphrase..."
|
||||||
|
if echo "$TEST_PASSPHRASE" | $SECRET_BINARY import test-vault > /dev/null 2>&1; then
|
||||||
|
print_success "Import succeeded with mnemonic env var (prompted for passphrase)"
|
||||||
|
else
|
||||||
|
print_error "Import failed with mnemonic env var"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4b: Import with passphrase env var set, no mnemonic env var
|
||||||
|
echo -e "\n${YELLOW}Test 4b: Import with SB_UNLOCK_PASSPHRASE set, SB_SECRET_MNEMONIC unset${NC}"
|
||||||
|
reset_state
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
if $SECRET_BINARY vault create test-vault2 > /dev/null 2>&1; then
|
||||||
|
print_success "Created test-vault2 for import testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault2"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import should prompt for mnemonic
|
||||||
|
echo "Importing with passphrase env var set, should prompt for mnemonic..."
|
||||||
|
if echo "$TEST_MNEMONIC" | $SECRET_BINARY import test-vault2 > /dev/null 2>&1; then
|
||||||
|
print_success "Import succeeded with passphrase env var (prompted for mnemonic)"
|
||||||
|
else
|
||||||
|
print_error "Import failed with passphrase env var"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4c: Import with both env vars set
|
||||||
|
echo -e "\n${YELLOW}Test 4c: Import with both SB_SECRET_MNEMONIC and SB_UNLOCK_PASSPHRASE set${NC}"
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
if $SECRET_BINARY vault create test-vault3 > /dev/null 2>&1; then
|
||||||
|
print_success "Created test-vault3 for import testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault3"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import should not prompt for anything
|
||||||
|
echo "Importing with both env vars set, should not prompt..."
|
||||||
|
if $SECRET_BINARY import test-vault3 > /dev/null 2>&1; then
|
||||||
|
print_success "Import succeeded with both env vars (no prompts)"
|
||||||
|
else
|
||||||
|
print_error "Import failed with both env vars"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4d: Import with neither env var set
|
||||||
|
echo -e "\n${YELLOW}Test 4d: Import with neither SB_SECRET_MNEMONIC nor SB_UNLOCK_PASSPHRASE set${NC}"
|
||||||
|
reset_state
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
if $SECRET_BINARY vault create test-vault4 > /dev/null 2>&1; then
|
||||||
|
print_success "Created test-vault4 for import testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault4"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import should prompt for both mnemonic and passphrase
|
||||||
|
echo "Importing with neither env var set, should prompt for both..."
|
||||||
|
if expect -c "
|
||||||
|
spawn $SECRET_BINARY import test-vault4
|
||||||
|
expect \"Enter your BIP39 mnemonic phrase:\"
|
||||||
|
send \"$TEST_MNEMONIC\n\"
|
||||||
|
expect \"Enter passphrase for unlock key:\"
|
||||||
|
send \"$TEST_PASSPHRASE\n\"
|
||||||
|
expect \"Confirm passphrase:\"
|
||||||
|
send \"$TEST_PASSPHRASE\n\"
|
||||||
|
expect eof
|
||||||
|
" > /dev/null 2>&1; then
|
||||||
|
print_success "Import succeeded with no env vars (prompted for both)"
|
||||||
|
else
|
||||||
|
print_error "Import failed with no env vars"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4e: Import into non-existent vault (should fail)
|
||||||
|
echo -e "\n${YELLOW}Test 4e: Import into non-existent vault (should fail)${NC}"
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
|
||||||
|
echo "Importing into non-existent vault (should fail)..."
|
||||||
|
if $SECRET_BINARY import nonexistent-vault > /dev/null 2>&1; then
|
||||||
|
print_error "Import should have failed for non-existent vault"
|
||||||
|
else
|
||||||
|
print_success "Import correctly failed for non-existent vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 4f: Import with invalid mnemonic (should fail)
|
||||||
|
echo -e "\n${YELLOW}Test 4f: Import with invalid mnemonic (should fail)${NC}"
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="invalid mnemonic phrase that should not work"
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
|
||||||
|
# Create a vault first
|
||||||
|
if $SECRET_BINARY vault create test-vault5 > /dev/null 2>&1; then
|
||||||
|
print_success "Created test-vault5 for invalid mnemonic testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create test-vault5"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Importing with invalid mnemonic (should fail)..."
|
||||||
|
if $SECRET_BINARY import test-vault5 > /dev/null 2>&1; then
|
||||||
|
print_error "Import should have failed with invalid mnemonic"
|
||||||
|
else
|
||||||
|
print_success "Import correctly failed with invalid mnemonic"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset state for remaining tests
|
||||||
|
reset_state
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Test 5: Original import functionality test (using mnemonic-based)
|
||||||
|
print_step "5" "Testing original import functionality"
|
||||||
|
|
||||||
|
# Initialize to create default vault
|
||||||
|
if (echo "$TEST_PASSPHRASE"; echo "$TEST_PASSPHRASE") | $SECRET_BINARY init > /dev/null 2>&1; then
|
||||||
|
print_success "Initialized for Step 5 testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to initialize for Step 5 testing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Create work vault for import testing
|
||||||
|
if $SECRET_BINARY vault create work > /dev/null 2>&1; then
|
||||||
|
print_success "Created work vault for import testing"
|
||||||
|
else
|
||||||
|
print_error "Failed to create work vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch to work vault
|
||||||
|
echo "Switching to 'work' vault..."
|
||||||
|
if $SECRET_BINARY vault select work > /dev/null 2>&1; then
|
||||||
|
print_success "Switched to 'work' vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to switch to 'work' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Import into work vault using mnemonic (should derive long-term key)
|
||||||
|
echo "Importing mnemonic into 'work' vault..."
|
||||||
|
# Set passphrase for import command only
|
||||||
|
export SB_UNLOCK_PASSPHRASE="$TEST_PASSPHRASE"
|
||||||
|
if $SECRET_BINARY import work > /dev/null 2>&1; then
|
||||||
|
print_success "Imported mnemonic into 'work' vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to import mnemonic into 'work' vault"
|
||||||
|
fi
|
||||||
|
# Unset passphrase after import
|
||||||
|
unset SB_UNLOCK_PASSPHRASE
|
||||||
|
|
||||||
|
# Switch back to default vault
|
||||||
|
echo "Switching back to 'default' vault..."
|
||||||
|
if $SECRET_BINARY vault select default > /dev/null 2>&1; then
|
||||||
|
print_success "Switched back to 'default' vault"
|
||||||
|
else
|
||||||
|
print_error "Failed to switch back to 'default' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 6: Unlock key management
|
||||||
|
print_step "6" "Testing unlock key management"
|
||||||
|
|
||||||
|
# Create passphrase-protected unlock key
|
||||||
|
echo "Creating passphrase-protected unlock key..."
|
||||||
|
# Note: This test uses stdin input instead of environment variable to test the traditional approach
|
||||||
|
if echo "$TEST_PASSPHRASE" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then
|
||||||
|
print_success "Created passphrase-protected unlock key"
|
||||||
|
else
|
||||||
|
print_error "Failed to create passphrase-protected unlock key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List unlock keys
|
||||||
|
echo "Listing unlock keys..."
|
||||||
|
if $SECRET_BINARY keys list > /dev/null 2>&1; then
|
||||||
|
KEYS=$($SECRET_BINARY keys list)
|
||||||
|
echo "Available unlock keys: $KEYS"
|
||||||
|
print_success "Listed unlock keys"
|
||||||
|
else
|
||||||
|
print_error "Failed to list unlock keys"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 7: Secret management with mnemonic (keyless operation)
|
||||||
|
print_step "7" "Testing mnemonic-based secret operations (keyless)"
|
||||||
|
|
||||||
|
# Add secrets using mnemonic (no unlock key required)
|
||||||
|
echo "Adding secrets using mnemonic-based long-term key..."
|
||||||
|
|
||||||
|
# Test secret 1
|
||||||
|
if echo "my-super-secret-password" | $SECRET_BINARY add "database/password" > /dev/null 2>&1; then
|
||||||
|
print_success "Added secret: database/password"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: database/password"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test secret 2
|
||||||
|
if echo "api-key-12345" | $SECRET_BINARY add "api/key" > /dev/null 2>&1; then
|
||||||
|
print_success "Added secret: api/key"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: api/key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test secret 3 (with path)
|
||||||
|
if echo "ssh-private-key-content" | $SECRET_BINARY add "ssh/private-key" > /dev/null 2>&1; then
|
||||||
|
print_success "Added secret: ssh/private-key"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: ssh/private-key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test secret 4 (with dots and underscores)
|
||||||
|
if echo "jwt-secret-token" | $SECRET_BINARY add "app.config_jwt_secret" > /dev/null 2>&1; then
|
||||||
|
print_success "Added secret: app.config_jwt_secret"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret: app.config_jwt_secret"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve secrets using mnemonic
|
||||||
|
echo "Retrieving secrets using mnemonic-based long-term key..."
|
||||||
|
|
||||||
|
# Retrieve and verify secret 1
|
||||||
|
RETRIEVED_SECRET1=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_SECRET1" = "my-super-secret-password" ]; then
|
||||||
|
print_success "Retrieved and verified secret: database/password"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify secret: database/password"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve and verify secret 2
|
||||||
|
RETRIEVED_SECRET2=$($SECRET_BINARY get "api/key" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_SECRET2" = "api-key-12345" ]; then
|
||||||
|
print_success "Retrieved and verified secret: api/key"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify secret: api/key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve and verify secret 3
|
||||||
|
RETRIEVED_SECRET3=$($SECRET_BINARY get "ssh/private-key" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_SECRET3" = "ssh-private-key-content" ]; then
|
||||||
|
print_success "Retrieved and verified secret: ssh/private-key"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify secret: ssh/private-key"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List all secrets
|
||||||
|
echo "Listing all secrets..."
|
||||||
|
if $SECRET_BINARY list > /dev/null 2>&1; then
|
||||||
|
SECRETS=$($SECRET_BINARY list)
|
||||||
|
echo "Secrets in current vault:"
|
||||||
|
echo "$SECRETS" | while read -r secret; do
|
||||||
|
echo " - $secret"
|
||||||
|
done
|
||||||
|
print_success "Listed all secrets"
|
||||||
|
else
|
||||||
|
print_error "Failed to list secrets"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 8: Secret management without mnemonic (traditional unlock key approach)
|
||||||
|
print_step "8" "Testing traditional unlock key approach"
|
||||||
|
|
||||||
|
# Temporarily unset mnemonic to test traditional approach
|
||||||
|
unset SB_SECRET_MNEMONIC
|
||||||
|
|
||||||
|
# Add a secret using traditional unlock key approach
|
||||||
|
echo "Adding secret using traditional unlock key..."
|
||||||
|
if echo "traditional-secret-value" | $SECRET_BINARY add "traditional/secret" > /dev/null 2>&1; then
|
||||||
|
print_success "Added secret using traditional approach: traditional/secret"
|
||||||
|
else
|
||||||
|
print_error "Failed to add secret using traditional approach"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Retrieve secret using traditional unlock key approach
|
||||||
|
RETRIEVED_TRADITIONAL=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_TRADITIONAL" = "traditional-secret-value" ]; then
|
||||||
|
print_success "Retrieved and verified traditional secret: traditional/secret"
|
||||||
|
else
|
||||||
|
print_error "Failed to retrieve or verify traditional secret"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 9: Advanced unlock key management
|
||||||
|
print_step "9" "Testing advanced unlock key management"
|
||||||
|
|
||||||
|
# Re-enable mnemonic for key operations
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Create PGP unlock key (if GPG is available)
|
||||||
|
echo "Testing PGP unlock key creation..."
|
||||||
|
if command -v gpg >/dev/null 2>&1; then
|
||||||
|
# This would require a GPG key ID - for testing we'll just check the command exists
|
||||||
|
if $SECRET_BINARY keys add pgp --help > /dev/null 2>&1; then
|
||||||
|
print_success "PGP unlock key command available"
|
||||||
|
else
|
||||||
|
print_warning "PGP unlock key command not yet implemented"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "GPG not available for PGP unlock key testing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test Secure Enclave (macOS only)
|
||||||
|
if [[ "$OSTYPE" == "darwin"* ]]; then
|
||||||
|
echo "Testing Secure Enclave unlock key creation..."
|
||||||
|
if $SECRET_BINARY enroll sep > /dev/null 2>&1; then
|
||||||
|
print_success "Created Secure Enclave unlock key"
|
||||||
|
else
|
||||||
|
print_warning "Secure Enclave unlock key creation not yet implemented"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Secure Enclave only available on macOS"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Get current unlock key ID for testing
|
||||||
|
echo "Getting current unlock key for testing..."
|
||||||
|
if $SECRET_BINARY keys list > /dev/null 2>&1; then
|
||||||
|
CURRENT_KEY_ID=$($SECRET_BINARY keys list | head -n1 | awk '{print $1}')
|
||||||
|
if [ -n "$CURRENT_KEY_ID" ]; then
|
||||||
|
print_success "Found unlock key ID: $CURRENT_KEY_ID"
|
||||||
|
|
||||||
|
# Test key selection
|
||||||
|
echo "Testing unlock key selection..."
|
||||||
|
if $SECRET_BINARY key select "$CURRENT_KEY_ID" > /dev/null 2>&1; then
|
||||||
|
print_success "Selected unlock key: $CURRENT_KEY_ID"
|
||||||
|
else
|
||||||
|
print_warning "Unlock key selection not yet implemented"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 10: Secret name validation and edge cases
|
||||||
|
print_step "10" "Testing secret name validation and edge cases"
|
||||||
|
|
||||||
|
# Test valid names
|
||||||
|
VALID_NAMES=("valid-name" "valid.name" "valid_name" "valid/path/name" "123valid" "a" "very-long-name-with-many-parts/and/paths")
|
||||||
|
for name in "${VALID_NAMES[@]}"; do
|
||||||
|
if echo "test-value" | $SECRET_BINARY add "$name" --force > /dev/null 2>&1; then
|
||||||
|
print_success "Valid name accepted: $name"
|
||||||
|
else
|
||||||
|
print_error "Valid name rejected: $name"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test invalid names (these should fail)
|
||||||
|
echo "Testing invalid names (should fail)..."
|
||||||
|
INVALID_NAMES=("Invalid-Name" "invalid name" "invalid@name" "invalid#name" "invalid%name" "")
|
||||||
|
for name in "${INVALID_NAMES[@]}"; do
|
||||||
|
if echo "test-value" | $SECRET_BINARY add "$name" > /dev/null 2>&1; then
|
||||||
|
print_error "Invalid name accepted (should have been rejected): '$name'"
|
||||||
|
else
|
||||||
|
print_success "Invalid name correctly rejected: '$name'"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Test 11: Overwrite protection and force flag
|
||||||
|
print_step "11" "Testing overwrite protection and force flag"
|
||||||
|
|
||||||
|
# Try to add existing secret without --force (should fail)
|
||||||
|
if echo "new-value" | $SECRET_BINARY add "database/password" > /dev/null 2>&1; then
|
||||||
|
print_error "Overwrite protection failed - secret was overwritten without --force"
|
||||||
|
else
|
||||||
|
print_success "Overwrite protection working - secret not overwritten without --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to add existing secret with --force (should succeed)
|
||||||
|
if echo "new-password-value" | $SECRET_BINARY add "database/password" --force > /dev/null 2>&1; then
|
||||||
|
print_success "Force overwrite working - secret overwritten with --force"
|
||||||
|
|
||||||
|
# Verify the new value
|
||||||
|
RETRIEVED_NEW=$($SECRET_BINARY get "database/password" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_NEW" = "new-password-value" ]; then
|
||||||
|
print_success "Overwritten secret has correct new value"
|
||||||
|
else
|
||||||
|
print_error "Overwritten secret has incorrect value"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Force overwrite failed - secret not overwritten with --force"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 12: Cross-vault operations
|
||||||
|
print_step "12" "Testing cross-vault operations"
|
||||||
|
|
||||||
|
# Switch to work vault and add secrets there
|
||||||
|
echo "Switching to 'work' vault for cross-vault testing..."
|
||||||
|
if $SECRET_BINARY vault select work > /dev/null 2>&1; then
|
||||||
|
print_success "Switched to 'work' vault"
|
||||||
|
|
||||||
|
# Add work-specific secrets
|
||||||
|
if echo "work-database-password" | $SECRET_BINARY add "work/database" > /dev/null 2>&1; then
|
||||||
|
print_success "Added work-specific secret"
|
||||||
|
else
|
||||||
|
print_error "Failed to add work-specific secret"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# List secrets in work vault
|
||||||
|
if $SECRET_BINARY list > /dev/null 2>&1; then
|
||||||
|
WORK_SECRETS=$($SECRET_BINARY list)
|
||||||
|
echo "Secrets in work vault: $WORK_SECRETS"
|
||||||
|
print_success "Listed work vault secrets"
|
||||||
|
else
|
||||||
|
print_error "Failed to list work vault secrets"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Failed to switch to 'work' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Switch back to default vault
|
||||||
|
echo "Switching back to 'default' vault..."
|
||||||
|
if $SECRET_BINARY vault select default > /dev/null 2>&1; then
|
||||||
|
print_success "Switched back to 'default' vault"
|
||||||
|
|
||||||
|
# Verify default vault secrets are still there
|
||||||
|
if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
|
||||||
|
print_success "Default vault secrets still accessible"
|
||||||
|
else
|
||||||
|
print_error "Default vault secrets not accessible"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Failed to switch back to 'default' vault"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 13: File structure verification
|
||||||
|
print_step "13" "Verifying file structure"
|
||||||
|
|
||||||
|
echo "Checking file structure in $TEMP_DIR..."
|
||||||
|
if [ -d "$TEMP_DIR/vaults.d/default/secrets.d" ]; then
|
||||||
|
print_success "Default vault structure exists"
|
||||||
|
|
||||||
|
# Check a specific secret's file structure
|
||||||
|
SECRET_DIR="$TEMP_DIR/vaults.d/default/secrets.d/database%password"
|
||||||
|
if [ -d "$SECRET_DIR" ]; then
|
||||||
|
print_success "Secret directory exists: database%password"
|
||||||
|
|
||||||
|
# Check required files for per-secret key architecture
|
||||||
|
FILES=("value.age" "pub.age" "priv.age" "secret-metadata.json")
|
||||||
|
for file in "${FILES[@]}"; do
|
||||||
|
if [ -f "$SECRET_DIR/$file" ]; then
|
||||||
|
print_success "Required file exists: $file"
|
||||||
|
else
|
||||||
|
print_error "Required file missing: $file"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
else
|
||||||
|
print_error "Secret directory not found"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_error "Default vault structure not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check work vault structure
|
||||||
|
if [ -d "$TEMP_DIR/vaults.d/work" ]; then
|
||||||
|
print_success "Work vault structure exists"
|
||||||
|
else
|
||||||
|
print_error "Work vault structure not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check configuration files
|
||||||
|
if [ -f "$TEMP_DIR/configuration.json" ]; then
|
||||||
|
print_success "Global configuration file exists"
|
||||||
|
else
|
||||||
|
print_warning "Global configuration file not found (may not be implemented yet)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check current vault symlink
|
||||||
|
if [ -L "$TEMP_DIR/currentvault" ] || [ -f "$TEMP_DIR/currentvault" ]; then
|
||||||
|
print_success "Current vault link exists"
|
||||||
|
else
|
||||||
|
print_error "Current vault link not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 14: Environment variable error handling
|
||||||
|
print_step "14" "Testing environment variable error handling"
|
||||||
|
|
||||||
|
# Test with non-existent state directory
|
||||||
|
export SB_SECRET_STATE_DIR="/nonexistent/directory"
|
||||||
|
if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
|
||||||
|
print_error "Should have failed with non-existent state directory"
|
||||||
|
else
|
||||||
|
print_success "Correctly failed with non-existent state directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test init with non-existent directory (should work)
|
||||||
|
if $SECRET_BINARY init > /dev/null 2>&1; then
|
||||||
|
print_success "Init works with non-existent state directory"
|
||||||
|
else
|
||||||
|
print_error "Init should work with non-existent state directory"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Reset to working directory
|
||||||
|
export SB_SECRET_STATE_DIR="$TEMP_DIR"
|
||||||
|
|
||||||
|
# Test 15: Unlock key removal
|
||||||
|
print_step "15" "Testing unlock key removal"
|
||||||
|
|
||||||
|
# Re-enable mnemonic
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Create another unlock key for testing removal
|
||||||
|
echo "Creating additional unlock key for removal testing..."
|
||||||
|
# Use stdin input instead of environment variable
|
||||||
|
if echo "another-passphrase" | $SECRET_BINARY keys add passphrase > /dev/null 2>&1; then
|
||||||
|
print_success "Created additional unlock key"
|
||||||
|
|
||||||
|
# Get the key ID and try to remove it
|
||||||
|
if $SECRET_BINARY keys list > /dev/null 2>&1; then
|
||||||
|
KEY_TO_REMOVE=$($SECRET_BINARY keys list | tail -n1 | awk '{print $1}')
|
||||||
|
if [ -n "$KEY_TO_REMOVE" ]; then
|
||||||
|
echo "Attempting to remove unlock key: $KEY_TO_REMOVE"
|
||||||
|
if $SECRET_BINARY keys rm "$KEY_TO_REMOVE" > /dev/null 2>&1; then
|
||||||
|
print_success "Removed unlock key: $KEY_TO_REMOVE"
|
||||||
|
else
|
||||||
|
print_warning "Unlock key removal not yet implemented"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
print_warning "Could not create additional unlock key for removal testing"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test 16: Mixed approach compatibility
|
||||||
|
print_step "16" "Testing mixed approach compatibility"
|
||||||
|
|
||||||
|
# Verify mnemonic can access traditional secrets
|
||||||
|
RETRIEVED_MIXED=$($SECRET_BINARY get "traditional/secret" 2>/dev/null)
|
||||||
|
if [ "$RETRIEVED_MIXED" = "traditional-secret-value" ]; then
|
||||||
|
print_success "Mnemonic can access traditional secrets"
|
||||||
|
else
|
||||||
|
print_error "Mnemonic cannot access traditional secrets"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Test without mnemonic but with unlock key
|
||||||
|
unset SB_SECRET_MNEMONIC
|
||||||
|
if $SECRET_BINARY get "database/password" > /dev/null 2>&1; then
|
||||||
|
print_success "Traditional unlock key can access mnemonic-created secrets"
|
||||||
|
else
|
||||||
|
print_warning "Traditional unlock key cannot access mnemonic-created secrets (may need implementation)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Re-enable mnemonic for final tests
|
||||||
|
export SB_SECRET_MNEMONIC="$TEST_MNEMONIC"
|
||||||
|
|
||||||
|
# Final summary
|
||||||
|
echo -e "\n${GREEN}=== Test Summary ===${NC}"
|
||||||
|
echo -e "${GREEN}✓ Environment variable support (SB_SECRET_STATE_DIR, SB_SECRET_MNEMONIC)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Secret manager initialization${NC}"
|
||||||
|
echo -e "${GREEN}✓ Vault management (create, list, select)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Import functionality with all environment variable combinations${NC}"
|
||||||
|
echo -e "${GREEN}✓ Import error handling (non-existent vault, invalid mnemonic)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Unlock key management (passphrase, PGP, SEP)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Mnemonic-based secret operations (keyless)${NC}"
|
||||||
|
echo -e "${GREEN}✓ Traditional unlock key operations${NC}"
|
||||||
|
echo -e "${GREEN}✓ Secret name validation${NC}"
|
||||||
|
echo -e "${GREEN}✓ Overwrite protection and force flag${NC}"
|
||||||
|
echo -e "${GREEN}✓ Cross-vault operations${NC}"
|
||||||
|
echo -e "${GREEN}✓ Per-secret key file structure${NC}"
|
||||||
|
echo -e "${GREEN}✓ Unlock key removal${NC}"
|
||||||
|
echo -e "${GREEN}✓ Mixed approach compatibility${NC}"
|
||||||
|
echo -e "${GREEN}✓ Error handling${NC}"
|
||||||
|
|
||||||
|
echo -e "\n${GREEN}🎉 Comprehensive test completed!${NC}"
|
||||||
|
|
||||||
|
# Show usage examples for all implemented functionality
|
||||||
|
echo -e "\n${BLUE}=== Complete Usage Examples ===${NC}"
|
||||||
|
echo -e "${YELLOW}# Environment setup:${NC}"
|
||||||
|
echo "export SB_SECRET_STATE_DIR=\"/path/to/your/secrets\""
|
||||||
|
echo "export SB_SECRET_MNEMONIC=\"your twelve word mnemonic phrase here\""
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Initialization:${NC}"
|
||||||
|
echo "secret init"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Vault management:${NC}"
|
||||||
|
echo "secret vault list"
|
||||||
|
echo "secret vault create work"
|
||||||
|
echo "secret vault select work"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Import mnemonic (different combinations):${NC}"
|
||||||
|
echo "# With mnemonic env var set:"
|
||||||
|
echo "export SB_SECRET_MNEMONIC=\"abandon abandon...\""
|
||||||
|
echo "echo \"passphrase\" | secret import work"
|
||||||
|
echo ""
|
||||||
|
echo "# With passphrase env var set:"
|
||||||
|
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
||||||
|
echo "echo \"abandon abandon...\" | secret import work"
|
||||||
|
echo ""
|
||||||
|
echo "# With both env vars set:"
|
||||||
|
echo "export SB_SECRET_MNEMONIC=\"abandon abandon...\""
|
||||||
|
echo "export SB_UNLOCK_PASSPHRASE=\"passphrase\""
|
||||||
|
echo "secret import work"
|
||||||
|
echo ""
|
||||||
|
echo "# With neither env var set:"
|
||||||
|
echo "(echo \"abandon abandon...\"; echo \"passphrase\") | secret import work"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Unlock key management:${NC}"
|
||||||
|
echo "echo \"passphrase\" | secret keys add passphrase"
|
||||||
|
echo "secret keys add pgp <gpg-key-id>"
|
||||||
|
echo "secret enroll sep # macOS only"
|
||||||
|
echo "secret keys list"
|
||||||
|
echo "secret key select <key-id>"
|
||||||
|
echo "secret keys rm <key-id>"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Secret management:${NC}"
|
||||||
|
echo "echo \"my-secret\" | secret add \"app/password\""
|
||||||
|
echo "echo \"my-secret\" | secret add \"app/password\" --force"
|
||||||
|
echo "secret get \"app/password\""
|
||||||
|
echo "secret list"
|
||||||
|
echo ""
|
||||||
|
echo -e "${YELLOW}# Cross-vault operations:${NC}"
|
||||||
|
echo "secret vault select work"
|
||||||
|
echo "echo \"work-secret\" | secret add \"work/database\""
|
||||||
|
echo "secret vault select default"
|
Loading…
Reference in New Issue
Block a user