Derive the vault key from the mnemonic's words however they are spaced (closes #135) #160
@@ -18,6 +18,13 @@ https://git.eeqj.de/sneak/secret/milestone/12
|
||||
|
||||
# Completed Steps
|
||||
|
||||
- 2026-10-12: A mnemonic typed with extra spaces, tabs or line breaks gives the
|
||||
same long-term key as its words separated by single spaces
|
||||
(https://git.eeqj.de/sneak/secret/issues/135). The key was derived from the
|
||||
text exactly as typed, so a vault made with a double or trailing space did not
|
||||
open with the same words typed normally. `agehd.DeriveEntropy`, which every
|
||||
use of the mnemonic goes through, now joins the words with single spaces and
|
||||
puts them in Unicode NFKD, as BIP39 defines, before making the seed.
|
||||
- 2026-10-12: The keychain and Secure Enclave unlockers are gone from the code
|
||||
and the documents (https://git.eeqj.de/sneak/secret/issues/132). They were
|
||||
research: neither works without macOS code signing and entitlements, which
|
||||
|
||||
@@ -19,6 +19,7 @@ require (
|
||||
golang.org/x/crypto v0.38.0
|
||||
golang.org/x/sys v0.33.0
|
||||
golang.org/x/term v0.32.0
|
||||
golang.org/x/text v0.25.0
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -31,6 +32,5 @@ require (
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
+7
-2
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/btcsuite/btcd/btcutil/hdkeychain"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/btcsuite/btcutil/bech32"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
"sneak.berlin/go/secret/internal/bip39"
|
||||
"sneak.berlin/go/secret/pkg/bip85"
|
||||
)
|
||||
@@ -73,8 +74,12 @@ func IdentityFromEntropy(ent []byte) (*age.X25519Identity, error) {
|
||||
// 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, "")
|
||||
// BIP-39 makes the seed from the words joined by single spaces, in
|
||||
// Unicode NFKD. NewSeed hashes its input as given, so the mnemonic is
|
||||
// brought to that form first: the same words, however they were
|
||||
// typed, then give the same seed.
|
||||
sentence := norm.NFKD.String(strings.Join(strings.Fields(mnemonic), " "))
|
||||
seed := bip39.NewSeed(sentence, "")
|
||||
|
||||
// Create master key from seed
|
||||
masterKey, err := hdkeychain.NewMaster(seed, &chaincfg.MainNetParams)
|
||||
|
||||
@@ -175,6 +175,34 @@ func TestDeterministicDerivation(t *testing.T) {
|
||||
t.Logf("Index 1: %s", id3.String())
|
||||
}
|
||||
|
||||
// TestDeriveIdentityIgnoresExtraWhitespace checks that the same words give
|
||||
// the same identity however they are spaced, as when a mnemonic is typed or
|
||||
// pasted with a double space or a trailing newline.
|
||||
func TestDeriveIdentityIgnoresExtraWhitespace(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
want, err := DeriveIdentity(mnemonic, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("derive: %v", err)
|
||||
}
|
||||
|
||||
typedMnemonics := []string{
|
||||
strings.Replace(mnemonic, " ", " ", 1) + " ",
|
||||
"\t" + strings.ReplaceAll(mnemonic, " ", "\n") + "\n",
|
||||
}
|
||||
|
||||
for _, typed := range typedMnemonics {
|
||||
got, err := DeriveIdentity(typed, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("derive from %q: %v", typed, err)
|
||||
}
|
||||
|
||||
if got.String() != want.String() {
|
||||
t.Errorf("%q derives a different identity from %q", typed, mnemonic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeterministicXPRVDerivation(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user