mnemonicode.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  1. // Mnemonicode encoder/decoder.
  2. // Encodes arbitrary bytes as human-readable words.
  3. // Each group of 4 bytes maps to 3 words; trailing groups use fewer words.
  4. package main
  5. import (
  6. "embed"
  7. "encoding/json"
  8. "flag"
  9. "fmt"
  10. "io"
  11. "os"
  12. "path/filepath"
  13. "strings"
  14. )
  15. //go:embed wordlist.json
  16. var wordlistJSON []byte
  17. const mnBase = 1626
  18. var (
  19. words []string
  20. wordIndex map[string]int
  21. )
  22. func init() {
  23. var data struct {
  24. Words []string `json:"words"`
  25. }
  26. if err := json.Unmarshal(wordlistJSON, &data); err != nil {
  27. panic("failed to parse embedded wordlist: " + err.Error())
  28. }
  29. words = data.Words
  30. wordIndex = make(map[string]int, len(words))
  31. for i, w := range words {
  32. wordIndex[w] = i
  33. }
  34. }
  35. func wordsRequired(size int) int {
  36. return ((size + 1) * 3) / 4
  37. }
  38. func encode(data []byte) []string {
  39. result := make([]string, 0, wordsRequired(len(data)))
  40. for n := 0; n < wordsRequired(len(data)); n++ {
  41. grp := (n / 3) * 4
  42. remaining := len(data) - grp
  43. if remaining > 4 {
  44. remaining = 4
  45. }
  46. chunk := make([]byte, 4)
  47. copy(chunk, data[grp:grp+remaining])
  48. x := uint32(chunk[0]) | uint32(chunk[1])<<8 | uint32(chunk[2])<<16 | uint32(chunk[3])<<24
  49. pos := n % 3
  50. extra := 0
  51. if pos == 2 {
  52. if remaining == 3 {
  53. extra = mnBase
  54. }
  55. x /= mnBase * mnBase
  56. } else if pos == 1 {
  57. x /= mnBase
  58. }
  59. result = append(result, words[x%mnBase+uint32(extra)])
  60. }
  61. return result
  62. }
  63. func decode(encoded []string) ([]byte, error) {
  64. groups := len(encoded)/3 + 1
  65. buf := make([]byte, groups*4)
  66. offset := 0
  67. getU32 := func(pos int) uint32 {
  68. grp := pos & ^3
  69. return uint32(buf[grp]) | uint32(buf[grp+1])<<8 | uint32(buf[grp+2])<<16 | uint32(buf[grp+3])<<24
  70. }
  71. setU32 := func(pos int, v uint32) {
  72. grp := pos & ^3
  73. buf[grp] = byte(v)
  74. buf[grp+1] = byte(v >> 8)
  75. buf[grp+2] = byte(v >> 16)
  76. buf[grp+3] = byte(v >> 24)
  77. }
  78. for _, word := range encoded {
  79. idx, ok := wordIndex[strings.ToLower(word)]
  80. if !ok {
  81. return nil, fmt.Errorf("unrecognized word: %q", word)
  82. }
  83. pos := offset % 4
  84. x := getU32(offset)
  85. switch pos {
  86. case 3:
  87. return nil, fmt.Errorf("unexpected word after 24-bit remainder")
  88. case 0:
  89. x = uint32(idx)
  90. offset++
  91. case 1:
  92. x += uint32(idx) * mnBase
  93. offset++
  94. case 2:
  95. if idx >= mnBase {
  96. x += uint32(idx-mnBase) * mnBase * mnBase
  97. offset++ // offset%4 == 3 signals end-of-group
  98. } else {
  99. if idx > 1624 || (idx == 1624 && x > 1_312_671) {
  100. return nil, fmt.Errorf("invalid encoding: value out of range")
  101. }
  102. x += uint32(idx) * mnBase * mnBase
  103. offset += 2
  104. }
  105. }
  106. setU32(offset-1, x)
  107. }
  108. return buf[:offset], nil
  109. }
  110. func main() {
  111. sep := flag.String("s", "-", "word separator")
  112. flag.StringVar(sep, "separator", "-", "word separator")
  113. flag.Parse()
  114. decoding := strings.Contains(strings.ToLower(filepath.Base(os.Args[0])), "decode")
  115. if decoding {
  116. raw, err := io.ReadAll(os.Stdin)
  117. if err != nil {
  118. fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
  119. os.Exit(1)
  120. }
  121. parts := strings.FieldsFunc(string(raw), func(r rune) bool {
  122. return strings.ContainsRune(*sep, r) || r == '\n' || r == '\r'
  123. })
  124. var filtered []string
  125. for _, p := range parts {
  126. p = strings.TrimSpace(p)
  127. if p != "" {
  128. filtered = append(filtered, p)
  129. }
  130. }
  131. out, err := decode(filtered)
  132. if err != nil {
  133. fmt.Fprintf(os.Stderr, "error: %v\n", err)
  134. os.Exit(1)
  135. }
  136. os.Stdout.Write(out)
  137. } else {
  138. data, err := io.ReadAll(os.Stdin)
  139. if err != nil {
  140. fmt.Fprintf(os.Stderr, "error reading stdin: %v\n", err)
  141. os.Exit(1)
  142. }
  143. fmt.Println(strings.Join(encode(data), *sep))
  144. }
  145. }