<commit_before>import alt from "alt-instance";
import BaseStore from "stores/BaseStore";

import iDB from "idb-instance";
import idb_helper from "idb-helper";
import {cloneDeep} from "lodash";

import PrivateKeyStore from "stores/PrivateKeyStore";
import SettingsStore from "stores/SettingsStore";
import {WalletTcomb} from "./tcomb_structs";
import TransactionConfirmActions from "actions/TransactionConfirmActions";
import WalletUnlockActions from "actions/WalletUnlockActions";
import PrivateKeyActions from "actions/PrivateKeyActions";
import {ChainStore, PrivateKey, key, Aes} from "bitsharesjs/es";
import {Apis, ChainConfig} from "bitsharesjs-ws";
import AddressIndex from "stores/AddressIndex";

let aes_private = null;
let _passwordKey = <PASSWORD>;
// let transaction;

let TRACE = false;

let dictJson, AesWorker;
if (__ELECTRON__) {
    AesWorker = require("worker-loader?inline!workers/AesWorker");
    dictJson = require("json-loader!common/dictionary_en.json");
}

/** Represents a single wallet and related indexedDb database operations. */
class WalletDb extends BaseStore {

    constructor() {
        super()
        this.state = { wallet: null, saving_keys: false }
        // Confirm only works when there is a UI (this is for mocha unit tests)
        this.confirm_transactions = true
        ChainStore.subscribe(this.checkNextGeneratedKey.bind(this))
        this.generateNextKey_pubcache = []
        // WalletDb use to be a plan old javascript class (not an Alt store) so
        // for now many methods need to be exported...
        this._export(
            "checkNextGeneratedKey","getWallet","onLock","isLocked","decryptTcomb_PrivateKey","getPrivateKey","process_transaction","transaction_update","transaction_update_keys","getBrainKey","getBrainKeyPrivate","onCreateWallet","validatePassword","changePassword","generateNextKey","incrementBrainKeySequence","saveKeys","saveKey","setWalletModified","setBackupDate","setBrainkeyBackupDate","_updateWallet","loadDbData",
            "importKeysWorker", "resetBrainKeySequence", "decrementBrainKeySequence", "generateKeyFromPassword"
        );
        this.generatingKey = false;
    }

    /** Discover derived keys that are not in this wallet */
    checkNextGeneratedKey() {
        if( ! this.state.wallet) return;
        if( ! aes_private) return; // locked
        if( ! this.state.wallet.encrypted_brainkey) return; // no brainkey
        if(this.chainstore_account_ids_by_key === ChainStore.account_ids_by_key)
            return; // no change
        this.chainstore_account_ids_by_key = ChainStore.account_ids_by_key;
        // Helps to ensure we are looking at an un-used key
        try {
            this.generateNextKey( false /*save*/ );
        } catch(e) {
            console.error(e);
        }
    }

    getWallet() {
        return this.state.wallet
    }

    onLock() {
        _passwordKey = null;
        aes_private = null;
    }

    isLocked() {
        return !(!!aes_private || !!_passwordKey);
    }

    decryptTcomb_PrivateKey(private_key_tcomb) {
        if( ! private_key_tcomb) return null
        if( ! aes_private) throw new Error("wallet locked")
        let private_key_hex = aes_private.decryptHex(private_key_tcomb.encrypted_key)
        return PrivateKey.fromBuffer(new Buffer(private_key_hex, 'hex'))
    }

    /** @return ecc/PrivateKey or null */
    getPrivateKey(public_key) {
        if (_passwordKey) return _passwordKey[public_key];
        if(! public_key) return null
        if(public_key.Q) public_key = public_key.toPublicKeyString()
        let private_key_tcomb = PrivateKeyStore.getTcomb_byPubkey(public_key)
        if(! private_key_tcomb) return null
        return this.decryptTcomb_PrivateKey(private_key_tcomb)
    }

    process_transaction(tr, signer_pubkeys, broadcast, extra_keys = []) {
        const passwordLogin = SettingsStore.getState().settings.get("passwordLogin");

        if(!passwordLogin && Apis.instance().chain_id !== this.state.wallet.chain_id)
            return Promise.reject("Mismatched chain_id; expecting " +
                this.state.wallet.chain_id + ", but got " +
                Apis.instance().chain_id)

        return WalletUnlockActions.unlock().then( () => {
            return Promise.all([
                tr.set_required_fees(),
                tr.update_head_block()
            ]).then(()=> {
                let signer_pubkeys_added = {}
                if(signer_pubkeys) {
                    // Balance claims are by address, only the private
                    // key holder can know about these additional
                    // potential keys.
                    let pubkeys = PrivateKeyStore.getPubkeys_having_PrivateKey(signer_pubkeys)
                    if( ! pubkeys.length)
                        throw new Error("Missing signing key")

                    for(let pubkey_string of pubkeys) {
                        let private_key = this.getPrivateKey(pubkey_string)
                        tr.add_signer(private_key, pubkey_string)
                        signer_pubkeys_added[pubkey_string] = true
                    }
                }

                return tr.get_potential_signatures().then( ({pubkeys, addys})=> {
                    let my_pubkeys = PrivateKeyStore.getPubkeys_having_PrivateKey(pubkeys.concat(extra_keys), addys);

                    //{//Testing only, don't send All public keys!
                    //    let pubkeys_all = PrivateKeyStore.getPubkeys() // All public keys
                    //    tr.get_required_signatures(pubkeys_all).then( required_pubkey_strings =>
                    //        console.log('get_required_signatures all\t',required_pubkey_strings.sort(), pubkeys_all))
                    //    tr.get_required_signatures(my_pubkeys).then( required_pubkey_strings =>
                    //        console.log('get_required_signatures normal\t',required_pubkey_strings.sort(), pubkeys))
                    //}
                    return tr.get_required_signatures(my_pubkeys).then( required_pubkeys => {
                        for(let pubkey_string of required_pubkeys) {
                            if(signer_pubkeys_added[pubkey_string]) continue
                            let private_key = this.getPrivateKey(pubkey_string)
                            if( ! private_key)
                                // This should not happen, get_required_signatures will only
                                // returned keys from my_pubkeys
                                throw new Error("Missing signing key for " + pubkey_string)
                            tr.add_signer(private_key, pubkey_string)
                        }
                    })
                }).then(()=> {
                    if(broadcast) {
                        if(this.confirm_transactions) {
                            let p = new Promise((resolve, reject) => {
                                TransactionConfirmActions.confirm(tr, resolve, reject)
                            })
                            return p;
                        }
                        else
                            return tr.broadcast()

                    } else
                        return tr.serialize()
                })
            })
        })
    }

    transaction_update() {
        let transaction = iDB.instance().db().transaction(
            ["wallet"], "readwrite"
        )
        return transaction
    }

    transaction_update_keys() {
        let transaction = iDB.instance().db().transaction(
            ["wallet", "private_keys"], "readwrite"
        )
        return transaction
    }

    getBrainKey() {
        let wallet = this.state.wallet
        if ( ! wallet.encrypted_brainkey) throw new Error("missing brainkey")
        if ( ! aes_private) throw new Error("wallet locked")
        let brainkey_plaintext = aes_private.decryptHexToText( wallet.encrypted_brainkey )
        return brainkey_plaintext
    }

    getBrainKeyPrivate(brainkey_plaintext = this.getBrainKey()) {
        if( ! brainkey_plaintext) throw new Error("missing brainkey")
        return PrivateKey.fromSeed( key.normalize_brainKey(brainkey_plaintext) )
    }

    onCreateWallet(
        password_plaintext,
        brainkey_plaintext,
        unlock = false,
        public_name = "default"
    ) {
        let walletCreateFct = (dictionary) => {
            return new Promise( (resolve, reject) => {
                if( typeof password_plaintext !== 'string')
                    throw new Error("password string is required")

                let brainkey_backup_date
                if(brainkey_plaintext) {
                    if(typeof brainkey_plaintext !== "string")
                        throw new Error("Brainkey must be a string")

                    if(brainkey_plaintext.trim() === "")
                        throw new Error("Brainkey can not be an empty string")

                    if(brainkey_plaintext.length < 50)
                        throw new Error("Brainkey must be at least 50 characters long")

                    // The user just provided the Brainkey so this avoids
                    // bugging them to back it up again.
                    brainkey_backup_date = new Date()
                }
                let password_aes = Aes.fromSeed( password_plaintext )

                let encryption_buffer = key.get_random_key().toBuffer()
                // encryption_key is the global encryption key (does not change even if the passsword changes)
                let encryption_key = password_aes.encryptToHex( encryption_buffer )
                // If unlocking, local_aes_private will become the global aes_private object
                let local_aes_private = Aes.fromSeed( encryption_buffer )

                if( ! brainkey_plaintext)
                    brainkey_plaintext = key.suggest_brain_key(dictionary.en)
                else
                    brainkey_plaintext = key.normalize_brainKey(brainkey_plaintext)
                let brainkey_private = this.getBrainKeyPrivate( brainkey_plaintext )
                let brainkey_pubkey = brainkey_private.toPublicKey().toPublicKeyString()
                let encrypted_brainkey = local_aes_private.encryptToHex( brainkey_plaintext )

                let password_private = PrivateKey.fromSeed( password_plaintext )
                let password_pubkey = password_private.toPublicKey().toPublicKeyString()

                let wallet = {
                    public_name,
                    password_pubkey,
                    encryption_key,
                    encrypted_brainkey,
                    brainkey_pubkey,
                    brainkey_sequence: 0,
                    brainkey_backup_date,
                    created: new Date(),
                    last_modified: new Date(),
                    chain_id: Apis.instance().chain_id
                }
                WalletTcomb(wallet) // validation
                let transaction = this.transaction_update()
                let add = idb_helper.add( transaction.objectStore("wallet"), wallet )
                let end = idb_helper.on_transaction_end(transaction).then( () => {
                    this.state.wallet = wallet
                    this.setState({ wallet })
                    if(unlock) aes_private = local_aes_private
                })
                Promise.all([ add, end ]).then(() => {
                    resolve();
                }).catch(err => {
                    reject(err);
                })
            })
        };

        if (__ELECTRON__) {
            return walletCreateFct(dictJson);
        } else {
            let dictionaryPromise = brainkey_plaintext ? null : fetch(`${__BASE_URL__}dictionary.json`);
            return Promise.all([
                dictionaryPromise
            ]).then(res => {
                return brainkey_plaintext ? walletCreateFct(null) :
                    res[0].json().then(walletCreateFct);
            }).catch(err => {
                console.log("unable to fetch dictionary.json", err);
            });
        }
    }

    generateKeyFromPassword(accountName, role, password) {
        let seed = accountName + role + password;
        let privKey = PrivateKey.fromSeed(seed);
        let pubKey = privKey.toPublicKey().toString();

        return {privKey, pubKey};
    }

    /** This also serves as 'unlock' */
    validatePassword( password, unlock = false, account = null, roles = ["active", "owner", "memo"] ) {
        if (account) {
            roles.forEach(role => {
                let {privKey, pubKey} = this.generateKeyFromPassword(account, role, password);
                let acc = ChainStore.getAccount(account);

                function setKey(role, priv, pub) {
                    if (!_passwordKey) _passwordKey = {};
                    _passwordKey[pub] = priv;
                    PrivateKeyStore.setPasswordLoginKey({
                        pubkey: pubKey,
                        import_account_names: [account],
                        encrypted_key: null,
                        id: 1,
                        brainkey_sequence: null
                    });
                }

                if (acc) {
                    if (role === "memo") {
                        if (acc.getIn(["options", "memo_key"]) === pubKey) {
                            setKey(role, privKey, pubKey);
                        }
                    } else {
                        acc.getIn([role, "key_auths"]).forEach(auth => {
                            if (auth.get(0) === pubKey) {
                                setKey(role, privKey, pubKey);
                                return false;<commit_msg>Add support for logging in with private key directly<commit_after>import alt from "alt-instance";
import BaseStore from "stores/BaseStore";

import iDB from "idb-instance";
import idb_helper from "idb-helper";
import {cloneDeep} from "lodash";

import PrivateKeyStore from "stores/PrivateKeyStore";
import SettingsStore from "stores/SettingsStore";
import {WalletTcomb} from "./tcomb_structs";
import TransactionConfirmActions from "actions/TransactionConfirmActions";
import WalletUnlockActions from "actions/WalletUnlockActions";
import PrivateKeyActions from "actions/PrivateKeyActions";
import {ChainStore, PrivateKey, key, Aes} from "bitsharesjs/es";
import {Apis, ChainConfig} from "bitsharesjs-ws";
import AddressIndex from "stores/AddressIndex";

let aes_private = null;
let _passwordKey = null;
// let transaction;

let TRACE = false;

let dictJson, AesWorker;
if (__ELECTRON__) {
    AesWorker = require("worker-loader?inline!workers/AesWorker");
    dictJson = require("json-loader!common/dictionary_en.json");
}

/** Represents a single wallet and related indexedDb database operations. */
class WalletDb extends BaseStore {

    constructor() {
        super()
        this.state = { wallet: null, saving_keys: false }
        // Confirm only works when there is a UI (this is for mocha unit tests)
        this.confirm_transactions = true
        ChainStore.subscribe(this.checkNextGeneratedKey.bind(this))
        this.generateNextKey_pubcache = []
        // WalletDb use to be a plan old javascript class (not an Alt store) so
        // for now many methods need to be exported...
        this._export(
            "checkNextGeneratedKey","getWallet","onLock","isLocked","decryptTcomb_PrivateKey","getPrivateKey","process_transaction","transaction_update","transaction_update_keys","getBrainKey","getBrainKeyPrivate","onCreateWallet","validatePassword","changePassword","generateNextKey","incrementBrainKeySequence","saveKeys","saveKey","setWalletModified","setBackupDate","setBrainkeyBackupDate","_updateWallet","loadDbData",
            "importKeysWorker", "resetBrainKeySequence", "decrementBrainKeySequence", "generateKeyFromPassword"
        );
        this.generatingKey = false;
    }

    /** Discover derived keys that are not in this wallet */
    checkNextGeneratedKey() {
        if( ! this.state.wallet) return;
        if( ! aes_private) return; // locked
        if( ! this.state.wallet.encrypted_brainkey) return; // no brainkey
        if(this.chainstore_account_ids_by_key === ChainStore.account_ids_by_key)
            return; // no change
        this.chainstore_account_ids_by_key = ChainStore.account_ids_by_key;
        // Helps to ensure we are looking at an un-used key
        try {
            this.generateNextKey( false /*save*/ );
        } catch(e) {
            console.error(e);
        }
    }

    getWallet() {
        return this.state.wallet
    }

    onLock() {
        _passwordKey = null;
        aes_private = null;
    }

    isLocked() {
        return !(!!aes_private || !!_passwordKey);
    }

    decryptTcomb_PrivateKey(private_key_tcomb) {
        if( ! private_key_tcomb) return null
        if( ! aes_private) throw new Error("wallet locked")
        let private_key_hex = aes_private.decryptHex(private_key_tcomb.encrypted_key)
        return PrivateKey.fromBuffer(new Buffer(private_key_hex, 'hex'))
    }

    /** @return ecc/PrivateKey or null */
    getPrivateKey(public_key) {
        if (_passwordKey) return _passwordKey[public_key];
        if(! public_key) return null
        if(public_key.Q) public_key = public_key.toPublicKeyString()
        let private_key_tcomb = PrivateKeyStore.getTcomb_byPubkey(public_key)
        if(! private_key_tcomb) return null
        return this.decryptTcomb_PrivateKey(private_key_tcomb)
    }

    process_transaction(tr, signer_pubkeys, broadcast, extra_keys = []) {
        const passwordLogin = SettingsStore.getState().settings.get("passwordLogin");

        if(!passwordLogin && Apis.instance().chain_id !== this.state.wallet.chain_id)
            return Promise.reject("Mismatched chain_id; expecting " +
                this.state.wallet.chain_id + ", but got " +
                Apis.instance().chain_id)

        return WalletUnlockActions.unlock().then( () => {
            return Promise.all([
                tr.set_required_fees(),
                tr.update_head_block()
            ]).then(()=> {
                let signer_pubkeys_added = {}
                if(signer_pubkeys) {
                    // Balance claims are by address, only the private
                    // key holder can know about these additional
                    // potential keys.
                    let pubkeys = PrivateKeyStore.getPubkeys_having_PrivateKey(signer_pubkeys)
                    if( ! pubkeys.length)
                        throw new Error("Missing signing key")

                    for(let pubkey_string of pubkeys) {
                        let private_key = this.getPrivateKey(pubkey_string)
                        tr.add_signer(private_key, pubkey_string)
                        signer_pubkeys_added[pubkey_string] = true
                    }
                }

                return tr.get_potential_signatures().then( ({pubkeys, addys})=> {
                    let my_pubkeys = PrivateKeyStore.getPubkeys_having_PrivateKey(pubkeys.concat(extra_keys), addys);

                    //{//Testing only, don't send All public keys!
                    //    let pubkeys_all = PrivateKeyStore.getPubkeys() // All public keys
                    //    tr.get_required_signatures(pubkeys_all).then( required_pubkey_strings =>
                    //        console.log('get_required_signatures all\t',required_pubkey_strings.sort(), pubkeys_all))
                    //    tr.get_required_signatures(my_pubkeys).then( required_pubkey_strings =>
                    //        console.log('get_required_signatures normal\t',required_pubkey_strings.sort(), pubkeys))
                    //}
                    return tr.get_required_signatures(my_pubkeys).then( required_pubkeys => {
                        for(let pubkey_string of required_pubkeys) {
                            if(signer_pubkeys_added[pubkey_string]) continue
                            let private_key = this.getPrivateKey(pubkey_string)
                            if( ! private_key)
                                // This should not happen, get_required_signatures will only
                                // returned keys from my_pubkeys
                                throw new Error("Missing signing key for " + pubkey_string)
                            tr.add_signer(private_key, pubkey_string)
                        }
                    })
                }).then(()=> {
                    if(broadcast) {
                        if(this.confirm_transactions) {
                            let p = new Promise((resolve, reject) => {
                                TransactionConfirmActions.confirm(tr, resolve, reject)
                            })
                            return p;
                        }
                        else
                            return tr.broadcast()

                    } else
                        return tr.serialize()
                })
            })
        })
    }

    transaction_update() {
        let transaction = iDB.instance().db().transaction(
            ["wallet"], "readwrite"
        )
        return transaction
    }

    transaction_update_keys() {
        let transaction = iDB.instance().db().transaction(
            ["wallet", "private_keys"], "readwrite"
        )
        return transaction
    }

    getBrainKey() {
        let wallet = this.state.wallet
        if ( ! wallet.encrypted_brainkey) throw new Error("missing brainkey")
        if ( ! aes_private) throw new Error("wallet locked")
        let brainkey_plaintext = aes_private.decryptHexToText( wallet.encrypted_brainkey )
        return brainkey_plaintext
    }

    getBrainKeyPrivate(brainkey_plaintext = this.getBrainKey()) {
        if( ! brainkey_plaintext) throw new Error("missing brainkey")
        return PrivateKey.fromSeed( key.normalize_brainKey(brainkey_plaintext) )
    }

    onCreateWallet(
        password_plaintext,
        brainkey_plaintext,
        unlock = false,
        public_name = "default"
    ) {
        let walletCreateFct = (dictionary) => {
            return new Promise( (resolve, reject) => {
                if( typeof password_plaintext !== 'string')
                    throw new Error("password string is required")

                let brainkey_backup_date
                if(brainkey_plaintext) {
                    if(typeof brainkey_plaintext !== "string")
                        throw new Error("Brainkey must be a string")

                    if(brainkey_plaintext.trim() === "")
                        throw new Error("Brainkey can not be an empty string")

                    if(brainkey_plaintext.length < 50)
                        throw new Error("Brainkey must be at least 50 characters long")

                    // The user just provided the Brainkey so this avoids
                    // bugging them to back it up again.
                    brainkey_backup_date = new Date()
                }
                let password_aes = Aes.fromSeed( password_plaintext )

                let encryption_buffer = key.get_random_key().toBuffer()
                // encryption_key is the global encryption key (does not change even if the passsword changes)
                let encryption_key = password_aes.encryptToHex( encryption_buffer )
                // If unlocking, local_aes_private will become the global aes_private object
                let local_aes_private = Aes.fromSeed( encryption_buffer )

                if( ! brainkey_plaintext)
                    brainkey_plaintext = key.suggest_brain_key(dictionary.en)
                else
                    brainkey_plaintext = key.normalize_brainKey(brainkey_plaintext)
                let brainkey_private = this.getBrainKeyPrivate( brainkey_plaintext )
                let brainkey_pubkey = brainkey_private.toPublicKey().toPublicKeyString()
                let encrypted_brainkey = local_aes_private.encryptToHex( brainkey_plaintext )

                let password_private = PrivateKey.fromSeed( password_plaintext )
                let password_pubkey = password_private.toPublicKey().toPublicKeyString()

                let wallet = {
                    public_name,
                    password_pubkey,
                    encryption_key,
                    encrypted_brainkey,
                    brainkey_pubkey,
                    brainkey_sequence: 0,
                    brainkey_backup_date,
                    created: new Date(),
                    last_modified: new Date(),
                    chain_id: Apis.instance().chain_id
                }
                WalletTcomb(wallet) // validation
                let transaction = this.transaction_update()
                let add = idb_helper.add( transaction.objectStore("wallet"), wallet )
                let end = idb_helper.on_transaction_end(transaction).then( () => {
                    this.state.wallet = wallet
                    this.setState({ wallet })
                    if(unlock) aes_private = local_aes_private
                })
                Promise.all([ add, end ]).then(() => {
                    resolve();
                }).catch(err => {
                    reject(err);
                })
            })
        };

        if (__ELECTRON__) {
            return walletCreateFct(dictJson);
        } else {
            let dictionaryPromise = brainkey_plaintext ? null : fetch(`${__BASE_URL__}dictionary.json`);
            return Promise.all([
                dictionaryPromise
            ]).then(res => {
                return brainkey_plaintext ? walletCreateFct(null) :
                    res[0].json().then(walletCreateFct);
            }).catch(err => {
                console.log("unable to fetch dictionary.json", err);
            });
        }
    }

    generateKeyFromPassword(accountName, role, password) {
        let seed = accountName + role + password;
        let privKey = PrivateKey.fromSeed(seed);
        let pubKey = privKey.toPublicKey().toString();

        return {privKey, pubKey};
    }

    /** This also serves as 'unlock' */
    validatePassword( password, unlock = false, account = null, roles = ["active", "owner", "memo"] ) {
        if (account) {
            function setKey(role, priv, pub) {
                if (!_passwordKey) _passwordKey = {};
                _passwordKey[pub] = priv;
                PrivateKeyStore.setPasswordLoginKey({
                    pubkey: pub,
                    import_account_names: [account],
                    encrypted_key: null,
                    id: 1,
                    brainkey_sequence: null
                });
            }

            /* Check if the user tried to login with a private key */
            let fromWif;
            try {
                fromWif = PrivateKey.fromWif(password);
            } catch(err) {

            }
            let acc = ChainStore.getAccount(account);
            let key;
            if (fromWif) {
                key = {privKey: fromWif, pubKey: fromWif.toPublicKey().toString()};
            }

            /* Test the pubkey for each role against either the wif key, or the password generated keys */
            roles.forEach(role => {
                if (!fromWif) {
                    key = this.generateKeyFromPassword(account, role, password);
                }

                if (acc) {
                    if (role === "memo") {
                        if (acc.getIn(["options", "memo_key"]) === key.pubKey) {
                            setKey(role, key.privKey, key.pubKey);
                        }
                    } else {
                        acc.getIn([role, "key_auths"]).forEach(auth => {
                            if (auth.get(0) === key.pubKey) {
                                setKey(role, key.privKey, key.pubKey);
                                return false;
{'fraction_non_alphanumeric': 0.09230383195325663, 'fraction_numerical': 0.0005735383733017887, 'mean_word_length': 1.7903580716143228, 'pattern_counts': {'":': 0, '<': 6, '<?xml version=': 0, '>': 38, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 6, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '6171143', 'n_tokens_mistral': 7390, 'n_tokens_neox': 7035, 'n_words': 1541}