com.google.bitcoin.crypto.MnemonicCode Maven / Gradle / Ivy
The newest version!
/*
* 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 com.google.bitcoin.core.Sha256Hash;
import com.google.common.base.Joiner;
import org.spongycastle.util.encoders.Hex;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* A MnemonicCode object may be used to convert between binary seed values and
* lists of words per the BIP 39
* specification
*/
public class MnemonicCode {
private ArrayList wordList;
public static String BIP39_ENGLISH_SHA256 = "ad90bf3beb7b0eb7e5acd74727dc0da96e0a280a258354e7293fb7e211ac03db";
public enum Version { V0_5, V0_6 }
private static final int PBKDF2_ROUNDS_V0_5 = 4096;
private static final int PBKDF2_ROUNDS_V0_6 = 2048;
public MnemonicCode() throws IOException {
this(MnemonicCode.class.getResourceAsStream("mnemonic/wordlist/english.txt"), BIP39_ENGLISH_SHA256);
}
/**
* 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();
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");
}
}
/**
* Convert mnemonic word list to seed.
*/
public static byte[] toSeed(List words, String passphrase) {
return toSeed(words, passphrase, Version.V0_6);
}
public static byte[] toSeed(List words, String passphrase, Version version) {
// To create binary seed from mnemonic, we use PBKDF2 function
// with mnemonic sentence (in UTF-8) used as a password and
// string "mnemonic" + passphrase (again in UTF-8) used as a
// salt. Iteration count is set to 4096 and HMAC-SHA512 is
// used as a pseudo-random function. Desired length of the
// derived key is 512 bits (= 64 bytes).
//
String pass = Joiner.on(' ').join(words);
String salt = "mnemonic" + passphrase;
int rounds = (version == Version.V0_5) ? PBKDF2_ROUNDS_V0_5 : PBKDF2_ROUNDS_V0_6;
return PBKDF2SHA512.derive(pass, salt, rounds, 64);
}
/**
* Convert mnemonic word list to original entropy value.
*/
public byte[] toEntropy(List words) throws MnemonicException.MnemonicLengthException, MnemonicException.MnemonicWordException, MnemonicException.MnemonicChecksumException {
if (words.size() % 3 > 0)
throw new MnemonicException.MnemonicLengthException("Word list size must be multiple of three words.");
// Look up all the words in the list and construct the
// concatenation of the original entropy and the checksum.
//
int concatLenBits = words.size() * 11;
boolean[] concatBits = new boolean[concatLenBits];
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 MnemonicException.MnemonicWordException(word);
// Set the next 11 bits to the value of the index.
for (int ii = 0; ii < 11; ++ii)
concatBits[(wordindex * 11) + ii] = (ndx & (1 << (10 - ii))) != 0;
++wordindex;
}
int checksumLengthBits = concatLenBits / 33;
int entropyLengthBits = concatLenBits - checksumLengthBits;
// Extract original entropy as bytes.
byte[] entropy = new byte[entropyLengthBits / 8];
for (int ii = 0; ii < entropy.length; ++ii)
for (int jj = 0; jj < 8; ++jj)
if (concatBits[(ii * 8) + jj])
entropy[ii] |= 1 << (7 - jj);
// Take the digest of the entropy.
byte[] hash = Sha256Hash.create(entropy).getBytes();
boolean[] hashBits = bytesToBits(hash);
// Check all the checksum bits.
for (int i = 0; i < checksumLengthBits; ++i)
if (concatBits[entropyLengthBits + i] != hashBits[i])
throw new MnemonicException.MnemonicChecksumException();
return entropy;
}
/**
* Convert entropy data to mnemonic word list.
*/
public List toMnemonic(byte[] entropy) throws MnemonicException.MnemonicLengthException {
if (entropy.length % 4 > 0)
throw new MnemonicException.MnemonicLengthException("entropy length not multiple of 32 bits");
// We take initial entropy of ENT bits and compute its
// checksum by taking first ENT / 32 bits of its SHA256 hash.
byte[] hash = Sha256Hash.create(entropy).getBytes();
boolean[] hashBits = bytesToBits(hash);
boolean[] entropyBits = bytesToBits(entropy);
int checksumLengthBits = entropyBits.length / 32;
// We append these bits to the end of the initial entropy.
boolean[] concatBits = new boolean[entropyBits.length + checksumLengthBits];
System.arraycopy(entropyBits, 0, concatBits, 0, entropyBits.length);
System.arraycopy(hashBits, 0, concatBits, entropyBits.length, checksumLengthBits);
// Next we take these concatenated bits and split them into
// groups of 11 bits. Each group encodes number from 0-2047
// which is a position in a wordlist. We convert numbers into
// words and use joined words as mnemonic sentence.
ArrayList words = new ArrayList();
int nwords = concatBits.length / 11;
for (int i = 0; i < nwords; ++i) {
int index = 0;
for (int j = 0; j < 11; ++j) {
index <<= 1;
if (concatBits[(i * 11) + j])
index |= 0x1;
}
words.add(this.wordList.get(index));
}
return words;
}
/**
* Check to see if a mnemonic word list is valid.
*/
public void check(List words) throws MnemonicException {
toEntropy(words);
}
private static boolean[] bytesToBits(byte[] data) {
boolean[] bits = new boolean[data.length * 8];
for (int i = 0; i < data.length; ++i)
for (int j = 0; j < 8; ++j)
bits[(i * 8) + j] = (data[i] & (1 << (7 - j))) != 0;
return bits;
}
}