@@ -25,36 +25,48 @@ public enum Mnemonic {
2525 throw AlgorandError . encodingError ( " Key data must be 32 bytes " )
2626 }
2727
28- var words : [ String ] = [ ]
2928 let wordlist = BIP39Wordlist . english
3029
31- // Convert key data to bits
32- var bits = " "
33- for byte in keyData {
34- bits += String ( byte, radix: 2 ) . leftPadding ( toLength: 8 , withPad: " 0 " )
35- }
30+ // Convert key data to 11-bit words using little-endian bit packing
31+ // This matches the Algorand SDK implementation
32+ let keyWords = toElevenBit ( Array ( keyData) )
33+
34+ // Compute checksum: first 11 bits of SHA512/256 hash (little-endian)
35+ let checksumHash = SHA512_256 . hash ( data: keyData)
36+ let checksumWords = toElevenBit ( Array ( checksumHash. prefix ( 2 ) ) )
37+ let checksumWord = checksumWords [ 0 ]
38+
39+ // Build the 25-word mnemonic: 24 key words + 1 checksum word
40+ var words = keyWords. map { wordlist [ $0] }
41+ words. append ( wordlist [ checksumWord] )
42+
43+ return words. joined ( separator: " " )
44+ }
3645
37- // Algorand uses first 8 bits of SHA512/256 as checksum (not SHA256!)
38- let checksum = SHA512_256 . hash ( data: keyData)
39- let checksumByte = Array ( checksum) [ 0 ]
40- bits += String ( checksumByte, radix: 2 ) . leftPadding ( toLength: 8 , withPad: " 0 " )
41-
42- // Now we have 264 bits (256 + 8), which gives us 24 words
43- // We need exactly 25 words, so the last word encodes the remaining bits (0-padded)
44- // 264 bits / 11 = 24 words, with 0 bits remaining
45- // To get 25 words: 25 * 11 = 275 bits needed
46- // Pad with zeros to get 275 bits
47- bits += String ( repeating: " 0 " , count: 275 - bits. count)
48-
49- // Convert to 25 words (11 bits each)
50- for i in stride ( from: 0 , to: 275 , by: 11 ) {
51- let chunk = bits [ bits. index ( bits. startIndex, offsetBy: i) ..< bits. index ( bits. startIndex, offsetBy: i + 11 ) ]
52- if let index = Int ( chunk, radix: 2 ) {
53- words. append ( wordlist [ index] )
46+ /// Converts bytes to 11-bit numbers using little-endian bit packing
47+ /// This matches the Algorand SDK's _to_11_bit function
48+ private static func toElevenBit( _ data: [ UInt8 ] ) -> [ Int ] {
49+ var buffer : UInt32 = 0
50+ var numBits = 0
51+ var output : [ Int ] = [ ]
52+
53+ for byte in data {
54+ buffer |= UInt32 ( byte) << numBits
55+ numBits += 8
56+
57+ if numBits >= 11 {
58+ output. append ( Int ( buffer & 0x7FF ) )
59+ buffer >>= 11
60+ numBits -= 11
5461 }
5562 }
5663
57- return words. joined ( separator: " " )
64+ // Handle remaining bits
65+ if numBits > 0 {
66+ output. append ( Int ( buffer & 0x7FF ) )
67+ }
68+
69+ return output
5870 }
5971
6072 /// Decodes a 25-word mnemonic into key data
@@ -68,42 +80,59 @@ public enum Mnemonic {
6880 }
6981
7082 let wordlist = BIP39Wordlist . english
71- var bits = " "
7283
84+ // Convert words to 11-bit indices
85+ var indices : [ Int ] = [ ]
7386 for word in words {
7487 guard let index = wordlist. firstIndex ( of: word. lowercased ( ) ) else {
7588 throw AlgorandError . invalidMnemonic ( " Invalid word in mnemonic: \( word) " )
7689 }
77- bits += String ( index , radix : 2 ) . leftPadding ( toLength : 11 , withPad : " 0 " )
90+ indices . append ( index )
7891 }
7992
80- // Extract key data (first 256 bits)
81- let keyBits = bits. prefix ( 256 )
82- var keyData = Data ( )
83- for i in stride ( from: 0 , to: 256 , by: 8 ) {
84- let byte = keyBits [ keyBits. index ( keyBits. startIndex, offsetBy: i) ..< keyBits. index ( keyBits. startIndex, offsetBy: i + 8 ) ]
85- if let byteValue = UInt8 ( byte, radix: 2 ) {
86- keyData. append ( byteValue)
87- }
88- }
93+ // First 24 words encode the key, last word is checksum
94+ let keyIndices = Array ( indices. prefix ( 24 ) )
95+ let checksumIndex = indices [ 24 ]
96+
97+ // Convert 11-bit indices back to bytes using little-endian unpacking
98+ let keyData = fromElevenBit ( keyIndices, byteCount: 32 )
8999
90- // Verify checksum (8 bits at position 256-263)
91- let checksumBits = String ( bits [ bits. index ( bits. startIndex, offsetBy: 256 ) ..< bits. index ( bits. startIndex, offsetBy: 264 ) ] )
92- let computedChecksum = SHA512_256 . hash ( data: keyData)
93- let computedChecksumByte = Array ( computedChecksum) [ 0 ]
94- let computedChecksumBits = String ( computedChecksumByte, radix: 2 ) . leftPadding ( toLength: 8 , withPad: " 0 " )
100+ // Verify checksum
101+ let checksumHash = SHA512_256 . hash ( data: keyData)
102+ let expectedChecksumWords = toElevenBit ( Array ( checksumHash. prefix ( 2 ) ) )
103+ let expectedChecksum = expectedChecksumWords [ 0 ]
95104
96- guard checksumBits == computedChecksumBits else {
105+ guard checksumIndex == expectedChecksum else {
97106 throw AlgorandError . invalidMnemonic ( " Invalid checksum " )
98107 }
99108
100- // Verify padding bits (264-274) are all zeros
101- let paddingBits = String ( bits [ bits. index ( bits. startIndex, offsetBy: 264 ) ..< bits. index ( bits. startIndex, offsetBy: 275 ) ] )
102- guard paddingBits == String ( repeating: " 0 " , count: 11 ) else {
103- throw AlgorandError . invalidMnemonic ( " Invalid padding " )
109+ return keyData
110+ }
111+
112+ /// Converts 11-bit numbers back to bytes using little-endian bit unpacking
113+ /// This is the inverse of toElevenBit
114+ private static func fromElevenBit( _ indices: [ Int ] , byteCount: Int ) -> Data {
115+ var buffer : UInt32 = 0
116+ var numBits = 0
117+ var output : [ UInt8 ] = [ ]
118+
119+ for index in indices {
120+ buffer |= UInt32 ( index) << numBits
121+ numBits += 11
122+
123+ while numBits >= 8 && output. count < byteCount {
124+ output. append ( UInt8 ( buffer & 0xFF ) )
125+ buffer >>= 8
126+ numBits -= 8
127+ }
104128 }
105129
106- return keyData
130+ // Pad with zeros if needed (shouldn't be necessary for valid input)
131+ while output. count < byteCount {
132+ output. append ( 0 )
133+ }
134+
135+ return Data ( output)
107136 }
108137
109138 /// Validates a mnemonic
0 commit comments