Home Reference Source

lib/ecc/src/KeyUtils.js

import PrivateKey from "./PrivateKey";
import PublicKey from "./PublicKey";
import Address from "./address";
import Aes from "./aes";
import {sha256, sha512} from "./hash";
// import dictionary from './dictionary_en';
import secureRandom from "secure-random";
import {ChainConfig} from "bitsharesjs-ws";
const Buffer = require("safe-buffer").Buffer;

// hash for .25 second
var HASH_POWER_MILLS = 250;

const key = {
    /** Uses 1 second of hashing power to create a key/password checksum.  An
    implementation can re-call this method with the same password to re-match
    the strength of the CPU (either after moving from a desktop to a mobile,
    mobile to desktop, or N years from now when CPUs are presumably stronger).

    A salt is used for all the normal reasons...

    @return object {
        aes_private: Aes,
        checksum: "{hash_iteration_count},{salt},{checksum}"
    }
    */
    aes_checksum(password) {
        if (!(typeof password === "string")) {
            throw new "password string required"();
        }
        var salt = secureRandom.randomBuffer(4).toString("hex");
        var iterations = 0;
        var secret = salt + password;
        // hash for .1 second
        var start_t = Date.now();
        while (Date.now() - start_t < HASH_POWER_MILLS) {
            secret = sha256(secret);
            iterations += 1;
        }

        var checksum = sha256(secret);
        var checksum_string = [
            iterations,
            salt.toString("hex"),
            checksum.slice(0, 4).toString("hex")
        ].join(",");

        return {
            aes_private: Aes.fromSeed(secret),
            checksum: checksum_string
        };
    },

    /** Provide a matching password and key_checksum.  A "wrong password"
    error is thrown if the password does not match.  If this method takes
    much more or less than 1 second to return, one should consider updating
    all encyrpted fields using a new key.key_checksum.
    */
    aes_private(password, key_checksum) {
        var [iterations, salt, checksum] = key_checksum.split(",");
        var secret = salt + password;
        for (
            var i = 0;
            0 < iterations ? i < iterations : i > iterations;
            0 < iterations ? i++ : i++
        ) {
            secret = sha256(secret);
        }
        var new_checksum = sha256(secret);
        if (!(new_checksum.slice(0, 4).toString("hex") === checksum)) {
            throw new Error("wrong password");
        }
        return Aes.fromSeed(secret);
    },

    /**
        A week random number generator can run out of entropy.  This should ensure even the worst random number implementation will be reasonably safe.

        @param1 string entropy of at least 32 bytes
    */
    random32ByteBuffer(entropy = this.browserEntropy()) {
        if (!(typeof entropy === "string")) {
            throw new Error("string required for entropy");
        }

        if (entropy.length < 32) {
            throw new Error("expecting at least 32 bytes of entropy");
        }

        var start_t = Date.now();

        while (Date.now() - start_t < HASH_POWER_MILLS)
            entropy = sha256(entropy);

        var hash_array = [];
        hash_array.push(entropy);

        // Hashing for 1 second may helps the computer is not low on entropy (this method may be called back-to-back).
        hash_array.push(secureRandom.randomBuffer(32));

        return sha256(Buffer.concat(hash_array));
    },

    suggest_brain_key: function(
        dictionary = ",",
        entropy = this.browserEntropy()
    ) {
        var randomBuffer = this.random32ByteBuffer(entropy);

        var word_count = 16;
        var dictionary_lines = dictionary.split(",");

        if (!(dictionary_lines.length === 49744)) {
            throw new Error(
                `expecting ${49744} but got ${
                    dictionary_lines.length
                } dictionary words`
            );
        }

        var brainkey = [];
        var end = word_count * 2;

        for (let i = 0; i < end; i += 2) {
            // randomBuffer has 256 bits / 16 bits per word == 16 words
            var num = (randomBuffer[i] << 8) + randomBuffer[i + 1];

            // convert into a number between 0 and 1 (inclusive)
            var rndMultiplier = num / Math.pow(2, 16);
            var wordIndex = Math.round(dictionary_lines.length * rndMultiplier);

            brainkey.push(dictionary_lines[wordIndex]);
        }
        return this.normalize_brainKey(brainkey.join(" "));
    },

    get_random_key(entropy) {
        return PrivateKey.fromBuffer(this.random32ByteBuffer(entropy));
    },

    get_brainPrivateKey(brainKey, sequence = 0) {
        if (sequence < 0) {
            throw new Error("invalid sequence");
        }
        brainKey = key.normalize_brainKey(brainKey);
        return PrivateKey.fromBuffer(sha256(sha512(brainKey + " " + sequence)));
    },

    // Turn invisible space like characters into a single space
    normalize_brainKey(brainKey) {
        if (!(typeof brainKey === "string")) {
            throw new Error("string required for brainKey");
        }

        brainKey = brainKey.trim();
        return brainKey.split(/[\t\n\v\f\r ]+/).join(" ");
    },

    browserEntropy() {
        var entropyStr = "";
        try {
            entropyStr =
                new Date().toString() +
                " " +
                window.screen.height +
                " " +
                window.screen.width +
                " " +
                window.screen.colorDepth +
                " " +
                " " +
                window.screen.availHeight +
                " " +
                window.screen.availWidth +
                " " +
                window.screen.pixelDepth +
                navigator.language +
                " " +
                window.location +
                " " +
                window.history.length;

            for (var i = 0, mimeType; i < navigator.mimeTypes.length; i++) {
                mimeType = navigator.mimeTypes[i];
                entropyStr +=
                    mimeType.description +
                    " " +
                    mimeType.type +
                    " " +
                    mimeType.suffixes +
                    " ";
            }
            console.log("INFO\tbrowserEntropy gathered");
        } catch (error) {
            //nodejs:ReferenceError: window is not defined
            entropyStr = sha256(new Date().toString());
        }

        var b = Buffer.from(entropyStr);
        entropyStr += b.toString("binary") + " " + new Date().toString();
        return entropyStr;
    },

    // @return array of 5 legacy addresses for a pubkey string parameter.
    addresses(pubkey, address_prefix = ChainConfig.address_prefix) {
        var public_key = PublicKey.fromPublicKeyString(pubkey, address_prefix);
        // S L O W
        var address_string = [
            Address.fromPublic(public_key, false, 0).toString(address_prefix), // btc_uncompressed
            Address.fromPublic(public_key, true, 0).toString(address_prefix), // btc_compressed
            Address.fromPublic(public_key, false, 56).toString(address_prefix), // pts_uncompressed
            Address.fromPublic(public_key, true, 56).toString(address_prefix), // pts_compressed
            public_key.toAddressString(address_prefix) // bts_short, most recent format
        ];
        return address_string;
    }
};

export default key;