mirror of
https://github.com/bitcoinj/bitcoinj.git
synced 2025-01-19 05:33:44 +01:00
Added MnemonicCode, implements BIP-0039.
This commit is contained in:
parent
c11456c9f4
commit
8dcead3508
230
core/src/main/java/com/google/bitcoin/crypto/MnemonicCode.java
Normal file
230
core/src/main/java/com/google/bitcoin/crypto/MnemonicCode.java
Normal 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
@ -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+")));
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user