206 lines
5.8 KiB
Markdown
206 lines
5.8 KiB
Markdown
# 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
|
|
``` |