Skip to content

Commit

Permalink
First commit
Browse files Browse the repository at this point in the history
  • Loading branch information
ottosch committed Jul 22, 2024
0 parents commit eca0887
Show file tree
Hide file tree
Showing 15 changed files with 1,279 additions and 0 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
lastseed
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# lastseed

Display the possible last seed words from 11, 14, 17, 20 or 23 words.
Entropy is also displayed and can be pasted into [Ian Coleman bip39](https://github.com/iancoleman/bip39) or similar tools.

## Usage

```bash
./lastseed traffic rabbit canal shadow eternal public evil cup poem drift episode box manual sick entry original deny
```

or

```bash
./lastseed
Enter seed words:
traffic rabbit canal shadow eternal public evil cup poem drift episode box manual sick entry original deny
```

![Usage example](example.png)

## Build

```bash
go build -o lastseed ./src
```
Binary file added example.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
module github.com/ottosch/lastseed

go 1.22.5
55 changes: 55 additions & 0 deletions src/bip39/verify.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package bip39

import (
"crypto/sha256"
"fmt"
"math/big"
"os"

"github.com/ottosch/lastseed/src/bip39/wordlist"
)

// VerifyWordCount checks if the number of words matches a suitable number of bip39 seed words
func VerifyWordCount(words []string) {
actualCount := len(words) + 1
if actualCount < 12 || actualCount > 24 || actualCount%3 != 0 {
fmt.Fprintf(os.Stderr, "%d word(s) input. Expected 11, 14, 17, 20 or 23\n", len(words))
os.Exit(1)
}
}

// VerifyValidWords checks if all words belong to bip39 word list
func VerifyValidWords(words []string) {
for _, w := range words {
_, found := wordlist.WordMap[w]
if !found {
fmt.Fprintf(os.Stderr, "Word \"%s\" is not part of the BIP39 word list\n", w)
os.Exit(1)
}
}
}

// ValidChecksum performs checksum validation
func ValidChecksum(fullBitstring *big.Int, checksumSize uint) bool {
wordCount := int(checksumSize) * 3

withoutChecksumBits := new(big.Int).Set(fullBitstring)
withoutChecksumBits.Rsh(withoutChecksumBits, checksumSize)

withoutChecksumBitCount := (wordCount - 1) * 11 // we still don't have the last word here
withoutChecksumByteLen := (withoutChecksumBitCount + 7) / 8
withoutChecksumBytes := make([]byte, withoutChecksumByteLen)
withoutChecksumBits.FillBytes(withoutChecksumBytes)

hashBits := sha256.Sum256(withoutChecksumBytes)[0] >> (8 - checksumSize) // ex 4 bits: 01010111000110001101 -> 00001000
bitmask := byte((1 << checksumSize) - 1) // 00001111
checksumBits := hashBits & bitmask // 00001000

withChecksumBitCount := wordCount*11 + int(checksumSize) // now all words + checksum
withChecksumByteLen := (withChecksumBitCount + 7) / 8
withChecksumBytes := make([]byte, withChecksumByteLen)
fullBitstring.FillBytes(withChecksumBytes)

inputChecksumBits := withChecksumBytes[len(withChecksumBytes)-1] & bitmask
return checksumBits == inputChecksumBits
}
620 changes: 620 additions & 0 deletions src/bip39/wordlist/wordlist.go

Large diffs are not rendered by default.

42 changes: 42 additions & 0 deletions src/lastseed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"bufio"
"fmt"
"os"
"regexp"
"strings"

"github.com/ottosch/lastseed/src/seed"
"github.com/ottosch/lastseed/src/table"
)

var (
spacesRegex = regexp.MustCompile(`\s{2,}`)
reader = bufio.NewReader(os.Stdin)
)

func main() {
words := readWordsFromCliArgs()
for words == "" {
fmt.Println("Enter seed words: ")
words, _ = reader.ReadString('\n')
}

words = spacesRegex.ReplaceAllString(strings.TrimSpace(strings.ToLower(words)), " ")
s := seed.NewSeed(words)

fmt.Println()

table.DrawSummary(s)
table.DrawResults(s)
}

func readWordsFromCliArgs() string {
args := os.Args
if len(args) == 1 {
return ""
}

return strings.Join(args[1:], " ")
}
38 changes: 38 additions & 0 deletions src/seed/result.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package seed

import (
"encoding/hex"
"math/big"
)

type Result struct {
bitstring *big.Int
lastWord string
}

func NewResult(bitstring *big.Int, lastWord string) *Result {
return &Result{bitstring, lastWord}
}

func (r *Result) Bitstring() *big.Int {
return r.bitstring
}

func (r *Result) LastWord() string {
return r.lastWord
}

// Entropy returns entropy in hex
func (r *Result) Entropy(checksumSize uint) string {
wordCount := int(checksumSize) * 3

withoutChecksumBits := new(big.Int).Set(r.bitstring)
withoutChecksumBits.Rsh(withoutChecksumBits, checksumSize)

bitCount := wordCount*11 - int(checksumSize) // entropy doesn't have checksum
byteLen := (bitCount + 7) / 8
fbs := make([]byte, byteLen)
withoutChecksumBits.FillBytes(fbs)

return hex.EncodeToString(fbs)
}
83 changes: 83 additions & 0 deletions src/seed/seed.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package seed

import (
"math"
"math/big"
"strings"

"github.com/ottosch/lastseed/src/bip39"
"github.com/ottosch/lastseed/src/bip39/wordlist"
)

type Seed struct {
bitstring *big.Int // the bits of the seed words
words []string
results []*Result
}

func NewSeed(wordsStr string) *Seed {
words := strings.Split(wordsStr, " ")

bip39.VerifyWordCount(words)
bip39.VerifyValidWords(words)

s := &Seed{words: words}
s.fillBitstring(words)

s.calcLastWords()
return s
}

func (s *Seed) GetWords() []string {
return s.words
}

func (s *Seed) GetWordCount() int {
return len(s.words) + 1
}

func (s *Seed) GetChecksumSize() uint {
return uint(s.GetWordCount() / 3)
}

func (s *Seed) GetBitstring() *big.Int {
return new(big.Int).Set(s.bitstring)
}

func (s *Seed) GetResults() []*Result {
return s.results
}

func (s *Seed) calcLastWords() {
totalWords := int(math.Pow(2, float64(11-s.GetChecksumSize())))
results := make([]*Result, 0, totalWords)

for i, bipWord := range wordlist.Wordlist {
fullBitstring := addWordToBitstring(s.GetBitstring(), i)
if bip39.ValidChecksum(fullBitstring, s.GetChecksumSize()) {
results = append(results, NewResult(fullBitstring, bipWord))
}
}

s.results = results
}

// fillBitstring sets the seed bit string according to the words indices
func (s *Seed) fillBitstring(words []string) {
num := new(big.Int)
for _, w := range words {
index := int64(wordlist.WordMap[w])
indexBig := new(big.Int).SetInt64(index)
num.Or(num.Lsh(num, 11), indexBig) // num << 11 | index
}

s.bitstring = num
}

// addWordToBitstring adds the word index to the current bit string
func addWordToBitstring(bitstring *big.Int, wordIndex int) *big.Int {
fullBitstring := new(big.Int).Set(bitstring)
lastWordIndex := new(big.Int).SetInt64(int64(wordIndex))
fullBitstring.Or(fullBitstring.Lsh(fullBitstring, 11), lastWordIndex)
return fullBitstring
}
42 changes: 42 additions & 0 deletions src/table/constants.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package table

const (
topLeft = '┌'
topSeparator = '┬'
topRight = '┐'
bottomLeft = '└'
bottomSeparator = '┴'
bottomRight = '┘'
middleLeft = '├'
middleSeparator = '┼'
middleRight = '┤'
vertical = '│'
horizontal = '─'

wordStr = "Word"
entropyStr = "Entropy"

ALIGN_LEFT = 1 << iota // 1 << 0 == 1
ALIGN_CENTER // 1 << 1 == 2
ALIGN_RIGHT // 1 << 2 == 4

BORDER_TOP
BORDER_BOTTOM
BORDER_MIDDLE
TEXT_MIDDLE
)

var (
top lineType = 1
bottom lineType = 2
middle lineType = 3
textMiddle lineType = 4

alignLeft alignType = 1
alignCenter alignType = 2
alignRight alignType = 3
)

type lineType int

type alignType int
74 changes: 74 additions & 0 deletions src/table/grid.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package table

import (
"log"
"strings"
)

// TableCell a single table cell and its text
type TableCell struct {
text string
}

// NewCell creates a table cell, content aligned
func NewCell(text string, length int, align *TextAlign) *TableCell {
cellText := align.AlignText(text, length)
return &TableCell{text: cellText}
}

// TableRow a table row, composed of various, styled cells
type TableRow struct {
cells []*TableCell
style *lineStyle
}

// TextRow creates a row of text cells
func TextRow(texts []string, sizes []int, settings int) *TableRow {
if len(texts) != len(sizes) || len(texts)%2 != 0 {
log.Fatalf("invalid number of columns: %d, %d\n", len(texts), len(sizes))
}

cellSettings := parseSettings(settings)
cells := make([]*TableCell, len(texts))

for i, text := range texts {
cells[i] = NewCell(text, sizes[i], cellSettings.Align)
}

return &TableRow{cells: cells, style: cellSettings.Style}
}

// GridRow creates a row of grid (border) content only
func GridRow(sizes []int, settings int) *TableRow {
if len(sizes)%2 != 0 {
log.Fatalf("invalid number of grid cells: %v\n", sizes)
}

cellSettings := parseSettings(settings)
cells := make([]*TableCell, len(sizes))

for i, size := range sizes {
cells[i] = &TableCell{text: strings.Repeat(string(horizontal), size)}
}

return &TableRow{cells: cells, style: cellSettings.Style}
}

func (g *TableRow) String() string {
var result strings.Builder

strList := make([]string, 0, len(g.cells)/2)
for i := 0; i < len(g.cells); i += 2 {
var sb strings.Builder
sb.WriteRune(g.style.leftBorder)
sb.WriteString(g.cells[i].text)
sb.WriteRune(g.style.separator)
sb.WriteString(g.cells[i+1].text)
sb.WriteRune(g.style.rightBorder)
strList = append(strList, sb.String())
}

result.WriteString(strings.Join(strList, " "))
result.WriteRune('\n')
return result.String()
}
Loading

0 comments on commit eca0887

Please # to comment.