All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.syntifi.crypto.key.mnemonic.MnemonicCode Maven / Gradle / Ivy

Go to download

SDK to streamline the 3rd party Java client integration processes. Such 3rd parties include exchanges & app developers.

The newest version!
package com.syntifi.crypto.key.mnemonic;

/*
 * Copyright 2013 Ken Sedgwick
 * Copyright 2014 Andreas Schildbach
 *
 * 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.
 *
 * CHANGES:
 * - Removing static initializer
 * - Using own Hex and Sha256 implementations
 * - Removing logs, watches, ...
 * - Removing internal dependencies to helper/utils
 * - Adding method to secure random derive the key
 * - Adding support for multiple languages
 *
 */

import com.syntifi.crypto.key.encdec.Hex;
import com.syntifi.crypto.key.hash.Sha256;
import com.syntifi.crypto.key.mnemonic.exception.MnemonicException;
import org.bouncycastle.crypto.digests.SHA512Digest;
import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator;
import org.bouncycastle.crypto.params.KeyParameter;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.text.Collator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;

/**
 * A MnemonicCode object may be used to convert between binary seed values and
 * lists of words per the BIP 39
 * specification
 * Original implementation at:
 * https://github.com/bitcoinj/bitcoinj/blob/master/core/src/main/java/org/bitcoinj/crypto/MnemonicCode.java
 *
 * @author Alexandre Carvalho
 * @author Andre Bertolace
 * @since 0.3.0
 */
public class MnemonicCode {
    private static final int PBKDF2_ROUNDS = 2048;
    private final List wordList;
    private final Language language;

    /**
     * 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.
     *
     * @param language      words languages
     * @throws IOException  if an error ocrurs when processing the file buffers
     */
    public MnemonicCode(Language language) throws IOException {
        this.language = language;
        InputStream wordStream = getClass().getResourceAsStream("/" + language.getFileName());
        if (wordStream == null)
            throw new FileNotFoundException(language.getFileName());
        try (BufferedReader br = new BufferedReader(new InputStreamReader(wordStream, language.getCharset()))) {
            this.wordList = br.lines()
                    .collect(Collectors.collectingAndThen(Collectors.toList(), Collections::unmodifiableList));
        }

        if (this.wordList.size() != 2048)
            throw new IllegalArgumentException("input stream did not contain 2048 bytes");

        // If a wordListDigest is supplied check to make sure it matches.
        if (language.getCheckSum()!= null) {
            StringBuilder stringBuilder = new StringBuilder();
            for (String s : this.getWordList()) {
                stringBuilder.append(s);
            }
            byte[] digest = Sha256.digest(String.valueOf(stringBuilder).getBytes(language.getCharset()));
            String hexDigest = Hex.encode(digest);
            if (!hexDigest.equals(language.getCheckSum()))
                throw new IllegalArgumentException("wordlist checksum mismatch");
        }
    }

    /**
     * Convert mnemonic word list to seed.
     *
     * @param words list of words
     * @return derived seed in byte array
     */
    public byte[] toSeed(List words) {
        return toSeed(words, "");
    }

    /**
     * Convert mnemonic word list to seed.
     *
     * @param words list of words
     * @param passphrase password, use {@link #toSeed(List)} if not required
     * @return derived seed in byte array
     */
    public byte[] toSeed(List words, String passphrase) {
        if (passphrase == null)
            throw new RuntimeException("A null passphrase is not allowed.");

        // 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 2048 and HMAC-SHA512 is
        // used as a pseudo-random function. Desired length of the
        // derived key is 512 bits (= 64 bytes).
        //
        String pass = String.join(" ", words);
        String salt = "mnemonic" + passphrase;

        final PKCS5S2ParametersGenerator pbkdf2 = new PKCS5S2ParametersGenerator(new SHA512Digest());
        pbkdf2.init(
                pass.getBytes(StandardCharsets.UTF_8),
                salt.getBytes(StandardCharsets.UTF_8),
                PBKDF2_ROUNDS);

        final KeyParameter key = (KeyParameter) pbkdf2.generateDerivedParameters(512);
        return key.getKey();
    }

    private 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;
    }

    /**
     * Gets the word list this code uses.
     *
     * @return unmodifiable word list
     */
    public List getWordList() {
        return wordList;
    }

    /**
     * Convert mnemonic word list to original entropy value.
     *
     * @param words list of words
     * @return entropy byte array
     * @throws MnemonicException.MnemonicLengthException if the number of words in the list is not multiple of 3 or empty
     * @throws MnemonicException.MnemonicWordException   if a word in the list is not part of the dictionary
     * @throws MnemonicException.MnemonicChecksumException if the checksum of the file does not match the specified one
     */
    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.");

        if (words.size() == 0)
            throw new MnemonicException.MnemonicLengthException("Word list is empty.");

        // 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;
        Collator collator = Collator.getInstance(language.getLocale());
        //collator.setDecomposition(Collator.FULL_DECOMPOSITION);
        //collator.setStrength(Collator.PRIMARY);
        for (String word : words) {
            // Find the words index in the wordlist.
            int ndx = Collections.binarySearch(this.wordList, word, collator);
            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 = Sha256.digest(entropy);
        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.
     *
     * @param entropy byte array
     * @return list of words
     * @throws MnemonicException.MnemonicLengthException if the number of words in the list is not multiple of 3 or empty
     */
    public List toMnemonic(byte[] entropy) throws MnemonicException.MnemonicLengthException {
        if (entropy.length % 4 > 0)
            throw new MnemonicException.MnemonicLengthException("Entropy length not multiple of 32 bits.");

        if (entropy.length == 0)
            throw new MnemonicException.MnemonicLengthException("Entropy is empty.");

        // We take initial entropy of ENT bits and compute its
        // checksum by taking first ENT / 32 bits of its SHA256 hash.

        byte[] hash = Sha256.digest(entropy);
        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.
     *
     * @param words list of words
     * @throws MnemonicException if an errors occurs on reading the dictionary of on the given words
     */
    public void check(List words) throws MnemonicException {
        toEntropy(words);
    }

    /**
     * Method to generate words from securerandom entropy
     *
     * @return list of mnemonic words
     * @throws IOException if an error occurs in reading the dictionary file
     * @throws MnemonicException.MnemonicLengthException if the number of words in the list is not multiple of 3 or empty
     */
    public List generateSecureRandomWords() throws IOException, MnemonicException.MnemonicLengthException {
        MnemonicCode mnemonicCode = new MnemonicCode(this.language);
        SecureRandom rnd = new SecureRandom();
        byte[] entropy = new byte[16];
        rnd.nextBytes(entropy);
        return mnemonicCode.toMnemonic(entropy);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy