secret/pkg/agehd/README.md
2025-05-28 14:06:29 -07:00

5.8 KiB

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

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)

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

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

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:

go test -v ./internal/agehd