mirror of
https://github.com/therootcompany/golib.git
synced 2026-02-20 10:48:04 +00:00
feat: add encoding/base2048
This commit is contained in:
parent
d2f362ae50
commit
a68cac6529
107
encoding/base2048/base2048.go
Normal file
107
encoding/base2048/base2048.go
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
package base2048
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math/big"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrUnknownWord = errors.New("unknown word in mnemonic")
|
||||||
|
ErrChecksumMismatch = errors.New("checksum does not match")
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncodeToString returns the base2048 encoding of src.
|
||||||
|
func EncodeToString(src []byte) string {
|
||||||
|
words := EncodeToWords(src)
|
||||||
|
|
||||||
|
return strings.Join(words, " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
// EncodeToWords returns the base2048 slice of src.
|
||||||
|
func EncodeToWords(src []byte) []string {
|
||||||
|
bits := len(src) * 8
|
||||||
|
checkBits := bits % 11
|
||||||
|
if checkBits != 0 {
|
||||||
|
checkBits = 11 - checkBits
|
||||||
|
}
|
||||||
|
|
||||||
|
hash := sha256.Sum256(src)
|
||||||
|
|
||||||
|
// Always prepare 16 bits worth of material (high byte first)
|
||||||
|
checkMaterial := (uint16(hash[0]) << 8) | uint16(hash[1])
|
||||||
|
|
||||||
|
// Shift right so the top checkBits bits become the low bits
|
||||||
|
shift := 16 - uint16(checkBits)
|
||||||
|
check := checkMaterial >> shift
|
||||||
|
|
||||||
|
// src<<checkBits | check
|
||||||
|
bi := new(big.Int).SetBytes(src)
|
||||||
|
bi.Lsh(bi, uint(checkBits))
|
||||||
|
bi.Or(bi, big.NewInt(int64(check)))
|
||||||
|
|
||||||
|
// Extract 11-bit words from LSB
|
||||||
|
numWords := (bits + checkBits) / 11
|
||||||
|
words := make([]string, numWords)
|
||||||
|
mask := big.NewInt(2047) // 2^11 - 1
|
||||||
|
|
||||||
|
for i := numWords - 1; i >= 0; i-- {
|
||||||
|
var wordIdx big.Int
|
||||||
|
wordIdx.And(bi, mask)
|
||||||
|
words[i] = wordList[wordIdx.Uint64()]
|
||||||
|
bi.Rsh(bi, 11)
|
||||||
|
}
|
||||||
|
|
||||||
|
return words
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeString returns the bytes represented by the base2048 string of words.
|
||||||
|
// If the input doesn't pass verification, it returns the decoded data and ErrChecksumMismatch.
|
||||||
|
// strings.Fields() is used to split on runs of whitespace.
|
||||||
|
func DecodeString(phrase string) ([]byte, error) {
|
||||||
|
words := strings.Fields(phrase)
|
||||||
|
return DecodeWords(words)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DecodeWords returns the bytes represented by the base2048 word slice.
|
||||||
|
// If the input doesn't pass verification, it returns the decoded data and ErrChecksumMismatch.
|
||||||
|
func DecodeWords(words []string) ([]byte, error) {
|
||||||
|
numWords := len(words)
|
||||||
|
|
||||||
|
// Build big.Int from bits (MSB first)
|
||||||
|
bi := big.NewInt(0)
|
||||||
|
for _, word := range words {
|
||||||
|
bits, ok := wordMap[word]
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: %q", ErrUnknownWord, word)
|
||||||
|
}
|
||||||
|
bi.Lsh(bi, 11)
|
||||||
|
bi.Or(bi, big.NewInt(int64(bits)))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate bit lengths
|
||||||
|
checkBits := numWords / 3
|
||||||
|
entBits := numWords*11 - checkBits
|
||||||
|
entLen := entBits / 8
|
||||||
|
|
||||||
|
// Extract entropy
|
||||||
|
entBi := new(big.Int).Rsh(bi, uint(checkBits))
|
||||||
|
entropy := make([]byte, entLen)
|
||||||
|
entBi.FillBytes(entropy)
|
||||||
|
|
||||||
|
h := sha256.Sum256(entropy)
|
||||||
|
expCheck := uint64(h[0]) >> (8 - uint(checkBits))
|
||||||
|
|
||||||
|
var mask big.Int
|
||||||
|
mask.Lsh(big.NewInt(1), uint(checkBits))
|
||||||
|
mask.Sub(&mask, big.NewInt(1))
|
||||||
|
|
||||||
|
gotCheck := new(big.Int).And(bi, &mask).Uint64()
|
||||||
|
|
||||||
|
if gotCheck != expCheck {
|
||||||
|
return entropy, ErrChecksumMismatch
|
||||||
|
}
|
||||||
|
return entropy, nil
|
||||||
|
}
|
||||||
150
encoding/base2048/base2048_test.go
Normal file
150
encoding/base2048/base2048_test.go
Normal file
@ -0,0 +1,150 @@
|
|||||||
|
package base2048
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Copied from the reference implementation
|
||||||
|
// https://github.com/trezor/python-mnemonic/blob/master/vectors.json
|
||||||
|
var fixtures = [][]string{
|
||||||
|
{
|
||||||
|
"00000000000000000000000000000000",
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||||
|
"legal winner thank year wave sausage worth useful legal winner thank yellow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"80808080808080808080808080808080",
|
||||||
|
"letter advice cage absurd amount doctor acoustic avoid letter advice cage above",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ffffffffffffffffffffffffffffffff",
|
||||||
|
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo wrong",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"000000000000000000000000000000000000000000000000",
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon agent",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||||
|
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal will",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"808080808080808080808080808080808080808080808080",
|
||||||
|
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter always",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo when",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"0000000000000000000000000000000000000000000000000000000000000000",
|
||||||
|
"abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon art",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
|
||||||
|
"legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth useful legal winner thank year wave sausage worth title",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"8080808080808080808080808080808080808080808080808080808080808080",
|
||||||
|
"letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic avoid letter advice cage absurd amount doctor acoustic bless",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
"zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo zoo vote",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"9e885d952ad362caeb4efe34a8e91bd2",
|
||||||
|
"ozone drill grab fiber curtain grace pudding thank cruise elder eight picnic",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"6610b25967cdcca9d59875f5cb50b0ea75433311869e930b",
|
||||||
|
"gravity machine north sort system female filter attitude volume fold club stay feature office ecology stable narrow fog",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"68a79eaca2324873eacc50cb9c6eca8cc68ea5d936f98787c60c7ebc74e6ce7c",
|
||||||
|
"hamster diagram private dutch cause delay private meat slide toddler razor book happy fancy gospel tennis maple dilemma loan word shrug inflict delay length",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c0ba5a8e914111210f2bd131f3d5e08d",
|
||||||
|
"scheme spot photo card baby mountain device kick cradle pact join borrow",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"6d9be1ee6ebd27a258115aad99b7317b9c8d28b6d76431c3",
|
||||||
|
"horn tenant knee talent sponsor spell gate clip pulse soap slush warm silver nephew swap uncle crack brave",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"9f6a2878b2520799a44ef18bc7df394e7061a224d2c33cd015b157d746869863",
|
||||||
|
"panda eyebrow bullet gorilla call smoke muffin taste mesh discover soft ostrich alcohol speed nation flash devote level hobby quick inner drive ghost inside",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"23db8160a31d3e0dca3688ed941adbf3",
|
||||||
|
"cat swing flag economy stadium alone churn speed unique patch report train",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"8197a4a47f0425faeaa69deebc05ca29c0a5b5cc76ceacc0",
|
||||||
|
"light rule cinnamon wrap drastic word pride squirrel upgrade then income fatal apart sustain crack supply proud access",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"066dca1a2bb7e8a1db2832148ce9933eea0f3ac9548d793112d9a95c9407efad",
|
||||||
|
"all hour make first leader extend hole alien behind guard gospel lava path output census museum junior mass reopen famous sing advance salt reform",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"f30f8c1da665478f49b001d94c5fc452",
|
||||||
|
"vessel ladder alter error federal sibling chat ability sun glass valve picture",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"c10ec20dc3cd9f652c7fac2f1230f7a3c828389a14392f05",
|
||||||
|
"scissors invite lock maple supreme raw rapid void congress muscle digital elegant little brisk hair mango congress clump",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"f585c11aec520db57dd353c69554b21a89b20fb0650966fa0a9d6f74fd989d8f",
|
||||||
|
"void come effort suffer camp survey warrior heavy shoot primary clutch crush open amazing screen patrol group space point ten exist slush involve unfold",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMnemonicVectors(t *testing.T) {
|
||||||
|
for i, vec := range fixtures {
|
||||||
|
entropyHex := vec[0]
|
||||||
|
expectedMnemonic := vec[1]
|
||||||
|
|
||||||
|
t.Run(fmt.Sprintf("vector-%d", i+1), func(t *testing.T) {
|
||||||
|
entropy, err := hex.DecodeString(entropyHex)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("invalid hex entropy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
gotMnemonic := EncodeToString(entropy)
|
||||||
|
if gotMnemonic != expectedMnemonic {
|
||||||
|
t.Errorf("mnemonic mismatch\nexpected: %q\ngot: %q", expectedMnemonic, gotMnemonic)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := DecodeString(gotMnemonic); err != nil {
|
||||||
|
t.Error("Verify() returned false for valid mnemonic")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInvalidChecksum(t *testing.T) {
|
||||||
|
invalid := "apple apple apple apple apple apple apple apple apple apple apple apple"
|
||||||
|
|
||||||
|
_, err := DecodeString(invalid)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "checksum") {
|
||||||
|
t.Errorf("expected checksum error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnknownWord(t *testing.T) {
|
||||||
|
invalid := "a a a a a a a a a a a a"
|
||||||
|
|
||||||
|
_, err := DecodeString(invalid)
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "unknown word") {
|
||||||
|
t.Errorf("expected unknown word error, got: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
2063
encoding/base2048/wordlist.go
Normal file
2063
encoding/base2048/wordlist.go
Normal file
File diff suppressed because it is too large
Load Diff
Loading…
x
Reference in New Issue
Block a user