UNPKG

blockstack

Version:

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

183 lines (165 loc) 5.28 kB
import { validateMnemonic, mnemonicToEntropy, entropyToMnemonic } from 'bip39'; import { randomBytes, GetRandomBytes } from './cryptoRandom'; import { createSha2Hash } from './sha2Hash'; import { createHmacSha256 } from './hmacSha256'; import { createCipher } from './aesCipher'; import { createPbkdf2 } from './pbkdf2'; import { TriplesecDecryptSignature } from './cryptoUtils'; /** * Encrypt a raw mnemonic phrase to be password protected * @param {string} phrase - Raw mnemonic phrase * @param {string} password - Password to encrypt mnemonic with * @return {Promise<Buffer>} The encrypted phrase * @private * @ignore * */ export async function encryptMnemonic( phrase: string, password: string, opts?: { getRandomBytes?: GetRandomBytes; } ): Promise<Buffer> { // hex encoded mnemonic string let mnemonicEntropy: string; try { // must be bip39 mnemonic mnemonicEntropy = mnemonicToEntropy(phrase); } catch (error) { console.error('Invalid mnemonic phrase provided'); console.error(error); throw new Error('Not a valid bip39 mnemonic'); } // normalize plaintext to fixed length byte string const plaintextNormalized = Buffer.from(mnemonicEntropy, 'hex'); // AES-128-CBC with SHA256 HMAC const pbkdf2 = await createPbkdf2(); let salt: Buffer; if (opts && opts.getRandomBytes) { salt = opts.getRandomBytes(16); } else { salt = randomBytes(16); } const keysAndIV = await pbkdf2.derive(password, salt, 100000, 48, 'sha512'); const encKey = keysAndIV.slice(0, 16); const macKey = keysAndIV.slice(16, 32); const iv = keysAndIV.slice(32, 48); const cipher = await createCipher(); const cipherText = await cipher.encrypt( 'aes-128-cbc', encKey, iv, plaintextNormalized ); const hmacPayload = Buffer.concat([salt, cipherText]); const hmacSha256 = await createHmacSha256(); const hmacDigest = await hmacSha256.digest(macKey, hmacPayload); const payload = Buffer.concat([salt, hmacDigest, cipherText]); return payload; } // Used to distinguish bad password during decrypt vs invalid format class PasswordError extends Error {} /** * @ignore */ async function decryptMnemonicBuffer( dataBuffer: Buffer, password: string ): Promise<string> { const salt = dataBuffer.slice(0, 16); const hmacSig = dataBuffer.slice(16, 48); // 32 bytes const cipherText = dataBuffer.slice(48); const hmacPayload = Buffer.concat([salt, cipherText]); const pbkdf2 = await createPbkdf2(); const keysAndIV = await pbkdf2.derive(password, salt, 100000, 48, 'sha512'); const encKey = keysAndIV.slice(0, 16); const macKey = keysAndIV.slice(16, 32); const iv = keysAndIV.slice(32, 48); const decipher = await createCipher(); const decryptedResult = await decipher.decrypt( 'aes-128-cbc', encKey, iv, cipherText ); const hmacSha256 = await createHmacSha256(); const hmacDigest = await hmacSha256.digest(macKey, hmacPayload); // hash both hmacSig and hmacDigest so string comparison time // is uncorrelated to the ciphertext const sha2Hash = await createSha2Hash(); const hmacSigHash = await sha2Hash.digest(hmacSig); const hmacDigestHash = await sha2Hash.digest(hmacDigest); if (!hmacSigHash.equals(hmacDigestHash)) { // not authentic throw new PasswordError('Wrong password (HMAC mismatch)'); } let mnemonic: string; try { mnemonic = entropyToMnemonic(decryptedResult); } catch (error) { console.error('Error thrown by `entropyToMnemonic`'); console.error(error); throw new PasswordError('Wrong password (invalid plaintext)'); } if (!validateMnemonic(mnemonic)) { throw new PasswordError('Wrong password (invalid plaintext)'); } return mnemonic; } /** * Decrypt legacy triplesec keys * @param {Buffer} dataBuffer - The encrypted key * @param {String} password - Password for data * @return {Promise<Buffer>} Decrypted seed * @private * @ignore */ function decryptLegacy( dataBuffer: Buffer, password: string, triplesecDecrypt: TriplesecDecryptSignature ): Promise<Buffer> { return new Promise<Buffer>((resolve, reject) => { if (!triplesecDecrypt) { reject(new Error('The `triplesec.decrypt` function must be provided')); } triplesecDecrypt( { key: Buffer.from(password), data: dataBuffer }, (err, plaintextBuffer) => { if (!err) { resolve(plaintextBuffer); } else { reject(err); } } ); }); } /** * Decrypt an encrypted mnemonic phrase with a password. * Legacy triplesec encrypted payloads are also supported. * @param data - Buffer or hex-encoded string of the encrypted mnemonic * @param password - Password for data * @return the raw mnemonic phrase * @private * @ignore */ export async function decryptMnemonic( data: string | Buffer, password: string, triplesecDecrypt?: TriplesecDecryptSignature ) { const dataBuffer = Buffer.isBuffer(data) ? data : Buffer.from(data, 'hex'); try { return await decryptMnemonicBuffer(dataBuffer, password); } catch (err) { if (err instanceof PasswordError) { throw err; } const data = await decryptLegacy(dataBuffer, password, triplesecDecrypt); return data.toString(); } }