Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: use wordlist-4096 for denser encoding #26

Merged
merged 4 commits into from
Jan 29, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
149 changes: 84 additions & 65 deletions README.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cmd/mnemonikey/generate.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ var GenerateCommand = &Command[GenerateOptions]{
"",
"Write the words of the recovery phrase to this `file` in PLAIN TEXT. Useful for debugging. "+
"Do not use this if you care about keeping your keys safe. Words will be separated by a "+
"single space and the file will contain the exact 15 words and nothing else.",
"single space. The file will contain the exact words and nothing else.",
)
},
Execute: func(opts *GenerateOptions, args []string) error {
Expand Down
1 change: 1 addition & 0 deletions cmd/mnemonikey/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ replace github.com/kklash/mnemonikey => ../..

require (
github.com/kklash/mnemonikey v0.0.0-00010101000000-000000000000
github.com/kklash/wordlist4096 v0.0.0-20230128235818-1dcc136efd79
golang.org/x/term v0.3.0
)

Expand Down
2 changes: 2 additions & 0 deletions cmd/mnemonikey/go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/kklash/wordlist4096 v0.0.0-20230128235818-1dcc136efd79 h1:HXBtBpHwOHwWLmsIAlNNoTikGimBdkWblSUCbyc3wf8=
github.com/kklash/wordlist4096 v0.0.0-20230128235818-1dcc136efd79/go.mod h1:HHJbj9GOEhOBCMSBTwJO0NaOzl4dgu3uuavrgtzrH+Q=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
Expand Down
2 changes: 1 addition & 1 deletion cmd/mnemonikey/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ var subcommands = map[string]Runner{

var RootCommand = &Command[RootOptions]{
Name: "mnemonikey - https://github.com/kklash/mnemonikey",
Description: "Deterministically generate and recover OpenPGP keys with BIP39 mnemonic backup phrases.",
Description: "Deterministically generate and recover OpenPGP keys with mnemonic backup phrases.",
UsageExamples: []string{
"mnemonikey <COMMAND> [help | --help | -h] [OPTIONS]\n",
"mnemonikey generate - Generate a new OpenPGP private key and display its recovery phrase.",
Expand Down
6 changes: 3 additions & 3 deletions cmd/mnemonikey/recover.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import (
"strings"

"github.com/kklash/mnemonikey"
"github.com/kklash/mnemonikey/mnemonic"
"github.com/kklash/wordlist4096"
)

const maxSubkeyIndex uint = 0xFFFF
Expand Down Expand Up @@ -56,7 +56,7 @@ var RecoverCommand = &Command[RecoverOptions]{
"word-file",
"",
"Read the words of the mnemonic from this `file`. Words should be separated by whitespace "+
"and the file should contain the exact 15 words. Useful for debugging.",
"and the file should contain the exact words. Useful for debugging.",
)

flags.BoolVar(
Expand Down Expand Up @@ -180,7 +180,7 @@ func readWordFile(fpath string) ([]string, error) {
words := make([]string, 0, mnemonikey.MnemonicSize)
for scanner.Scan() {
word := strings.ToLower(scanner.Text())
if _, ok := mnemonic.WordMap[word]; !ok {
if _, ok := wordlist4096.WordMap[word]; !ok {
return nil, fmt.Errorf("found word in %s not present in wordlist", fpath)
}
words = append(words, word)
Expand Down
14 changes: 7 additions & 7 deletions cmd/mnemonikey/word_input.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
"golang.org/x/term"

"github.com/kklash/mnemonikey"
"github.com/kklash/mnemonikey/mnemonic"
"github.com/kklash/wordlist4096"
)

const (
Expand All @@ -28,7 +28,7 @@ func eprintln(values ...any) { fmt.Fprintln(os.Stderr, values...) }
// userInputMnemonic accepts raw input from the user's terminal
// to get a mnemonic phrase of the given word count.
//
// Only returns words in the BIP39 word list.
// Only returns words in the wordlist.
func userInputMnemonic(wordCount uint) ([]string, error) {
oldState, err := term.MakeRaw(int(os.Stdin.Fd()))
if err != nil {
Expand Down Expand Up @@ -93,8 +93,8 @@ func userInputMnemonic(wordCount uint) ([]string, error) {
break
}

// See if the user's input might be a valid BIP39 word.
searchResult := mnemonic.Search(wordInput)
// See if the user's input might be a valid word in the wordlist.
searchResult := wordlist4096.Search(wordInput)

// Autocomplete without submitting
if charBuf[0] == '\t' && len(searchResult.Suffixes) > 0 {
Expand Down Expand Up @@ -159,7 +159,7 @@ func userInputMnemonic(wordCount uint) ([]string, error) {
// A simpler input process, used in cases where the raw terminal input
// manipulation in userInputMnemonic doesn't work for some people.
//
// Only returns words in the BIP39 word list.
// Only returns words in the wordlist.
func userInputMnemonicSimple(wordCount uint) ([]string, error) {
words := make([]string, wordCount)
scanner := bufio.NewScanner(os.Stdin)
Expand All @@ -178,8 +178,8 @@ func userInputMnemonicSimple(wordCount uint) ([]string, error) {
}
wordInput := strings.ToLower(strings.TrimSpace(scanner.Text()))

// See if the user's input might be a valid BIP39 word.
searchResult := mnemonic.Search(wordInput)
// See if the user's input might be a valid word in the wordlist.
searchResult := wordlist4096.Search(wordInput)

if searchResult.ExactMatch {
// We have a match! Remove any error message if needed
Expand Down
5 changes: 4 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ module github.com/kklash/mnemonikey

go 1.19

require golang.org/x/crypto v0.4.0
require (
github.com/kklash/wordlist4096 v0.0.0-20230128235818-1dcc136efd79
golang.org/x/crypto v0.4.0
)

require golang.org/x/sys v0.3.0 // indirect
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
github.com/kklash/wordlist4096 v0.0.0-20230128235818-1dcc136efd79 h1:HXBtBpHwOHwWLmsIAlNNoTikGimBdkWblSUCbyc3wf8=
github.com/kklash/wordlist4096 v0.0.0-20230128235818-1dcc136efd79/go.mod h1:HHJbj9GOEhOBCMSBTwJO0NaOzl4dgu3uuavrgtzrH+Q=
golang.org/x/crypto v0.4.0 h1:UVQgzMY87xqpKNgb+kDsll2Igd33HszWHFLmpaRMq/8=
golang.org/x/crypto v0.4.0/go.mod h1:3quD/ATkf6oY+rnes5c3ExXTbLc8mueNue5/DoinL80=
golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ=
Expand Down
21 changes: 11 additions & 10 deletions mnemonic/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,15 @@ import (
"math/big"
"math/bits"
"strings"

"github.com/kklash/wordlist4096"
)

// ErrInvalidWord is returned when a word is passed to DecodeMnemonic which is not
// a member of the BIP39 english word list.
var ErrInvalidWord = errors.New("word is not a member of the BIP39 word list")
// ErrInvalidWord is returned when a word is passed to DecodeWords which is not
// a member of the wordlist.
var ErrInvalidWord = errors.New("word is not a member of the wordlist")

// DecodeIndices decodes the given BIP39 indices into an integer representation.
// DecodeIndices decodes the given wordlist indices into an integer representation.
//
// Returns ErrInvalidIndex if any of the given indices are outside the
// domain of the word list size.
Expand All @@ -29,15 +31,14 @@ func DecodeIndices(indices []uint16) (*big.Int, error) {
return payloadInt, nil
}

// DecodeMnemonic decodes the given mnemonic phrase into their indices in the
// BIP39 english word list. The words can be in either upper or lower case.
// DecodeWords decodes the given set of words into their indices in the
// wordlist. The words can be in either upper or lower case.
//
// Returns ErrInvalidWord if any of the words are not members of the BIP39
// english word list.
func DecodeMnemonic(words []string) ([]uint16, error) {
// Returns ErrInvalidWord if any of the words are not members of the wordlist.
func DecodeWords(words []string) ([]uint16, error) {
indices := make([]uint16, len(words))
for i, word := range words {
index, ok := WordMap[strings.ToLower(word)]
index, ok := wordlist4096.WordMap[strings.ToLower(word)]
if !ok {
return nil, ErrInvalidWord
}
Expand Down
37 changes: 26 additions & 11 deletions mnemonic/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,43 +2,58 @@ package mnemonic

import (
"errors"
"fmt"
"math/big"
"math/bits"

"github.com/kklash/wordlist4096"
)

// BitsPerWord is the number of bits of information represented by each word
// in a BIP39 mnemonic phrase.
const BitsPerWord uint = 11
// in the wordlist.
const BitsPerWord = wordlist4096.BitsPerWord

var wordMask = big.NewInt(int64((1 << BitsPerWord) - 1))

// ErrInvalidIndex is returned when given a BIP39 word index which
// ErrInvalidIndex is returned when given a word index which
// cannot be represented in at most BitsPerWord bits.
var ErrInvalidIndex = errors.New("mnemonic index value is too large")
var ErrInvalidIndex = errors.New("word index value is too large")

// EncodeToIndices encodes the given payload as a slice of word indices.
//
// The bitSize parameter determines how many words will be used to encode the
// payload, which is calculated as:
//
// nWords = ceil(bitSize / BitsPerWord)
//
// bitSize does not necessarily have to be evenly divisible by BitsPerWord. If there is
// any unused space, it will be encoded at the leading edge of the resulting slice of indices.
func EncodeToIndices(payloadInt *big.Int, bitSize uint) ([]uint16, error) {
if payloadLen := payloadInt.BitLen(); uint(payloadLen) > bitSize {
return nil, fmt.Errorf("payload size (%d bits) is larger than given bitSize %d", payloadLen, bitSize)
}

// EncodeToIndices encodes the given payload as a slice of BIP39 word indices.
func EncodeToIndices(payloadInt *big.Int, bitSize uint) []uint16 {
nWords := (bitSize + BitsPerWord - 1) / BitsPerWord
indices := make([]uint16, nWords)
for i := nWords - 1; payloadInt.BitLen() > 0; i-- {
indices[i] = uint16(new(big.Int).And(payloadInt, wordMask).Uint64())
payloadInt.Rsh(payloadInt, BitsPerWord)
}
return indices
return indices, nil
}

// EncodeToMnemonic encodes the given BIP39 word indices as a BIP39 english
// mnemonic phrase. The returned mnemonic is encoded with lower-case characters.
// EncodeToWords encodes the given word indices as a series of words from
// the wordlist. The returned phrase is encoded with lower-case characters.
//
// Returns ErrInvalidIndex if any of the given indices are outside the
// domain of the word list.
func EncodeToMnemonic(indices []uint16) ([]string, error) {
func EncodeToWords(indices []uint16) ([]string, error) {
words := make([]string, len(indices))
for i, index := range indices {
if uint(bits.Len16(index)) > BitsPerWord {
return nil, ErrInvalidIndex
}
words[i] = WordList[index]
words[i] = wordlist4096.WordList[index]
}
return words, nil
}
87 changes: 0 additions & 87 deletions mnemonic/search.go

This file was deleted.

Loading