Skip to content

Commit

Permalink
feat: use webcrypto/aes-cbc-256, remove blockstack.js dep, closes #176
Browse files Browse the repository at this point in the history
  • Loading branch information
kyranjamie committed Sep 25, 2020
1 parent 61042de commit 68ae719
Show file tree
Hide file tree
Showing 8 changed files with 133 additions and 70 deletions.
52 changes: 52 additions & 0 deletions app/crypto/key-encryption.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { Buffer } from 'buffer';

const algorithmName = 'AES-CBC';

function extractEncryptionKey(hash: Uint8Array) {
return hash.slice(0, 32);
}

function extractEncryptionInitVector(hash: Uint8Array) {
return hash.slice(32, hash.length);
}

async function deriveWebCryptoKey(derivedKeyHash: Uint8Array) {
const format = 'raw';
const key = extractEncryptionKey(derivedKeyHash);
const algorithm = { name: algorithmName };
const extractable = false;
const keyUsages: KeyUsage[] = ['encrypt', 'decrypt'];
return crypto.subtle.importKey(format, key, algorithm, extractable, keyUsages);
}

interface EncryptMnemonicArgs {
mnemonic: string;
derivedKeyHash: Uint8Array;
}

export async function encryptMnemonic({ mnemonic, derivedKeyHash }: EncryptMnemonicArgs) {
const key = await deriveWebCryptoKey(derivedKeyHash);
const iv = extractEncryptionInitVector(derivedKeyHash);
const cipherArrayBuffer = await crypto.subtle.encrypt(
{ name: algorithmName, iv },
key,
new TextEncoder().encode(mnemonic)
);
return Buffer.from(cipherArrayBuffer).toString('hex');
}

interface DecryptMnemonicArgs {
encryptedMnemonic: string;
derivedKeyHash: Uint8Array;
}

export async function decryptMnemonic({ encryptedMnemonic, derivedKeyHash }: DecryptMnemonicArgs) {
if (derivedKeyHash.length !== 48) throw new Error('Key must be of length 48');
const key = await deriveWebCryptoKey(derivedKeyHash);
const iv = extractEncryptionInitVector(derivedKeyHash);
const algorithm = { name: 'AES-CBC', iv };
const encryptedBuffer = new Buffer(encryptedMnemonic, 'hex');
const decrypted = await crypto.subtle.decrypt(algorithm, key, encryptedBuffer);
const textDecoder = new TextDecoder();
return textDecoder.decode(decrypted);
}
60 changes: 54 additions & 6 deletions app/crypto/key-generation.spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { generateSalt, generateDerivedKey } from './key-generation';
import { generateSalt, deriveKey } from './key-generation';

import crypto from 'crypto';
// https://stackoverflow.com/a/52612372/1141891
Expand All @@ -8,14 +8,62 @@ Object.defineProperty(global, 'crypto', {
},
});

describe(generateDerivedKey.name, () => {
describe(deriveKey.name, () => {
test('a argon2id hash is returned', async () => {
const salt = '$2a$12$BwnByfKrfRbpxsazN712T.';
const pass = 'f255cadb0af84854819c63f26c53e1a9';
const result = await generateDerivedKey({ salt, pass });
expect(result).toEqual(
'5d46ddfd7273e1a74ba1db937693bfd59de4881d58b86ed4002ee24abf156a77cf12885ee0e50de19af8c67e0115eb0a82576b11864226a6c157aac8a500e9f8'
);
const { derivedKeyHash } = await deriveKey({ salt, pass });
const expectedResultArray = [
94,
0,
166,
167,
20,
189,
146,
233,
48,
163,
248,
178,
48,
11,
140,
87,
82,
126,
73,
82,
237,
166,
232,
173,
90,
192,
67,
200,
149,
147,
30,
223,
60,
15,
133,
99,
89,
142,
223,
116,
131,
24,
169,
157,
157,
245,
159,
140,
];
expect(derivedKeyHash).toEqual(Uint8Array.from(expectedResultArray));
});
});

Expand Down
13 changes: 7 additions & 6 deletions app/crypto/key-generation.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { memoizeWith, identity } from 'ramda';
import argon2 from 'argon2-browser';
import { hash, ArgonType } from 'argon2-browser';

export async function generateDerivedKey({ pass, salt }: { pass: string; salt: string }) {
const { hashHex } = await argon2.hash({
export async function deriveKey({ pass, salt }: { pass: string; salt: string }) {
const result = await hash({
pass,
salt,
hashLen: 64,
type: argon2.ArgonType.Argon2id,
hashLen: 48,
time: 400,
type: ArgonType.Argon2id,
});
return hashHex;
return { derivedKeyHash: result.hash };
}

export function generateRandomHexString() {
Expand Down
2 changes: 1 addition & 1 deletion app/crypto/validate-password.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import zxcvbn, { ZXCVBNResult } from 'zxcvbn';
import { validatePassword, blankPasswordValidation } from './validate-password';
import { validatePassword } from './validate-password';

jest.mock('zxcvbn', () => jest.fn(() => ({ score: 4 })));

Expand Down
3 changes: 1 addition & 2 deletions app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@
},
"license": "MIT",
"dependencies": {
"@blockstack/keychain": "0.8.5",
"blockstack": "21.1.0"
"@blockstack/keychain": "0.8.5"
}
}
2 changes: 1 addition & 1 deletion app/pages/onboarding/08-set-password/set-password.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export const SetPassword: React.FC = () => {
{weakPasswordWarningMessage(strengthResult)}
</Text>
)}
<OnboardingButton type="submit" mt="loose" isDisabled={btnDisabled}>
<OnboardingButton type="submit" mt="loose" isLoading={btnDisabled} isDisabled={btnDisabled}>
Continue
</OnboardingButton>
</Onboarding>
Expand Down
23 changes: 7 additions & 16 deletions app/store/keys/keys.actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,17 @@ import { useHistory } from 'react-router';
import { push } from 'connected-react-router';
import { createAction, Dispatch } from '@reduxjs/toolkit';
import log from 'electron-log';
import {
generateMnemonicRootKeychain,
deriveRootKeychainFromMnemonic,
deriveStxAddressChain,
} from '@blockstack/keychain';
import { encryptMnemonic, decryptMnemonic } from 'blockstack';
import { ChainID } from '@blockstack/stacks-transactions';
import { generateMnemonicRootKeychain, deriveRootKeychainFromMnemonic } from '@blockstack/keychain';

import { RootState } from '..';
import routes from '../../constants/routes.json';
import { MNEMONIC_ENTROPY } from '../../constants';
import { persistSalt, persistEncryptedMnemonic } from '../../utils/disk-store';
import { safeAwait } from '../../utils/safe-await';
import { selectMnemonic, selectKeysSlice } from './keys.reducer';
import { generateSalt, generateDerivedKey } from '../../crypto/key-generation';
import { generateSalt, deriveKey } from '../../crypto/key-generation';
import { deriveStxAddressKeychain } from '../../crypto/derive-address-keychain';
import { encryptMnemonic, decryptMnemonic } from '../../crypto/key-encryption';

type History = ReturnType<typeof useHistory>;

Expand All @@ -44,15 +39,14 @@ export function setPassword({ password, history }: { password: string; history:
return async (dispatch: Dispatch, getState: () => RootState) => {
const mnemonic = selectMnemonic(getState());
const salt = generateSalt();
const derivedEncryptionKey = await generateDerivedKey({ pass: password, salt });
const { derivedKeyHash } = await deriveKey({ pass: password, salt });

if (!mnemonic) {
log.error('Cannot derive encryption key unless a mnemonic has been generated');
return;
}

const encryptedMnemonicBuffer = await encryptMnemonic(mnemonic, derivedEncryptionKey);
const encryptedMnemonic = encryptedMnemonicBuffer.toString('hex');
const encryptedMnemonic = await encryptMnemonic({ derivedKeyHash, mnemonic });
const rootNode = await deriveRootKeychainFromMnemonic(mnemonic);
const { address } = deriveStxAddressKeychain(rootNode);
persistSalt(salt);
Expand Down Expand Up @@ -82,13 +76,10 @@ export function decryptWallet({ password, history }: { password: string; history
return;
}

const key = await generateDerivedKey({ pass: password, salt });
const { derivedKeyHash } = await deriveKey({ pass: password, salt });

//
// TODO: remove casting within blockstack.js library
// https://github.com/blockstack/blockstack.js/pull/797
const [error, mnemonic] = await safeAwait(
decryptMnemonic(encryptedMnemonic, key, undefined as any)
decryptMnemonic({ encryptedMnemonic, derivedKeyHash })
);

if (error) {
Expand Down
48 changes: 10 additions & 38 deletions app/yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -296,34 +296,6 @@ blockstack@21.0.0-alpha.2:
uuid "^3.3.3"
zone-file "^1.0.0"

blockstack@21.1.0:
version "21.1.0"
resolved "https://registry.yarnpkg.com/blockstack/-/blockstack-21.1.0.tgz#4c0b2678647f7c697efe98f50e24482a1dc0cf09"
integrity sha512-K3n161dRhDqBPzSe1gbg0+O7Xd5u00p6Ort2O+DAPMqs3aIy6XI/NhJgs7pestetL/iaGhaJ9EdDUHP4nU4zVQ==
dependencies:
"@types/bn.js" "^4.11.6"
"@types/cheerio" "^0.22.13"
"@types/elliptic" "^6.4.10"
"@types/node" "^12.7.12"
"@types/randombytes" "^2.0.0"
ajv "^4.11.5"
bip39 "^3.0.2"
bitcoinjs-lib "^5.1.6"
bn.js "^4.11.8"
cross-fetch "^3.0.4"
elliptic "^6.5.1"
form-data "^2.5.1"
jsontokens "3.0.0"
query-string "^6.8.3"
randombytes "^2.1.0"
request "^2.88.0"
ripemd160-min "0.0.5"
schema-inspector "^1.6.8"
sha.js "^2.4.11"
tslib "^1.10.0"
uuid "^3.3.3"
zone-file "^1.0.0"

bn.js@^4.0.0, bn.js@^4.11.8, bn.js@^4.4.0:
version "4.11.9"
resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.9.tgz#26d556829458f9d1e81fc48952493d0ba3507828"
Expand Down Expand Up @@ -647,29 +619,29 @@ jsonify@~0.0.0:
resolved "https://registry.yarnpkg.com/jsonify/-/jsonify-0.0.0.tgz#2c74b6ee41d93ca51b7b5aaee8f503631d252a73"
integrity sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=

jsontokens@3.0.0, jsontokens@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-3.0.0.tgz#629984d260a4081b11541313acdba708377314d3"
integrity sha512-P0QZC5AjOkn3t1ej6OuI7+XqoEctYj83UK4pw0WpHY4/z6a5PpZCJSpp5NZodq94GFkw2PfB9DPFoDM5qpyp/g==
jsontokens@3.0.0-alpha.0:
version "3.0.0-alpha.0"
resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-3.0.0-alpha.0.tgz#16d04a2019a6dbe2392e4eeb489ac47ec3d855d7"
integrity sha512-+2JdFr2d3XBfamUnETQdv76u3AwBl8jmLp2Hgb/uK5NTys0FpoR5KbIIqv3QrXtjn0uIIPJz9JmhqDAnXd2Nhg==
dependencies:
"@types/elliptic" "^6.4.9"
asn1.js "^5.0.1"
base64url "^3.0.1"
ecdsa-sig-formatter "^1.0.11"
elliptic "^6.4.1"
sha.js "^2.4.11"
key-encoder "^2.0.3"

jsontokens@3.0.0-alpha.0:
version "3.0.0-alpha.0"
resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-3.0.0-alpha.0.tgz#16d04a2019a6dbe2392e4eeb489ac47ec3d855d7"
integrity sha512-+2JdFr2d3XBfamUnETQdv76u3AwBl8jmLp2Hgb/uK5NTys0FpoR5KbIIqv3QrXtjn0uIIPJz9JmhqDAnXd2Nhg==
jsontokens@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/jsontokens/-/jsontokens-3.0.0.tgz#629984d260a4081b11541313acdba708377314d3"
integrity sha512-P0QZC5AjOkn3t1ej6OuI7+XqoEctYj83UK4pw0WpHY4/z6a5PpZCJSpp5NZodq94GFkw2PfB9DPFoDM5qpyp/g==
dependencies:
"@types/elliptic" "^6.4.9"
asn1.js "^5.0.1"
base64url "^3.0.1"
ecdsa-sig-formatter "^1.0.11"
elliptic "^6.4.1"
key-encoder "^2.0.3"
sha.js "^2.4.11"

jsprim@^1.2.2:
version "1.4.1"
Expand Down

0 comments on commit 68ae719

Please sign in to comment.