Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
39f6340
first pass at moving address generation functionality into blockstack.js
Apr 17, 2018
b485c78
dont use HDNode in public facing interfaces
Apr 17, 2018
5384bdf
comments for the _intended_ public functions of the wallet object
Apr 17, 2018
3d425db
added unit tests
Apr 17, 2018
5d3d780
Merge branch 'develop' into feature/add-wallet-functions
Apr 23, 2018
7b5b3d7
add changelog entry for wallet functions
Apr 23, 2018
8eb3f42
Merge branch 'develop' into feature/add-wallet-functions
May 17, 2018
aa1ace5
add a long derivation path that encodes all the bytes from the sha256…
May 17, 2018
ca8dd6a
adding code and tests for the alternative new derivation path
May 22, 2018
ec40c63
more bit twiddling tests
May 22, 2018
c120395
Merge branch 'develop' into feature/add-wallet-functions
Jul 3, 2018
23469e6
only implement the long derivation path for the new apps key
Jul 3, 2018
02feb70
remove dead code paths
Jul 4, 2018
a42fe44
Add encryption and decryption to wallet.
wbobeirne Jul 17, 2018
62fa8fa
Support buffers for decryption
wbobeirne Jul 17, 2018
c2ff7ce
Merge branch 'develop', refactor for bitcoinjs-lib & add bip32 flow t…
larrysalibra Jul 31, 2018
956e86d
fix bip32 typedef errors
larrysalibra Jul 31, 2018
50248c0
mark methods not part of app developer API as private
larrysalibra Jul 31, 2018
e276efa
Merge branch 'develop' into feature/add-wallet-functions
larrysalibra Jul 31, 2018
e8b6c72
Merge branch 'develop' into feature/add-wallet-functions
Aug 6, 2018
78fdcb8
add toBase58 to wallet so clients twiddle with bip32s less
Aug 6, 2018
36bb1e5
use incorrect password message when given a bad password
Aug 6, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- The `BlockstackWallet` class in `blockstack.js` supports generating private keys
and addresses within the hierarchical derivation scheme used by the Blockstack
Browser and supported by the Blockstack ecosystem.
- Support for `verify` and `sign` keywords in `getFile` and `putFile`
respectively. This enables support for ECDSA signatures on SHA256
hashes in the storage operations, and works for encrypted and
Expand All @@ -16,7 +19,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- New `TransactionSigner` interface to allow for different signing agents
in the `transactions` functions (e.g., makePreorder).
- `putFile` can now optionally take the public key for which you want
to encrypt the file. Thanks to @bodymindarts for this!
to encrypt the file. Thanks to @bodymindarts for this!

### Changed
- `encryptContent` now takes a public key instead of a private key to
Expand Down
2 changes: 2 additions & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ export {
isLaterVersion, isSameOriginAbsoluteUrl, hexStringToECPair, ecPairToHexString
} from './utils'

export { BlockstackWallet, IdentityKeyPair } from './wallet'

export { transactions, safety } from './operations'

export { network } from './network'
Expand Down
321 changes: 321 additions & 0 deletions src/wallet.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,321 @@
/* @flow */
import { HDNode } from 'bitcoinjs-lib'
import { ecPairToHexString } from './utils'
import crypto from 'crypto'

const APPS_NODE_INDEX = 0
const IDENTITY_KEYCHAIN = 888
const BLOCKSTACK_ON_BITCOIN = 0

const BITCOIN_BIP_44_PURPOSE = 44
const BITCOIN_COIN_TYPE = 0
const BITCOIN_ACCOUNT_INDEX = 0

const EXTERNAL_ADDRESS = 'EXTERNAL_ADDRESS'
const CHANGE_ADDRESS = 'CHANGE_ADDRESS'

export type IdentityKeyPair = {
key: string,
keyID: string,
address: string,
appsNodeKey: string,
salt: string
}

function hashCode(string) {
let hash = 0
if (string.length === 0) return hash
for (let i = 0; i < string.length; i++) {
const character = string.charCodeAt(i)
hash = (hash << 5) - hash + character
hash = hash & hash
}
return hash & 0x7fffffff
}

function getNodePrivateKey(hdNode): string {
return ecPairToHexString(hdNode.keyPair)
}

function getNodePublicKey(hdNode): string {
return hdNode.keyPair.getPublicKeyBuffer().toString('hex')
}

export function getFirst62BitsAsNumbers(buff: Buffer): Array<number> {
// now, lets use the leading 62 bits to get two indexes
// start with two ints --> 64 bits
const firstInt32 = buff.readInt32BE(0)
const secondInt32 = buff.readInt32BE(4)
// zero-left shift of one gives us the first 31 bits (as a number < 2^31)
const firstIndex = firstInt32 >>> 1
// save the 32nd bit
const secondIndexLeadingBit = (firstInt32 & 1)
// zero-left shift of two gives us the next 30 bits, then we add
// that 32nd bit to the front.
const secondIndex = (secondInt32 >>> 2) | (secondIndexLeadingBit << 30)
return [firstIndex, secondIndex]
}

/**
* 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.
*/
export class BlockstackWallet {
rootNode: HDNode

constructor(rootNode: HDNode) {
this.rootNode = rootNode
}

/**
* Initialize a blockstack wallet
* @param {Buffer} seed - the input seed for initializing the root node
* of the hierarchical wallet
* @return {BlockstackWallet} the constructed wallet
*/
static fromSeedBuffer(seed: Buffer): BlockstackWallet {
return new BlockstackWallet(HDNode.fromSeedBuffer(seed))
}

/**
* Initialize a blockstack wallet
* @param {string} keychain - the Base58 string used to initialize
* the root node of the hierarchical wallet
* @return {BlockstackWallet} the constructed wallet
*/
static fromBase58(keychain: string): BlockstackWallet {
return new BlockstackWallet(HDNode.fromBase58(keychain))
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Extract the mnemonic decryption stuff from https://github.com/blockstack/blockstack-browser/blob/master/app/js/utils/encryption-utils.js and make an async fromMnemonic(mnemonic: string, password: string) static function as well. Maybe a static encryptMnemonic as well, to keep the full implementation in one place?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I like that idea.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This has been pushed, we now have decryptMnemonic, encryptMnemonic, BlockstackWallet.generateMnemonic, and BlockstackWallet.fromEncryptedMnemonic.

getIdentityPrivateKeychain(): HDNode {
return this.rootNode
.deriveHardened(IDENTITY_KEYCHAIN)
.deriveHardened(BLOCKSTACK_ON_BITCOIN)
}

getBitcoinPrivateKeychain(): HDNode {
return this.rootNode
.deriveHardened(BITCOIN_BIP_44_PURPOSE)
.deriveHardened(BITCOIN_COIN_TYPE)
.deriveHardened(BITCOIN_ACCOUNT_INDEX)
}

getBitcoinNode(addressIndex: number, chainType: string = EXTERNAL_ADDRESS): HDNode {
return BlockstackWallet.getNodeFromBitcoinKeychain(
this.getBitcoinPrivateKeychain().toBase58(),
addressIndex, chainType)
}

getIdentityAddressNode(identityIndex: number): HDNode {
const identityPrivateKeychain = this.getIdentityPrivateKeychain()
return identityPrivateKeychain.deriveHardened(identityIndex)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function doesn't seem to line up with blockstack-browser's implementation, https://github.com/blockstack/blockstack-browser/blob/master/app/js/utils/account-utils.js#L212. Let me know if there's something I'm missing here.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Scratch that, the difference here is that the linked code uses the identity private keychain, where as the wallet class uses the root node.


static getAppsNode(identityNode: HDNode): HDNode {
return identityNode.deriveHardened(APPS_NODE_INDEX)
}

/**
* Get a salt for use with creating application specific addresses
* @return {String} the salt
*/
getIdentitySalt(): string {
const identityPrivateKeychain = this.getIdentityPrivateKeychain()
const publicKeyHex = getNodePublicKey(identityPrivateKeychain)
return crypto.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
*/
getBitcoinAddress(addressIndex: number): string {
return this.getBitcoinNode(addressIndex).getAddress()
}

/**
* 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.
*/
getBitcoinPrivateKey(addressIndex: number): string {
return getNodePrivateKey(this.getBitcoinNode(addressIndex))
}

/**
* Get the root node for the bitcoin public keychain
* @return {String} base58-encoding of the public node
*/
getBitcoinPublicKeychain(): string {
return this.getBitcoinPrivateKeychain().neutered().toBase58()
}

/**
* Get the root node for the identity public keychain
* @return {String} base58-encoding of the public node
*/
getIdentityPublicKeychain(): string {
return this.getIdentityPrivateKeychain().neutered().toBase58()
}

static getNodeFromBitcoinKeychain(keychainBase58: string, addressIndex: number,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be an instance method that just takes index and chainType, and uses this.rootNode.derive

chainType: string = EXTERNAL_ADDRESS): HDNode {
let chain
if (chainType === EXTERNAL_ADDRESS) {
chain = 0
} else if (chainType === CHANGE_ADDRESS) {
chain = 1
} else {
throw new Error('Invalid chain type')
}
const keychain = HDNode.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
*/
static getAddressFromBitcoinKeychain(keychainBase58: string, addressIndex: number,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, should be an instance method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not an instance method so that user's can derive these parts without unlocking the root key.

chainType: string = EXTERNAL_ADDRESS): string {
return BlockstackWallet
.getNodeFromBitcoinKeychain(keychainBase58, addressIndex, chainType)
.getAddress()
}

/**
* 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
*/
static getLegacyAppPrivateKey(appsNodeKey: string, salt: string, appDomain: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, should be instance method

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above -- if this is an instance method, then you need to have the master key in memory to get this key.

const hash = crypto
.createHash('sha256')
.update(`${appDomain}${salt}`)
.digest('hex')
const appIndex = hashCode(hash)
const appNode = HDNode.fromBase58(appsNodeKey).deriveHardened(appIndex)
return getNodePrivateKey(appNode).slice(0, 64)
}


/**
* Get a ECDSA private key hex-string for an application-specific
* address, this address will use the first 62 bits of the SHA256 hash
* of `appDomain,sig("app-node-salt" with appsNodeKey)`
* @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
*/
static getAppPrivateKeySecretSalt(appsNodeKey: string, salt: string, appDomain: string): string {
const appsNode = HDNode.fromBase58(appsNodeKey)

// we will *sign* the input salt, which creates a secret value
const saltHash = crypto.createHash('sha256')
.update(`app-key-salt:${salt}`)
.digest()
const secretValue = appsNode.sign(saltHash).toDER().toString('hex')

const hash = crypto
.createHash('sha256')
.update(`${appDomain},${secretValue}`)
.digest()

const indexes = getFirst62BitsAsNumbers(hash)
const appNode = appsNode.deriveHardened(indexes[0]).deriveHardened(indexes[1])
return getNodePrivateKey(appNode).slice(0, 64)
}


/**
* 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
*/
static getAppPrivateKey(appsNodeKey: string, salt: string, appDomain: string): string {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above, should be instance method

const hash = crypto
.createHash('sha256')
.update(`${appDomain}${salt}`)
.digest('hex')
const 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 (let 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))
}
let appNode = HDNode.fromBase58(appsNodeKey)
appIndexHexes.forEach((hex) => {
if (hex.length > 6) {
throw new Error('Invalid hex string length')
}
appNode = appNode.deriveHardened(parseInt(hex, 16))
})
return getNodePrivateKey(appNode).slice(0, 64)
}

/**
* 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
*/
getIdentityKeyPair(addressIndex: number, alwaysUncompressed: ?boolean = false): IdentityKeyPair {
const identityNode = this.getIdentityAddressNode(addressIndex)

const address = identityNode.getAddress()
let identityKey = getNodePrivateKey(identityNode)
if (alwaysUncompressed && identityKey.length === 66) {
identityKey = identityKey.slice(0, 64)
}

const identityKeyID = getNodePublicKey(identityNode)
const appsNodeKey = BlockstackWallet.getAppsNode(identityNode).toBase58()
const salt = this.getIdentitySalt()
const keyPair = {
key: identityKey,
keyID: identityKeyID,
address, appsNodeKey, salt
}
return keyPair
}
}
5 changes: 4 additions & 1 deletion tests/unitTests/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { runProofsUnitTests } from './unitTestsProofs'
import { runUtilsTests } from './unitTestsUtils'
import { runEncryptionTests } from './unitTestsEncryption'
import { runStorageTests } from './unitTestsStorage'
import { runOperationsTests } from './unitTestsOperations'
import { runOperationsTests } from './unitTestsOperations'
import { runWalletTests } from './unitTestsWallet'

// Utils tests
runUtilsTests()
Expand All @@ -26,3 +27,5 @@ runStorageTests()

// Operations Tests
runOperationsTests()

runWalletTests()
Loading