Added MnemonicCode, implements BIP-0039.

This commit is contained in:
Ken Sedgwick 2013-11-14 19:51:13 -08:00 committed by Mike Hearn
parent c11456c9f4
commit 8dcead3508
3 changed files with 2455 additions and 0 deletions

View File

@ -0,0 +1,230 @@
/*
* Copyright 2013 Ken Sedgwick
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.spongycastle.crypto.engines.RijndaelEngine;
import org.spongycastle.crypto.params.KeyParameter;
import org.spongycastle.util.encoders.Hex;
import com.google.bitcoin.core.Sha256Hash;
/**
* A MnemonicCode object may be used to convert between binary seed values and
* lists of words per <a href="https://en.bitcoin.it/wiki/BIP_0039">the BIP 39
* specification</a>
*
* NOTE - as of 15 Oct 2013 the spec at
* https://en.bitcoin.it/wiki/BIP_0039 is out-of-date. The correct
* spec can be found at https://github.com/trezor/python-mnemonic
*/
public class MnemonicCode {
private ArrayList<String> wordList;
public static String BIP0039_ENGLISH_SHA256 =
"ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db";
/**
* Creates an MnemonicCode object, initializing with words read
* from the supplied input stream. If a wordListDigest is
* supplied the digest of the words will be checked.
*/
public MnemonicCode(InputStream wordstream, String wordListDigest)
throws IOException, IllegalArgumentException {
BufferedReader br = new BufferedReader(new InputStreamReader(wordstream, "UTF-8"));
String word;
this.wordList = new ArrayList<String>();
MessageDigest md;
try {
md = MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException ex) {
throw new RuntimeException(ex); // Can't happen.
}
while ((word = br.readLine()) != null) {
md.update(word.getBytes());
this.wordList.add(word);
}
br.close();
if (this.wordList.size() != 2048)
throw new IllegalArgumentException("input stream did not contain 2048 words");
// If a wordListDigest is supplied check to make sure it matches.
if (wordListDigest != null) {
byte[] digest = md.digest();
String hexdigest = new String(Hex.encode(digest));
if (!hexdigest.equals(wordListDigest))
throw new IllegalArgumentException("wordlist digest mismatch");
}
}
/**
* Encodes a 128, 192 or 256 bit seed into a list of words.
*/
public List<String> encode(byte[] seed) throws IllegalArgumentException {
// 2. Make sure its length (L) is 128, 192 or 256 bits.
int len = seed.length * 8;
if (len != 128 && len != 192 && len != 256)
throw new IllegalArgumentException("seed not 128, 192 or 256 bits");
// 3. Encrypt input data 10000x with Rijndael (ECB mode).
// Set key to SHA256 hash of string ("mnemonic" + user_password).
// Set block size to input size (that's why Rijndael is used, not AES).
byte[] indata = stretch(len, seed);
// Convert binary data to array of boolean for processing.
boolean[] inarray = new boolean[indata.length * 8];
for (int ii = 0; ii < indata.length; ++ii)
for (int kk = 0; kk < 8; ++kk)
inarray[(ii * 8) + kk] = (indata[ii] & (1 << (7 - kk))) != 0;
// 4-6 Compute checksum.
boolean[] chksum = checksum(inarray);
// 7. Concatenate I and C into encoded data (E). Length of E is divisable by 33 bits.
boolean[] ee = new boolean[inarray.length + chksum.length];
for (int ii = 0; ii < inarray.length; ++ii)
ee[ii] = inarray[ii];
for (int ii = 0; ii < chksum.length; ++ii)
ee[inarray.length + ii] = chksum[ii];
// 8. Keep taking 11 bits from E until there are none left.
// 9. Treat them as integer W, add word with index W to the output.
ArrayList<String> words = new ArrayList<String>();
int nwords = ee.length / 11;
for (int ii = 0; ii < nwords; ++ii) {
int ndx = 0;
for (int kk = 0; kk < 11; ++kk) {
ndx <<= 1;
if (ee[(ii * 11) + kk])
ndx |= 0x1;
}
words.add(this.wordList.get(ndx));
}
return words;
}
/**
* Decodes a list of words into a seed value.
*/
public byte[] decode(List<String> words) throws IllegalArgumentException {
int nwords = words.size();
// 2. Make sure the number of words is 12, 18 or 24.
if (nwords != 12 && nwords != 18 && nwords != 24)
throw new IllegalArgumentException("Mnemonic code not 12, 18 or 24 words");
// 3. Figure out word indexes in a dictionary and output them as binary stream E.
int len = nwords * 11;
boolean[] ee = new boolean[len];
int wordindex = 0;
for (String word : words) {
// Find the words index in the wordlist.
int ndx = Collections.binarySearch(this.wordList, word);
if (ndx < 0)
throw new IllegalArgumentException("\"" + word + "\" invalid");
// Set the next 11 bits to the value of the index.
for (int ii = 0; ii < 11; ++ii)
ee[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0;
++wordindex;
}
// 5. Split E into two parts: B and C, where B are first L/33*32 bits, C are last L/33 bits.
int bblen = (len / 33) * 32;
int cclen = len - bblen;
boolean[] bb = new boolean[bblen];
for (int ii = 0; ii < bblen; ++ii)
bb[ii] = ee[ii];
boolean[] cc = new boolean[cclen];
for (int ii = 0; ii < cclen; ++ii)
cc[ii] = ee[bblen + ii];
// 6. Make sure C is the checksum of B (using the step 5 from the above paragraph).
boolean[] chksum = checksum(bb);
if (!Arrays.equals(chksum, cc))
throw new IllegalArgumentException("checksum error");
// 8. Treat B as binary data.
byte[] outdata = new byte[bblen / 8];
for (int ii = 0; ii < outdata.length; ++ii)
for (int jj = 0; jj < 8; ++jj)
if (bb[(ii * 8) + jj])
outdata[ii] |= 1 << (7 - jj);
// 9. Decrypt this data 10000x with Rijndael (ECB mode),
// use the same parameters as used in step 3 of encryption.
byte[] seed = unstretch(bblen, outdata);
return seed;
}
private byte[] stretch(int len, byte[] data) {
// 3. Encrypt input data 10000x with Rijndael (ECB mode).
// Set key to SHA256 hash of string ("mnemonic" + user_password).
// Set block size to input size (that's why Rijndael is used, not AES).
byte[] mnemonic = {'m', 'n', 'e', 'm', 'o', 'n', 'i', 'c'};
byte[] key = Sha256Hash.create(mnemonic).getBytes();
RijndaelEngine cipher = new RijndaelEngine(len);
cipher.init(true, new KeyParameter(key));
for (int ii = 0; ii < 10000; ++ii)
cipher.processBlock(data, 0, data, 0);
return data;
}
private byte[] unstretch(int len, byte[] data) {
// 9. Decrypt this data 10000x with Rijndael (ECB mode),
// use the same parameters as used in step 3 of encryption.
byte[] mnemonic = {'m', 'n', 'e', 'm', 'o', 'n', 'i', 'c'};
byte[] key = Sha256Hash.create(mnemonic).getBytes();
RijndaelEngine cipher = new RijndaelEngine(len);
cipher.init(false, new KeyParameter(key));
for (int ii = 0; ii < 10000; ++ii)
cipher.processBlock(data, 0, data, 0);
return data;
}
private boolean[] checksum(boolean[] bits) {
// 4. Compute the length of the checkum (LC). LC = L/32
int lc = bits.length / 32;
// 5. Split I into chunks of LC bits (I1, I2, I3, ...).
// 6. XOR them altogether and produce the checksum C. C = I1 xor I2 xor I3 ... xor In.
boolean[] cc = new boolean[lc];
for (int ii = 0; ii < 32; ++ii)
for (int jj = 0; jj < lc; ++jj)
cc[jj] ^= bits[(ii * lc) + jj];
return cc;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,177 @@
/*
* Copyright 2013 Ken Sedgwick
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.bitcoin.crypto;
import org.junit.Test;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import org.spongycastle.util.encoders.Hex;
import static org.junit.Assert.assertEquals;
public class MnemonicCodeTest {
// These vectors are from https://raw.github.com/trezor/python-mnemonic/master/vectors.json
String vectors[] = {
"00000000000000000000000000000000",
"risk tiger venture dinner age assume float denial penalty hello game wing",
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"truth chase learn pretty right casual acoustic frozen betray main slogan method",
"80808080808080808080808080808080",
"olive garment twenty drill people finish hat own usual level milk usage",
"ffffffffffffffffffffffffffffffff",
"laundry faint system client frog vanish plug shell slot cable large embrace",
"000000000000000000000000000000000000000000000000",
"giant twelve seat embark ostrich jazz leader lunch budget hover much weapon vendor build truth garden year list",
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"awful faint gun mean fuel side slogan marine glad donkey velvet oyster movie real type digital dress federal",
"808080808080808080808080808080808080808080808080",
"bless carpet daughter animal hospital pave faculty escape fortune song sign twin unknown bread mobile normal agent use",
"ffffffffffffffffffffffffffffffffffffffffffffffff",
"saddle curve flight drama client resemble venture arch will ordinary enrich clutch razor shallow trophy tumble dice outer",
"0000000000000000000000000000000000000000000000000000000000000000",
"supreme army trim onion neglect coach squirrel spider device glass cabbage giant web digital floor able social magnet only fork fuel embrace salt fence",
"7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f7f",
"cloth video uncle switch year captain artist country adjust edit inherit ocean tennis soda baby express hospital forest panel actual profit boy spice elite",
"8080808080808080808080808080808080808080808080808080808080808080",
"fence twin prize extra choose mask twist deny cereal quarter can power term ostrich leg staff nature nut swift sausage amateur aim script wisdom",
"ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
"moon fiscal evidence exile rifle series neglect giant exclude banana glance frown kangaroo globe turtle hat fitness casual sudden select idle arctic best unlock",
"449ea2d7249c6e0d8d295424fb8894cf",
"choice barrel artefact cram increase sell veteran matrix mirror hollow walk pave",
"75fc3f44a7ff8e2b8af05aa18bded3827a3796df406763dd",
"crack outside teach chat praise client manual scorpion predict chalk decrease casino lunch garbage enable ball when bamboo",
"1cce2f8c2c6a7f2d8473ebf1c32ce13b36737835d7a8768f44dcf96d64782c0e",
"muffin evoke all fiber night guard black quote neck expire dial tenant leisure have dragon neck notable peace captain insane nice uphold shine angry",
"3daa82dd08bd144ec9fb9f77c6ece3d2",
"foil dawn net enroll turtle bird vault trumpet service fun immune unveil",
"9720239c0039f8446d44334daec325f3c24b3a490315d6d9",
"damp all desert dash insane pear debate easily soup enough goddess make friend plug violin pact wealth insect",
"fe58c6644bc3fad95832d4400cea0cce208c8b19bb4734a26995440b7fae7600",
"wet sniff asthma once gap enrich pumpkin define trust rude gesture keen grass fine emerge census immense smooth ritual spirit rescue problem beef choice",
"99fe82c94edadffe75e1cc64cbd7ada7",
"thing real emerge verify domain cloud lens teach travel radio effort glad",
"4fd6e8d06d55b4700130f8f462f7f9bfc6188da83e3faadb",
"diary opinion lobster code orange odor insane permit spirit evolve upset final antique grant friend dutch say enroll",
"7a547fb59606e89ba88188013712946f6cb31c3e0ca606a7ee1ff23f57272c63",
"layer owner legal stadium glance oyster element spell episode eager wagon stand pride old defense black print junior fade easy topic ready galaxy debris",
"e5fc62d20e0e5d9b2756e8d4d91cbb80",
"flat make unit discover rifle armed unit acquire group panel nerve want",
"d29be791a9e4b6a48ff79003dbf31d6afabdc4290a273765",
"absurd valve party disorder basket injury make blanket vintage ancient please random theory cart retire odor borrow belt",
"c87c135433c16f1ecbf9919dc53dd9f30f85824dc7264d4e1bd644826c902be2",
"upper will wisdom term once bean blur inquiry used bamboo frequent hamster amazing cake attack any author mimic leopard day token joy install company",
};
@Test
public void testEncodeVectors() throws Exception {
InputStream wordstream = getClass().getResourceAsStream("mnemonic/wordlist/english.txt");
MnemonicCode mc = new MnemonicCode(wordstream, MnemonicCode.BIP0039_ENGLISH_SHA256);
for (int ii = 0; ii < vectors.length; ii += 2) {
List<String> words = mc.encode(Hex.decode(vectors[ii]));
assertEquals(vectors[ii+1], join(words));
}
}
@Test
public void testDecodeVectors() throws Exception {
InputStream wordstream = getClass().getResourceAsStream("mnemonic/wordlist/english.txt");
MnemonicCode mc = new MnemonicCode(wordstream, MnemonicCode.BIP0039_ENGLISH_SHA256);
for (int ii = 0; ii < vectors.length; ii += 2) {
byte[] seed = mc.decode(split(vectors[ii+1]));
assertEquals(vectors[ii], new String(Hex.encode(seed)));
}
}
@Test
public void testBadSeedLength() throws Exception {
InputStream wordstream = getClass().getResourceAsStream("mnemonic/wordlist/english.txt");
MnemonicCode mc = new MnemonicCode(wordstream, MnemonicCode.BIP0039_ENGLISH_SHA256);
boolean sawException = false;
try {
byte[] seed = Hex.decode("7f7f7f7f7f7f7f7f7f7f7f7f7f7f");
List<String> words = mc.encode(seed);
} catch (IllegalArgumentException ex) {
sawException = true;
}
assertEquals(true, sawException);
}
@Test
public void testBadWordsLength() throws Exception {
InputStream wordstream = getClass().getResourceAsStream("mnemonic/wordlist/english.txt");
MnemonicCode mc = new MnemonicCode(wordstream, MnemonicCode.BIP0039_ENGLISH_SHA256);
boolean sawException = false;
try {
List<String> words = split("risk tiger venture dinner age assume float denial penalty");
byte[] seed = mc.decode(words);
} catch (IllegalArgumentException ex) {
sawException = true;
}
assertEquals(true, sawException);
}
static public String join(List<String> list) {
StringBuilder sb = new StringBuilder();
boolean first = true;
for (String item : list)
{
if (first)
first = false;
else
sb.append(" ");
sb.append(item);
}
return sb.toString();
}
static public List<String> split(String words) {
return new ArrayList<String>(Arrays.asList(words.split("\\s+")));
}
}