initial
This commit is contained in:
86
internal/agehd/agehd.go
Normal file
86
internal/agehd/agehd.go
Normal file
@@ -0,0 +1,86 @@
|
||||
// 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"
|
||||
bip85 "github.com/bitmask-dev/go-bip85"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
"github.com/tyler-smith/go-bip32"
|
||||
"github.com/tyler-smith/go-bip39"
|
||||
)
|
||||
|
||||
const (
|
||||
purpose = uint32(83696968) // fixed by BIP-85 (“bip”)
|
||||
vendorID = uint32(592366788) // berlin.sneak
|
||||
appID = uint32(733482323) // secret
|
||||
firstH = bip32.FirstHardenedChild
|
||||
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))
|
||||
}
|
||||
clamp(ent)
|
||||
|
||||
data, err := bech32.ConvertBits(ent, 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.
|
||||
func DeriveEntropy(mnemonic string, n uint32) ([]byte, error) {
|
||||
seed := bip39.NewSeed(mnemonic, "")
|
||||
|
||||
root, err := bip32.NewMasterKey(seed)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// m / purpose′ / vendor′
|
||||
purp, err := root.NewChildKey(purpose + firstH)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
vend, err := purp.NewChildKey(vendorID + firstH)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return bip85.NewFromBip32(vend).DeriveRawEntropy(appID, 32, n)
|
||||
}
|
||||
|
||||
// DeriveIdentity is the primary public helper.
|
||||
func DeriveIdentity(mnemonic string, n uint32) (*age.X25519Identity, error) {
|
||||
ent, err := DeriveEntropy(mnemonic, n)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return IdentityFromEntropy(ent)
|
||||
}
|
||||
47
internal/agehd/agehd_test.go
Normal file
47
internal/agehd/agehd_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package agehd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"filippo.io/age"
|
||||
)
|
||||
|
||||
const mnemonic = "abandon abandon abandon abandon abandon " +
|
||||
"abandon abandon abandon abandon abandon abandon about"
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user