latest
This commit is contained in:
206
pkg/agehd/README.md
Normal file
206
pkg/agehd/README.md
Normal file
@@ -0,0 +1,206 @@
|
||||
# agehd - Deterministic Age Identities from BIP85
|
||||
|
||||
The `agehd` package derives deterministic X25519 age identities using BIP85 entropy derivation and a deterministic random number generator (DRNG). This package only supports proper BIP85 sources: BIP39 mnemonics and extended private keys (xprv).
|
||||
|
||||
## Features
|
||||
|
||||
- **Deterministic key generation**: Same input always produces the same age identity
|
||||
- **BIP85 compliance**: Uses the BIP85 standard for entropy derivation
|
||||
- **Multiple key support**: Generate multiple keys from the same source using different indices
|
||||
- **Two BIP85 input methods**: Support for BIP39 mnemonics and extended private keys (xprv)
|
||||
- **Vendor/application scoped**: Uses vendor-specific derivation paths to avoid conflicts
|
||||
|
||||
## Derivation Path
|
||||
|
||||
The package uses the following BIP85 derivation path:
|
||||
|
||||
```
|
||||
m/83696968'/592366788'/733482323'/n'
|
||||
```
|
||||
|
||||
Where:
|
||||
- `83696968'` is the BIP85 root path ("bip" in ASCII)
|
||||
- `592366788'` is the vendor ID (sha256("berlin.sneak") & 0x7fffffff)
|
||||
- `733482323'` is the application ID (sha256("secret") & 0x7fffffff)
|
||||
- `n'` is the sequential index (0, 1, 2, ...)
|
||||
|
||||
## Usage
|
||||
|
||||
### From BIP39 Mnemonic
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
// Derive the first identity (index 0)
|
||||
identity, err := agehd.DeriveIdentity(mnemonic, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Secret key: %s\n", identity.String())
|
||||
fmt.Printf("Public key: %s\n", identity.Recipient().String())
|
||||
}
|
||||
```
|
||||
|
||||
### From Extended Private Key (XPRV)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
xprv := "xprv9s21ZrQH143K2LBWUUQRFXhucrQqBpKdRRxNVq2zBqsx8HVqFk2uYo8kmbaLLHRdqtQpUm98uKfu3vca1LqdGhUtyoFnCNkfmXRyPXLjbKb"
|
||||
|
||||
// Derive the first identity (index 0) from the xprv
|
||||
identity, err := agehd.DeriveIdentityFromXPRV(xprv, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Secret key: %s\n", identity.String())
|
||||
fmt.Printf("Public key: %s\n", identity.Recipient().String())
|
||||
}
|
||||
```
|
||||
|
||||
### Multiple Identities
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
// Derive multiple identities with different indices
|
||||
for i := uint32(0); i < 3; i++ {
|
||||
identity, err := agehd.DeriveIdentity(mnemonic, i)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Identity %d: %s\n", i, identity.Recipient().String())
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### From Raw Entropy
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"git.eeqj.de/sneak/secret/pkg/agehd"
|
||||
)
|
||||
|
||||
func main() {
|
||||
mnemonic := "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
// First derive entropy using BIP85
|
||||
entropy, err := agehd.DeriveEntropy(mnemonic, 0)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
// Then create identity from entropy
|
||||
identity, err := agehd.IdentityFromEntropy(entropy)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
fmt.Printf("Secret key: %s\n", identity.String())
|
||||
fmt.Printf("Public key: %s\n", identity.Recipient().String())
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
### Functions
|
||||
|
||||
#### `DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error)`
|
||||
|
||||
Derives a deterministic age identity from a BIP39 mnemonic and index.
|
||||
|
||||
- `mnemonic`: A valid BIP39 mnemonic phrase
|
||||
- `n`: The derivation index (0, 1, 2, ...)
|
||||
- Returns: An age X25519 identity or an error
|
||||
|
||||
#### `DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error)`
|
||||
|
||||
Derives a deterministic age identity from an extended private key (xprv) and index.
|
||||
|
||||
- `xprv`: A valid extended private key in xprv format
|
||||
- `n`: The derivation index (0, 1, 2, ...)
|
||||
- Returns: An age X25519 identity or an error
|
||||
|
||||
#### `DeriveEntropy(mnemonic string, n uint32) ([]byte, error)`
|
||||
|
||||
Derives 32 bytes of entropy from a BIP39 mnemonic and index using BIP85.
|
||||
|
||||
- `mnemonic`: A valid BIP39 mnemonic phrase
|
||||
- `n`: The derivation index
|
||||
- Returns: 32 bytes of entropy or an error
|
||||
|
||||
#### `DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error)`
|
||||
|
||||
Derives 32 bytes of entropy from an extended private key (xprv) and index using BIP85.
|
||||
|
||||
- `xprv`: A valid extended private key in xprv format
|
||||
- `n`: The derivation index
|
||||
- Returns: 32 bytes of entropy or an error
|
||||
|
||||
#### `IdentityFromEntropy(ent []byte) (*age.X25519Identity, error)`
|
||||
|
||||
Converts 32 bytes of entropy into an age X25519 identity.
|
||||
|
||||
- `ent`: Exactly 32 bytes of entropy
|
||||
- Returns: An age X25519 identity or an error
|
||||
|
||||
## Implementation Details
|
||||
|
||||
1. **BIP85 Entropy Derivation**: The package uses the BIP85 standard to derive 64 bytes of entropy from the input source
|
||||
2. **DRNG**: A BIP85 DRNG (Deterministic Random Number Generator) using SHAKE256 is seeded with the 64-byte entropy
|
||||
3. **Key Generation**: 32 bytes are read from the DRNG to generate the age private key
|
||||
4. **RFC-7748 Clamping**: The private key is clamped according to RFC-7748 for X25519
|
||||
5. **Bech32 Encoding**: The key is encoded using Bech32 with the "age-secret-key-" prefix
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- The same mnemonic/xprv and index will always produce the same identity
|
||||
- Different indices produce cryptographically independent identities
|
||||
- The vendor/application scoping prevents conflicts with other BIP85 applications
|
||||
- The DRNG ensures high-quality randomness for key generation
|
||||
- Private keys are properly clamped for X25519 usage
|
||||
- Only accepts proper BIP85 sources (mnemonics and xprv keys), not arbitrary passphrases
|
||||
|
||||
## Testing
|
||||
|
||||
Run the tests with:
|
||||
|
||||
```bash
|
||||
go test -v ./internal/agehd
|
||||
```
|
||||
139
pkg/agehd/agehd.go
Normal file
139
pkg/agehd/agehd.go
Normal file
@@ -0,0 +1,139 @@
|
||||
// Package agehd derives deterministic X25519 age identities from a
|
||||
// BIP-39 seed using a vendor/application-scoped BIP-85 path:
|
||||
//
|
||||
// m / 83696968′ / <vendor id>′ / <application id>′ / n′
|
||||
//
|
||||
// • vendor id = 592 366 788 (sha256("berlin.sneak") & 0x7fffffff)
|
||||
// • app id = 733 482 323 (sha256("secret") & 0x7fffffff)
|
||||
// • n = sequential index (0,1,…)
|
||||
package agehd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"filippo.io/age"
|
||||
"git.eeqj.de/sneak/secret/pkg/bip85"
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
const (
|
||||
purpose = uint32(83696968) // fixed by BIP-85 ("bip")
|
||||
vendorID = uint32(592366788) // berlin.sneak
|
||||
appID = uint32(733482323) // secret
|
||||
hrp = "age-secret-key-" // Bech32 HRP used by age
|
||||
)
|
||||
|
||||
// clamp applies RFC-7748 clamping to a 32-byte scalar.
|
||||
func clamp(k []byte) {
|
||||
k[0] &= 248
|
||||
k[31] &= 127
|
||||
k[31] |= 64
|
||||
}
|
||||
|
||||
// IdentityFromEntropy converts 32 deterministic bytes into an
|
||||
// *age.X25519Identity by round-tripping through Bech32.
|
||||
func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
|
||||
if len(ent) != 32 {
|
||||
return nil, fmt.Errorf("need 32-byte scalar, got %d", len(ent))
|
||||
}
|
||||
|
||||
// Make a copy to avoid modifying the original
|
||||
key := make([]byte, 32)
|
||||
copy(key, ent)
|
||||
clamp(key)
|
||||
|
||||
data, err := bech32.ConvertBits(key, 8, 5, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bech32 convert: %w", err)
|
||||
}
|
||||
s, err := bech32.Encode(hrp, data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("bech32 encode: %w", err)
|
||||
}
|
||||
return age.ParseX25519Identity(strings.ToUpper(s))
|
||||
}
|
||||
|
||||
// DeriveEntropy derives 32 bytes of application-scoped entropy from the
|
||||
// supplied BIP-39 mnemonic and index n using BIP85.
|
||||
func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) {
|
||||
// Convert mnemonic to seed
|
||||
seed := bip39.NewSeed(mnemonic, "")
|
||||
|
||||
// Create master key from seed
|
||||
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create master key: %w", err)
|
||||
}
|
||||
|
||||
// Build the BIP85 derivation path: m/83696968'/vendor'/app'/n'
|
||||
path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n)
|
||||
|
||||
// Derive BIP85 entropy (64 bytes)
|
||||
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err)
|
||||
}
|
||||
|
||||
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
|
||||
drng := bip85.NewBIP85DRNG(entropy)
|
||||
key := make([]byte, 32)
|
||||
_, err = drng.Read(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveEntropyFromXPRV derives 32 bytes of application-scoped entropy from the
|
||||
// supplied extended private key (xprv) and index n using BIP85.
|
||||
func DeriveEntropyFromXPRV(xprv string, n uint32) ([]byte, error) {
|
||||
// Parse the extended private key
|
||||
masterKey, err := bip85.ParseMasterKey(xprv)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse master key: %w", err)
|
||||
}
|
||||
|
||||
// Build the BIP85 derivation path: m/83696968'/vendor'/app'/n'
|
||||
path := fmt.Sprintf("m/%d'/%d'/%d'/%d'", purpose, vendorID, appID, n)
|
||||
|
||||
// Derive BIP85 entropy (64 bytes)
|
||||
entropy, err := bip85.DeriveBIP85Entropy(masterKey, path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to derive BIP85 entropy: %w", err)
|
||||
}
|
||||
|
||||
// Use BIP85 DRNG to generate deterministic 32 bytes for the age key
|
||||
drng := bip85.NewBIP85DRNG(entropy)
|
||||
key := make([]byte, 32)
|
||||
_, err = drng.Read(key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read from DRNG: %w", err)
|
||||
}
|
||||
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// DeriveIdentity is the primary public helper that derives a deterministic
|
||||
// age identity from a BIP39 mnemonic and index.
|
||||
func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) {
|
||||
ent, err := DeriveEntropy(mnemonic, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return IdentityFromEntropy(ent)
|
||||
}
|
||||
|
||||
// DeriveIdentityFromXPRV derives a deterministic age identity from an
|
||||
// extended private key (xprv) and index.
|
||||
func DeriveIdentityFromXPRV(xprv string, n uint32) (*age.X25519Identity, error) {
|
||||
ent, err := DeriveEntropyFromXPRV(xprv, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return IdentityFromEntropy(ent)
|
||||
}
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user