UNPKG

blockstack

Version:

The Blockstack Javascript library for authentication, identity, and storage.

460 lines (386 loc) 22.5 kB
import * as test from 'tape-promise/tape' import * as triplesec from 'triplesec' import * as elliptic from 'elliptic' import * as webCryptoPolyfill from '@peculiar/webcrypto' import { encryptECIES, decryptECIES, getHexFromBN, signECDSA, verifyECDSA, encryptMnemonic, decryptMnemonic } from '../../../src/encryption' import { ERROR_CODES } from '../../../src/errors' import { getGlobalScope } from '../../../src/utils' import * as pbkdf2 from '../../../src/encryption/pbkdf2' import * as aesCipher from '../../../src/encryption/aesCipher' import * as sha2Hash from '../../../src/encryption/sha2Hash' import * as hmacSha256 from '../../../src/encryption/hmacSha256' import * as ripemd160 from '../../../src/encryption/hashRipemd160' import * as BN from 'bn.js' import { getBufferFromBN } from '../../../src/encryption/ec' export function runEncryptionTests() { const privateKey = 'a5c61c6ca7b3e7e55edee68566aeab22e4da26baa285c7bd10e8d2218aa3b229' const publicKey = '027d28f9951ce46538951e3697c62588a87f1f1f295de4a14fdd4c780fc52cfe69' test('ripemd160 digest tests', async (t) => { const vectors = [ ['The quick brown fox jumps over the lazy dog', '37f332f68db77bd9d7edd4969571ad671cf9dd3b'], ['The quick brown fox jumps over the lazy cog', '132072df690933835eb8b6ad0b77e7b6f14acad7'], ['a', '0bdc9d2d256b3ee9daae347be6f4dc835a467ffe'], ['abc', '8eb208f7e05d987a9b044a8e98c6b087f15a0bfc'], ['message digest', '5d0689ef49d2fae572b881b123a85ffa21595f36'], ['', '9c1185a5c5e9fc54612808977ee8f548b2258d31'] ] const nodeCryptoHasher = await ripemd160.createHashRipemd160() t.equal(nodeCryptoHasher instanceof ripemd160.NodeCryptoRipemd160Digest, true, 'Node crypto should be detected for ripemd160 hash') for (const [input, expected] of vectors) { const result = await nodeCryptoHasher.digest(Buffer.from(input)) const resultHex = result.toString('hex') t.equal(resultHex, expected) } const polyfillHasher = new ripemd160.Ripemd160PolyfillDigest() for (const [input, expected] of vectors) { const result = await polyfillHasher.digest(Buffer.from(input)) const resultHex = result.toString('hex') t.equal(resultHex, expected) } const nodeCrypto = require('crypto') const createHashOrig = nodeCrypto.createHash nodeCrypto.createHash = () => { throw new Error('Artificial broken hash') } try { await ripemd160.hashRipemd160(Buffer.from('acb')) } finally { nodeCrypto.createHash = createHashOrig } }) test('sha2 digest tests', async (t) => { const globalScope = getGlobalScope() as any // Remove any existing global `crypto` variable for testing const globalCryptoOrig = { defined: 'crypto' in globalScope, value: globalScope.crypto } // Set global web `crypto` polyfill for testing const webCrypto = new webCryptoPolyfill.Crypto() globalScope.crypto = webCrypto const vectors = [ ['abc', 'ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad', 'ddaf35a193617abacc417349ae20413112e6fa4e89a97ea20a9eeee64b55d39a2192992a274fc1a836ba3c23a3feebbd454d4423643ce80e2a9ac94fa54ca49f'], ['', 'e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', 'cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e'], ['abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq', '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1', '204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445'] ] try { const webCryptoHasher = await sha2Hash.createSha2Hash() t.equal(webCryptoHasher instanceof sha2Hash.WebCryptoSha2Hash, true, 'Web crypto should be detected for sha2 hash') for (const [input, expected256, expected512] of vectors) { const result256 = await webCryptoHasher.digest(Buffer.from(input), 'sha256') t.equal(result256.toString('hex'), expected256) const result512 = await webCryptoHasher.digest(Buffer.from(input), 'sha512') t.equal(result512.toString('hex'), expected512) } const nodeCryptoHasher = new sha2Hash.NodeCryptoSha2Hash(require('crypto').createHash) for (const [input, expected256, expected512] of vectors) { const result256 = await nodeCryptoHasher.digest(Buffer.from(input), 'sha256') t.equal(result256.toString('hex'), expected256) const result512 = await nodeCryptoHasher.digest(Buffer.from(input), 'sha512') t.equal(result512.toString('hex'), expected512) } } finally { // Restore previous `crypto` global var if (globalCryptoOrig.defined) { globalScope.crypto = globalCryptoOrig.value } else { delete globalScope.crypto } } }) test('sha2 native digest fallback tests', async (t) => { const input = Buffer.from('abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq') const expectedOutput256 = '248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1' const expectedOutput512 = '204a8fc6dda82f0a0ced7beb8e08a41657c16ef468b228a8279be331a703c33596fd15c13b1b07f9aa1d3bea57789ca031ad85c7a71dd70354ec631238ca3445' // Test WebCrypto fallback const webCryptoSubtle = new webCryptoPolyfill.Crypto().subtle webCryptoSubtle.digest = () => { throw new Error('Artificial broken hash') } const nodeCryptoHasher = new sha2Hash.WebCryptoSha2Hash(webCryptoSubtle) const result256 = await nodeCryptoHasher.digest(input, 'sha256') t.equal(result256.toString('hex'), expectedOutput256) const result512 = await nodeCryptoHasher.digest(input, 'sha512') t.equal(result512.toString('hex'), expectedOutput512) // Test Node.js `crypto.createHash` fallback const nodeCrypto = require('crypto') const createHashOrig = nodeCrypto.createHash nodeCrypto.createHash = () => { throw new Error('Artificial broken hash') } try { const nodeCryptoHasher = new sha2Hash.NodeCryptoSha2Hash(require('crypto').createHash) const result256 = await nodeCryptoHasher.digest(input, 'sha256') t.equal(result256.toString('hex'), expectedOutput256) const result512 = await nodeCryptoHasher.digest(input, 'sha512') t.equal(result512.toString('hex'), expectedOutput512) } finally { nodeCrypto.createHash = createHashOrig } }) test('hmac-sha256 tests', async (t) => { const key = Buffer.alloc(32, 0xf5) const data = Buffer.alloc(100, 0x44) const expected = 'fe44c2197eb8a5678daba87ff2aba891d8b12224d8219acd4cfa5cee4f9acc77' const globalScope = getGlobalScope() as any // Remove any existing global `crypto` variable for testing const globalCryptoOrig = { defined: 'crypto' in globalScope, value: globalScope.crypto } delete globalScope.crypto try { const nodeCryptoHmac = await hmacSha256.createHmacSha256() t.assert(nodeCryptoHmac instanceof hmacSha256.NodeCryptoHmacSha256, 'should be type NodeCryptoHmacSha256 when global web crypto undefined') // Set global web `crypto` polyfill for testing const webCrypto = new webCryptoPolyfill.Crypto() globalScope.crypto = webCrypto const webCryptoHmac = await hmacSha256.createHmacSha256() t.assert(webCryptoHmac instanceof hmacSha256.WebCryptoHmacSha256, 'should be type WebCryptoHmacSha256 when global web crypto is available') const derivedNodeCrypto = (await nodeCryptoHmac.digest(key, data)).toString('hex') const derivedWebCrypto = (await webCryptoHmac.digest(key, data)).toString('hex') t.equal(expected, derivedNodeCrypto, 'NodeCryptoHmacSha256 should have digested to expected key') t.equal(expected, derivedWebCrypto, 'WebCryptoHmacSha256 should have digested to expected key') } finally { // Restore previous `crypto` global var if (globalCryptoOrig.defined) { globalScope.crypto = globalCryptoOrig.value } else { delete globalScope.crypto } } }) test('pbkdf2 digest tests', async (t) => { const salt = Buffer.alloc(16, 0xf0) const password = 'password123456' const digestAlgo = 'sha512' const iterations = 100000 const keyLength = 48 const globalScope = getGlobalScope() as any // Remove any existing global `crypto` variable for testing const globalCryptoOrig = { defined: 'crypto' in globalScope, value: globalScope.crypto } delete globalScope.crypto try { const nodeCryptoPbkdf2 = await pbkdf2.createPbkdf2() t.assert(nodeCryptoPbkdf2 instanceof pbkdf2.NodeCryptoPbkdf2, 'should be type NodeCryptoPbkdf2 when global web crypto undefined') // Set global web `crypto` polyfill for testing const webCrypto = new webCryptoPolyfill.Crypto() globalScope.crypto = webCrypto const webCryptoPbkdf2 = await pbkdf2.createPbkdf2() t.assert(webCryptoPbkdf2 instanceof pbkdf2.WebCryptoPbkdf2, 'should be type WebCryptoPbkdf2 when global web crypto is available') const polyFillPbkdf2 = new pbkdf2.WebCryptoPartialPbkdf2(webCrypto.subtle) const derivedNodeCrypto = (await nodeCryptoPbkdf2 .derive(password, salt, iterations, keyLength, digestAlgo)).toString('hex') const derivedWebCrypto = (await webCryptoPbkdf2 .derive(password, salt, iterations, keyLength, digestAlgo)).toString('hex') const derivedPolyFill = (await polyFillPbkdf2 .derive(password, salt, iterations, keyLength, digestAlgo)).toString('hex') const expected = '92f603459cc45a33eeb6ee06bb75d12bb8e61d9f679668392362bb104eab6d95027398e02f500c849a3dd1ccd63fb310' t.equal(expected, derivedNodeCrypto, 'NodeCryptoPbkdf2 should have derived expected key') t.equal(expected, derivedWebCrypto, 'WebCryptoPbkdf2 should have derived expected key') t.equal(expected, derivedPolyFill, 'PolyfillLibPbkdf2 should have derived expected key') } finally { // Restore previous `crypto` global var if (globalCryptoOrig.defined) { globalScope.crypto = globalCryptoOrig.value } else { delete globalScope.crypto } } }) test('aes-cbc tests', async (t) => { const globalScope = getGlobalScope() as any // Remove any existing global `crypto` variable for testing const globalCryptoOrig = { defined: 'crypto' in globalScope, value: globalScope.crypto } delete globalScope.crypto try { const nodeCryptoAesCipher = await aesCipher.createCipher() t.assert(nodeCryptoAesCipher instanceof aesCipher.NodeCryptoAesCipher, 'should be type NodeCryptoAesCipher when global web crypto undefined') // Set global web `crypto` polyfill for testing const webCrypto = new webCryptoPolyfill.Crypto() globalScope.crypto = webCrypto const webCryptoAesCipher = await aesCipher.createCipher() t.assert(webCryptoAesCipher instanceof aesCipher.WebCryptoAesCipher, 'should be type WebCryptoAesCipher when global web crypto is available') const key128 = Buffer.from('0f'.repeat(16), 'hex') const key256 = Buffer.from('0f'.repeat(32), 'hex') const iv = Buffer.from('f7'.repeat(16), 'hex') const inputData = Buffer.from('TestData'.repeat(20)) const inputDataHex = inputData.toString('hex') const expected128Cbc = '5aa1100a0a3133c9184dc661bc95c675a0fe5f02a67880f50702f8c88e7a445248d6dedfca80e72d00c3d277ea025eebde5940265fa00c1bfe80aebf3968b6eaf0564eda6ddd9e97548be1fa6d487e71353b11136193782d76d3b8d1895047e08a121c1706c083ceefdb9605a75a2310cccee1b0aaca632230f45f1172001cad96ae6d15db38ab9eed27b27b6f80353a5f30e3532a526a834a0f8273ffb2e9caaa92843b40c893e298f3b472fb26b11f' const expected128CbcBuffer = Buffer.from(expected128Cbc, 'hex') const expected256Cbc = '66a21fa53680d8182a79c1b90cdc38d398fe34d85c7ca5d45b8381fea4a84536e38514b3bcdba06655314607534be7ea370952ed6f334af709efc6504e600ce0b7c20fe3b469c29b63a391983b74aa12f1d859b477092c61e7814bd6c8d143ec21d34f79468c74c97ae9763ec11695e1e9a3a3b33f12561ecef9fbae79ddf7f2701c97ba1531801862662a9ce87a880934318a9e46a3941367fa68da3340f83941211aba7ec741826ff35d4f880243db' const expected256CbcBuffer = Buffer.from(expected256Cbc, 'hex') // Test aes-256-cbc encrypt const encrypted256NodeCrypto = (await nodeCryptoAesCipher .encrypt('aes-256-cbc', key256, iv, inputData)).toString('hex') const encrypted256WebCrypto = (await webCryptoAesCipher .encrypt('aes-256-cbc', key256, iv, inputData)).toString('hex') t.equal(expected256Cbc, encrypted256NodeCrypto, 'NodeCryptoAesCipher aes-256-cbc should have encrypted to expected ciphertext') t.equal(expected256Cbc, encrypted256WebCrypto, 'WebCryptoAesCipher aes-256-cbc should have encrypted to expected ciphertext') // Test aes-256-cbc decrypt const decrypted256NodeCrypto = (await nodeCryptoAesCipher .decrypt('aes-256-cbc', key256, iv, expected256CbcBuffer)).toString('hex') const decrypted256WebCrypto = (await webCryptoAesCipher .decrypt('aes-256-cbc', key256, iv, expected256CbcBuffer)).toString('hex') t.equal(inputDataHex, decrypted256NodeCrypto, 'NodeCryptoAesCipher aes-256-cbc should have decrypted to expected ciphertext') t.equal(inputDataHex, decrypted256WebCrypto, 'WebCryptoAesCipher aes-256-cbc should have decrypted to expected ciphertext') // Test aes-128-cbc encrypt const encrypted128NodeCrypto = (await nodeCryptoAesCipher .encrypt('aes-128-cbc', key128, iv, inputData)).toString('hex') const encrypted128WebCrypto = (await webCryptoAesCipher .encrypt('aes-128-cbc', key128, iv, inputData)).toString('hex') t.equal(expected128Cbc, encrypted128NodeCrypto, 'NodeCryptoAesCipher aes-128-cbc should have encrypted to expected ciphertext') t.equal(expected128Cbc, encrypted128WebCrypto, 'WebCryptoAesCipher aes-128-cbc should have encrypted to expected ciphertext') // Test aes-128-cbc decrypt const decrypted128NodeCrypto = (await nodeCryptoAesCipher .decrypt('aes-128-cbc', key128, iv, expected128CbcBuffer)).toString('hex') const decrypted128WebCrypto = (await webCryptoAesCipher .decrypt('aes-128-cbc', key128, iv, expected128CbcBuffer)).toString('hex') t.equal(inputDataHex, decrypted128NodeCrypto, 'NodeCryptoAesCipher aes-128-cbc should have decrypted to expected ciphertext') t.equal(inputDataHex, decrypted128WebCrypto, 'WebCryptoAesCipher aes-128-cbc should have decrypted to expected ciphertext') } finally { // Restore previous `crypto` global var if (globalCryptoOrig.defined) { globalScope.crypto = globalCryptoOrig.value } else { delete globalScope.crypto } } }) test('encrypt-to-decrypt works', async (t) => { t.plan(2) const testString = 'all work and no play makes jack a dull boy' let cipherObj = await encryptECIES(publicKey, Buffer.from(testString), true) let deciphered = await decryptECIES(privateKey, cipherObj) t.equal(deciphered, testString, 'Decrypted ciphertext does not match expected plaintext') const testBuffer = Buffer.from(testString) cipherObj = await encryptECIES(publicKey, testBuffer, false) deciphered = await decryptECIES(privateKey, cipherObj) t.equal(deciphered.toString('hex'), testBuffer.toString('hex'), 'Decrypted cipherbuffer does not match expected plainbuffer') }) test('encrypt-to-decrypt fails on bad mac', async (t) => { t.plan(3) const testString = 'all work and no play makes jack a dull boy' const cipherObj = await encryptECIES(publicKey, Buffer.from(testString), true) const evilString = 'some work and some play makes jack a dull boy' const evilObj = await encryptECIES(publicKey, Buffer.from(evilString), true) cipherObj.cipherText = evilObj.cipherText try { await decryptECIES(privateKey, cipherObj) t.true(false, 'Decryption should have failed when ciphertext modified') } catch (e) { t.true(true, 'Decryption correctly fails when ciphertext modified') t.equal(e.code, ERROR_CODES.FAILED_DECRYPTION_ERROR, 'Must have proper error code') const assertionMessage = 'Should indicate MAC error' t.notEqual(e.message.indexOf('failure in MAC check'), -1, assertionMessage) } }) test('sign-to-verify-works', async (t) => { t.plan(2) const testString = 'all work and no play makes jack a dull boy' let sigObj = await signECDSA(privateKey, testString) t.true(await verifyECDSA(testString, sigObj.publicKey, sigObj.signature), 'String content should be verified') const testBuffer = Buffer.from(testString) sigObj = await signECDSA(privateKey, testBuffer) t.true(await verifyECDSA(testBuffer, sigObj.publicKey, sigObj.signature), 'String buffer should be verified') }) test('sign-to-verify-fails', async (t) => { t.plan(3) const testString = 'all work and no play makes jack a dull boy' const failString = 'I should fail' let sigObj = await signECDSA(privateKey, testString) t.false(await verifyECDSA(failString, sigObj.publicKey, sigObj.signature), 'String content should not be verified') const testBuffer = Buffer.from(testString) sigObj = await signECDSA(privateKey, testBuffer) t.false(await verifyECDSA(Buffer.from(failString), sigObj.publicKey, sigObj.signature), 'Buffer content should not be verified') const badPK = '0288580b020800f421d746f738b221d384f098e911b81939d8c94df89e74cba776' sigObj = await signECDSA(privateKey, testBuffer) t.false(await verifyECDSA(Buffer.from(failString), badPK, sigObj.signature), 'Buffer content should not be verified') }) test('bn-padded-to-64-bytes', (t) => { const ecurve = new elliptic.ec('secp256k1') const evilHexes = ['ba40f85b152bea8c3812da187bcfcfb0dc6e15f9e27cb073633b1c787b19472f', 'e346010f923f768138152d0bad063999ff1da5361a81e6e6f9106241692a0076'] const results = evilHexes.map((hex) => { const ephemeralSK = ecurve.keyFromPrivate(hex) const ephemeralPK = ephemeralSK.getPublic() const sharedSecret = ephemeralSK.derive(ephemeralPK) return getHexFromBN(sharedSecret).length === 64 }) t.true(results.every(x => x), 'Evil hexes must all generate 64-len hex strings') const bnBuffer = getBufferFromBN(new BN(123)) t.equal(bnBuffer.byteLength, 32, 'getBufferFromBN should pad to 32 bytes') t.equal(bnBuffer.toString('hex'), getHexFromBN(new BN(123)), 'getBufferFromBN and getHexFromBN should match') t.end() }) test('encryptMnemonic & decryptMnemonic', async (t) => { const rawPhrase = 'march eager husband pilot waste rely exclude taste ' + 'twist donkey actress scene' const rawPassword = 'testtest' const encryptedPhrase = 'ffffffffffffffffffffffffffffffffca638cc39fc270e8be5c' + 'bf98347e42a52ee955e287ab589c571af5f7c80269295b0039e32ae13adf11bc6506f5ec' + '32dda2f79df4c44276359c6bac178ae393de' const preEncryptedPhrase = '7573f4f51089ba7ce2b95542552b7504de7305398637733' + '0579649dfbc9e664073ba614fac180d3dc237b21eba57f9aee5702ba819fe17a0752c4dc7' + '94884c9e75eb60da875f778bbc1aaca1bd373ea3' const legacyPhrase = 'vivid oxygen neutral wheat find thumb cigar wheel ' + 'board kiwi portion business' const legacyPassword = 'supersecret' const legacyEncrypted = '1c94d7de0000000304d583f007c71e6e5fef354c046e8c64b1' + 'adebd6904dcb007a1222f07313643873455ab2a3ab3819e99d518cc7d33c18bde02494aa' + '74efc35a8970b2007b2fc715f6067cee27f5c92d020b1806b0444994aab80050a6732131' + 'd2947a51bacb3952fb9286124b3c2b3196ff7edce66dee0dbd9eb59558e0044bddb3a78f' + '48a66cf8d78bb46bb472bd2d5ec420c831fc384293252459524ee2d668869f33c586a944' + '67d0ce8671260f4cc2e87140c873b6ca79fb86c6d77d134d7beb2018845a9e71e6c7ecde' + 'dacd8a676f1f873c5f9c708cc6070642d44d2505aa9cdba26c50ad6f8d3e547fb0cba710' + 'a7f7be54ff7ea7e98a809ddee5ef85f6f259b3a17a8d8dbaac618b80fe266a1e63ec19e4' + '76bee9177b51894ee' // Test encryption -> decryption. Can't be done with hard-coded values // due to random salt. await encryptMnemonic(rawPhrase, rawPassword) .then(encoded => decryptMnemonic(encoded.toString('hex'), rawPassword, triplesec.decrypt), (err) => { t.fail(`Should encrypt mnemonic phrase, instead errored: ${err}`) }) .then((decoded: string) => { t.true(decoded.toString() === rawPhrase, 'Should encrypt & decrypt a phrase correctly') }, (err) => { t.fail(`Should decrypt encrypted phrase, instead errored: ${err}`) }) // Test encryption with mocked randomBytes generator to use same salt try { const mockSalt = Buffer.from('ff'.repeat(16), 'hex') const encoded = await encryptMnemonic(rawPhrase, rawPassword, {getRandomBytes: () => mockSalt}) t.strictEqual(encoded.toString('hex'), encryptedPhrase) } catch (err) { t.fail(`Should have encrypted phrase with deterministic salt, instead errored: ${err}`) } // Test decryption with mocked randomBytes generator to use same salt try { const decoded = await decryptMnemonic(Buffer.from(encryptedPhrase, 'hex'), rawPassword, triplesec.decrypt) t.strictEqual(decoded, rawPhrase, 'Should encrypt & decrypt a phrase correctly') } catch (err) { t.fail(`Should have decrypted phrase with deterministic salt, instead errored: ${err}`) } // Test valid input (No salt, so it's the same every time) await decryptMnemonic(legacyEncrypted, legacyPassword, triplesec.decrypt).then((decoded) => { t.strictEqual(decoded, legacyPhrase, 'Should decrypt legacy encrypted phrase') }, (err) => { t.fail(`Should decrypt legacy encrypted phrase, instead errored: ${err}`) }) // Invalid inputs await encryptMnemonic('not a mnemonic phrase', 'password').then(() => { t.fail('Should have thrown on invalid mnemonic input') }, () => { t.pass('Should throw on invalid mnemonic input') }) await decryptMnemonic(preEncryptedPhrase, 'incorrect password', triplesec.decrypt).then(() => { t.fail('Should have thrown on incorrect password for decryption') }, () => { t.pass('Should throw on incorrect password') }) }) }