blockstack
Version:
The Blockstack Javascript library for authentication, identity, and storage.
420 lines (353 loc) • 15.3 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.BlockstackWallet = undefined;
var _regenerator = require('babel-runtime/regenerator');
var _regenerator2 = _interopRequireDefault(_regenerator);
var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
var _crypto = require('crypto');
var _crypto2 = _interopRequireDefault(_crypto);
var _bitcoinjsLib = require('bitcoinjs-lib');
var _bitcoinjsLib2 = _interopRequireDefault(_bitcoinjsLib);
var _bip = require('bip39');
var _bip2 = _interopRequireDefault(_bip);
var _bip3 = require('bip32');
var _bip4 = _interopRequireDefault(_bip3);
var _utils = require('./utils');
var _encryption = require('./encryption');
function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
var APPS_NODE_INDEX = 0;
var IDENTITY_KEYCHAIN = 888;
var BLOCKSTACK_ON_BITCOIN = 0;
var BITCOIN_BIP_44_PURPOSE = 44;
var BITCOIN_COIN_TYPE = 0;
var BITCOIN_ACCOUNT_INDEX = 0;
var EXTERNAL_ADDRESS = 'EXTERNAL_ADDRESS';
var CHANGE_ADDRESS = 'CHANGE_ADDRESS';
function hashCode(string) {
var hash = 0;
if (string.length === 0) return hash;
for (var i = 0; i < string.length; i++) {
var character = string.charCodeAt(i);
hash = (hash << 5) - hash + character;
hash &= hash;
}
return hash & 0x7fffffff;
}
function getNodePrivateKey(node) {
return (0, _utils.ecPairToHexString)(_bitcoinjsLib.ECPair.fromPrivateKey(node.privateKey));
}
function getNodePublicKey(node) {
return node.publicKey.toString('hex');
}
/**
* The BlockstackWallet class manages the hierarchical derivation
* paths for a standard blockstack client wallet. This includes paths
* for bitcoin payment address, blockstack identity addresses, blockstack
* application specific addresses.
* @private
*/
var BlockstackWallet = exports.BlockstackWallet = function () {
function BlockstackWallet(rootNode) {
_classCallCheck(this, BlockstackWallet);
this.rootNode = rootNode;
}
_createClass(BlockstackWallet, [{
key: 'toBase58',
value: function toBase58() {
return this.rootNode.toBase58();
}
/**
* Initialize a blockstack wallet from a seed buffer
* @param {Buffer} seed - the input seed for initializing the root node
* of the hierarchical wallet
* @return {BlockstackWallet} the constructed wallet
*/
}, {
key: 'getIdentityPrivateKeychain',
value: function getIdentityPrivateKeychain() {
return this.rootNode.deriveHardened(IDENTITY_KEYCHAIN).deriveHardened(BLOCKSTACK_ON_BITCOIN);
}
}, {
key: 'getBitcoinPrivateKeychain',
value: function getBitcoinPrivateKeychain() {
return this.rootNode.deriveHardened(BITCOIN_BIP_44_PURPOSE).deriveHardened(BITCOIN_COIN_TYPE).deriveHardened(BITCOIN_ACCOUNT_INDEX);
}
}, {
key: 'getBitcoinNode',
value: function getBitcoinNode(addressIndex) {
var chainType = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : EXTERNAL_ADDRESS;
return BlockstackWallet.getNodeFromBitcoinKeychain(this.getBitcoinPrivateKeychain().toBase58(), addressIndex, chainType);
}
}, {
key: 'getIdentityAddressNode',
value: function getIdentityAddressNode(identityIndex) {
var identityPrivateKeychain = this.getIdentityPrivateKeychain();
return identityPrivateKeychain.deriveHardened(identityIndex);
}
}, {
key: 'getIdentitySalt',
/**
* Get a salt for use with creating application specific addresses
* @return {String} the salt
*/
value: function getIdentitySalt() {
var identityPrivateKeychain = this.getIdentityPrivateKeychain();
var publicKeyHex = getNodePublicKey(identityPrivateKeychain);
return _crypto2.default.createHash('sha256').update(publicKeyHex).digest('hex');
}
/**
* Get a bitcoin receive address at a given index
* @param {number} addressIndex - the index of the address
* @return {String} address
*/
}, {
key: 'getBitcoinAddress',
value: function getBitcoinAddress(addressIndex) {
return BlockstackWallet.getAddressFromBIP32Node(this.getBitcoinNode(addressIndex));
}
/**
* Get the private key hex-string for a given bitcoin receive address
* @param {number} addressIndex - the index of the address
* @return {String} the hex-string. this will be either 64
* characters long to denote an uncompressed bitcoin address, or 66
* characters long for a compressed bitcoin address.
*/
}, {
key: 'getBitcoinPrivateKey',
value: function getBitcoinPrivateKey(addressIndex) {
return getNodePrivateKey(this.getBitcoinNode(addressIndex));
}
/**
* Get the root node for the bitcoin public keychain
* @return {String} base58-encoding of the public node
*/
}, {
key: 'getBitcoinPublicKeychain',
value: function getBitcoinPublicKeychain() {
return this.getBitcoinPrivateKeychain().neutered();
}
/**
* Get the root node for the identity public keychain
* @return {String} base58-encoding of the public node
*/
}, {
key: 'getIdentityPublicKeychain',
value: function getIdentityPublicKeychain() {
return this.getIdentityPrivateKeychain().neutered();
}
}, {
key: 'getIdentityKeyPair',
/**
* Get the keypair information for a given identity index. This
* information is used to obtain the private key for an identity address
* and derive application specific keys for that address.
* @param {number} addressIndex - the identity index
* @param {boolean} alwaysUncompressed - if true, always return a
* private-key hex string corresponding to the uncompressed address
* @return {Object} an IdentityKeyPair type object with keys:
* .key {String} - the private key hex-string
* .keyID {String} - the public key hex-string
* .address {String} - the identity address
* .appsNodeKey {String} - the base-58 encoding of the applications node
* .salt {String} - the salt used for creating app-specific addresses
*/
value: function getIdentityKeyPair(addressIndex) {
var alwaysUncompressed = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : false;
var identityNode = this.getIdentityAddressNode(addressIndex);
var address = BlockstackWallet.getAddressFromBIP32Node(identityNode);
var identityKey = getNodePrivateKey(identityNode);
if (alwaysUncompressed && identityKey.length === 66) {
identityKey = identityKey.slice(0, 64);
}
var identityKeyID = getNodePublicKey(identityNode);
var appsNodeKey = BlockstackWallet.getAppsNode(identityNode).toBase58();
var salt = this.getIdentitySalt();
var keyPair = {
key: identityKey,
keyID: identityKeyID,
address: address,
appsNodeKey: appsNodeKey,
salt: salt
};
return keyPair;
}
}], [{
key: 'fromSeedBuffer',
value: function fromSeedBuffer(seed) {
return new BlockstackWallet(_bip4.default.fromSeed(seed));
}
/**
* Initialize a blockstack wallet from a base58 string
* @param {string} keychain - the Base58 string used to initialize
* the root node of the hierarchical wallet
* @return {BlockstackWallet} the constructed wallet
*/
}, {
key: 'fromBase58',
value: function fromBase58(keychain) {
return new BlockstackWallet(_bip4.default.fromBase58(keychain));
}
/**
* Initialize a blockstack wallet from an encrypted phrase & password. Throws
* if the password is incorrect. Supports all formats of Blockstack phrases.
* @param {string} data - The encrypted phrase as a hex-encoded string
* @param {string} password - The plain password
* @return {Promise<BlockstackWallet>} the constructed wallet
*/
}, {
key: 'fromEncryptedMnemonic',
value: function fromEncryptedMnemonic(data, password) {
return (0, _encryption.decryptMnemonic)(data, password).then(function (mnemonic) {
var seed = _bip2.default.mnemonicToSeed(mnemonic);
return new BlockstackWallet(_bip4.default.fromSeed(seed));
}).catch(function (err) {
if (err.message && err.message.startsWith('bad header;')) {
throw new Error('Incorrect password');
} else {
throw err;
}
});
}
/**
* Generate a BIP-39 12 word mnemonic
* @return {Promise<string>} space-separated 12 word phrase
*/
}, {
key: 'generateMnemonic',
value: function generateMnemonic() {
return _bip2.default.generateMnemonic(128, _crypto.randomBytes);
}
/**
* Encrypt a mnemonic phrase with a password
* @param {string} mnemonic - Raw mnemonic phrase
* @param {string} password - Password to encrypt mnemonic with
* @return {Promise<string>} Hex-encoded encrypted mnemonic
*/
}, {
key: 'encryptMnemonic',
value: function () {
var _ref = _asyncToGenerator( /*#__PURE__*/_regenerator2.default.mark(function _callee(mnemonic, password) {
var encryptedBuffer;
return _regenerator2.default.wrap(function _callee$(_context) {
while (1) {
switch (_context.prev = _context.next) {
case 0:
_context.next = 2;
return (0, _encryption.encryptMnemonic)(mnemonic, password);
case 2:
encryptedBuffer = _context.sent;
return _context.abrupt('return', encryptedBuffer.toString('hex'));
case 4:
case 'end':
return _context.stop();
}
}
}, _callee, this);
}));
function encryptMnemonic(_x3, _x4) {
return _ref.apply(this, arguments);
}
return encryptMnemonic;
}()
}, {
key: 'getAppsNode',
value: function getAppsNode(identityNode) {
return identityNode.deriveHardened(APPS_NODE_INDEX);
}
}, {
key: 'getNodeFromBitcoinKeychain',
value: function getNodeFromBitcoinKeychain(keychainBase58, addressIndex) {
var chainType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : EXTERNAL_ADDRESS;
var chain = void 0;
if (chainType === EXTERNAL_ADDRESS) {
chain = 0;
} else if (chainType === CHANGE_ADDRESS) {
chain = 1;
} else {
throw new Error('Invalid chain type');
}
var keychain = _bip4.default.fromBase58(keychainBase58);
return keychain.derive(chain).derive(addressIndex);
}
/**
* Get a bitcoin address given a base-58 encoded bitcoin node
* (usually called the account node)
* @param {String} keychainBase58 - base58-encoding of the node
* @param {number} addressIndex - index of the address to get
* @param {String} chainType - either 'EXTERNAL_ADDRESS' (for a
* "receive" address) or 'CHANGE_ADDRESS'
* @return {String} the address
*/
}, {
key: 'getAddressFromBitcoinKeychain',
value: function getAddressFromBitcoinKeychain(keychainBase58, addressIndex) {
var chainType = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : EXTERNAL_ADDRESS;
return BlockstackWallet.getAddressFromBIP32Node(BlockstackWallet.getNodeFromBitcoinKeychain(keychainBase58, addressIndex, chainType));
}
/**
* Get a ECDSA private key hex-string for an application-specific
* address.
* @param {String} appsNodeKey - the base58-encoded private key for
* applications node (the `appsNodeKey` return in getIdentityKeyPair())
* @param {String} salt - a string, used to salt the
* application-specific addresses
* @param {String} appDomain - the appDomain to generate a key for
* @return {String} the private key hex-string. this will be a 64
* character string
*/
}, {
key: 'getLegacyAppPrivateKey',
value: function getLegacyAppPrivateKey(appsNodeKey, salt, appDomain) {
var hash = _crypto2.default.createHash('sha256').update('' + appDomain + salt).digest('hex');
var appIndex = hashCode(hash);
var appNode = _bip4.default.fromBase58(appsNodeKey).deriveHardened(appIndex);
return getNodePrivateKey(appNode).slice(0, 64);
}
}, {
key: 'getAddressFromBIP32Node',
value: function getAddressFromBIP32Node(node) {
return _bitcoinjsLib2.default.payments.p2pkh({ pubkey: node.publicKey }).address;
}
/**
* Get a ECDSA private key hex-string for an application-specific
* address.
* @param {String} appsNodeKey - the base58-encoded private key for
* applications node (the `appsNodeKey` return in getIdentityKeyPair())
* @param {String} salt - a string, used to salt the
* application-specific addresses
* @param {String} appDomain - the appDomain to generate a key for
* @return {String} the private key hex-string. this will be a 64
* character string
*/
}, {
key: 'getAppPrivateKey',
value: function getAppPrivateKey(appsNodeKey, salt, appDomain) {
var hash = _crypto2.default.createHash('sha256').update('' + appDomain + salt).digest('hex');
var appIndexHexes = [];
// note: there's hardcoded numbers here, precisely because I want this
// code to be very specific to the derivation paths we expect.
if (hash.length !== 64) {
throw new Error('Unexpected app-domain hash length of ' + hash.length);
}
for (var i = 0; i < 11; i++) {
// split the hash into 3-byte chunks
// because child nodes can only be up to 2^31,
// and we shouldn't deal in partial bytes.
appIndexHexes.push(hash.slice(i * 6, i * 6 + 6));
}
var appNode = _bip4.default.fromBase58(appsNodeKey);
appIndexHexes.forEach(function (hex) {
if (hex.length > 6) {
throw new Error('Invalid hex string length');
}
appNode = appNode.deriveHardened(parseInt(hex, 16));
});
return getNodePrivateKey(appNode).slice(0, 64);
}
}]);
return BlockstackWallet;
}();