UNPKG

aladinnetwork-blockstack

Version:

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

231 lines (202 loc) 7.12 kB
import { ec as EllipticCurve } from 'elliptic' import BN from 'bn.js' import crypto from 'crypto' import { getPublicKeyFromPrivate } from '../keys' const ecurve = new EllipticCurve('secp256k1') /** * @ignore */ export type CipherObject = { iv: string, ephemeralPK: string, cipherText: string, mac: string, wasString: boolean } /** * @ignore */ function aes256CbcEncrypt(iv: Buffer, key: Buffer, plaintext: Buffer) { const cipher = crypto.createCipheriv('aes-256-cbc', key, iv) return Buffer.concat([cipher.update(plaintext), cipher.final()]) } /** * @ignore */ function aes256CbcDecrypt(iv: Buffer, key: Buffer, ciphertext: Buffer) { const cipher = crypto.createDecipheriv('aes-256-cbc', key, iv) return Buffer.concat([cipher.update(ciphertext), cipher.final()]) } /** * @ignore */ function hmacSha256(key: Buffer, content: Buffer) { return crypto.createHmac('sha256', key).update(content).digest() } /** * @ignore */ function equalConstTime(b1: Buffer, b2: Buffer) { if (b1.length !== b2.length) { return false } let res = 0 for (let i = 0; i < b1.length; i++) { res |= b1[i] ^ b2[i] // jshint ignore:line } return res === 0 } /** * @ignore */ function sharedSecretToKeys(sharedSecret: Buffer) { // generate mac and encryption key from shared secret const hashedSecret = crypto.createHash('sha512').update(sharedSecret).digest() return { encryptionKey: hashedSecret.slice(0, 32), hmacKey: hashedSecret.slice(32) } } /** * @ignore */ export function getHexFromBN(bnInput: BN) { const hexOut = bnInput.toString('hex') if (hexOut.length === 64) { return hexOut } else if (hexOut.length < 64) { // pad with leading zeros // the padStart function would require node 9 const padding = '0'.repeat(64 - hexOut.length) return `${padding}${hexOut}` } else { throw new Error('Generated a > 32-byte BN for encryption. Failing.') } } /** * Encrypt content to elliptic curve publicKey using ECIES * @param {String} publicKey - secp256k1 public key hex string * @param {String | Buffer} content - content to encrypt * @return {Object} Object containing (hex encoded): * iv (initialization vector), cipherText (cipher text), * mac (message authentication code), ephemeral public key * wasString (boolean indicating with or not to return a buffer or string on decrypt) * * @private * @ignore */ export function encryptECIES(publicKey: string, content: string | Buffer): CipherObject { const isString = (typeof (content) === 'string') // always copy to buffer const plainText = content instanceof Buffer ? Buffer.from(content) : Buffer.from(content) const ecPK = ecurve.keyFromPublic(publicKey, 'hex').getPublic() as BN const ephemeralSK = ecurve.genKeyPair() const ephemeralPK = ephemeralSK.getPublic() const sharedSecret = ephemeralSK.derive(ecPK) as BN const sharedSecretHex = getHexFromBN(sharedSecret) const sharedKeys = sharedSecretToKeys( Buffer.from(sharedSecretHex, 'hex') ) const initializationVector = crypto.randomBytes(16) const cipherText = aes256CbcEncrypt( initializationVector, sharedKeys.encryptionKey, plainText ) const macData = Buffer.concat([initializationVector, Buffer.from(ephemeralPK.encodeCompressed()), cipherText]) const mac = hmacSha256(sharedKeys.hmacKey, macData) return { iv: initializationVector.toString('hex'), ephemeralPK: ephemeralPK.encodeCompressed('hex'), cipherText: cipherText.toString('hex'), mac: mac.toString('hex'), wasString: isString } } /** * Decrypt content encrypted using ECIES * @param {String} privateKey - secp256k1 private key hex string * @param {Object} cipherObject - object to decrypt, should contain: * iv (initialization vector), cipherText (cipher text), * mac (message authentication code), ephemeralPublicKey * wasString (boolean indicating with or not to return a buffer or string on decrypt) * @return {Buffer} plaintext * @throws {Error} if unable to decrypt * @private * @ignore */ export function decryptECIES(privateKey: string, cipherObject: CipherObject): Buffer | string { const ecSK = ecurve.keyFromPrivate(privateKey, 'hex') const ephemeralPK = ecurve.keyFromPublic(cipherObject.ephemeralPK, 'hex').getPublic() const sharedSecret = ecSK.derive(ephemeralPK) const sharedSecretBuffer = Buffer.from(getHexFromBN(sharedSecret), 'hex') const sharedKeys = sharedSecretToKeys(sharedSecretBuffer) const ivBuffer = Buffer.from(cipherObject.iv, 'hex') const cipherTextBuffer = Buffer.from(cipherObject.cipherText, 'hex') const macData = Buffer.concat([ivBuffer, Buffer.from(ephemeralPK.encodeCompressed()), cipherTextBuffer]) const actualMac = hmacSha256(sharedKeys.hmacKey, macData) const expectedMac = Buffer.from(cipherObject.mac, 'hex') if (!equalConstTime(expectedMac, actualMac)) { throw new Error('Decryption failed: failure in MAC check') } const plainText = aes256CbcDecrypt( ivBuffer, sharedKeys.encryptionKey, cipherTextBuffer ) if (cipherObject.wasString) { return plainText.toString() } else { return plainText } } /** * Sign content using ECDSA * * @param {String} privateKey - secp256k1 private key hex string * @param {Object} content - content to sign * @return {Object} contains: * signature - Hex encoded DER signature * public key - Hex encoded private string taken from privateKey * @private * @ignore */ export function signECDSA(privateKey: string, content: string | Buffer): { publicKey: string, signature: string } { const contentBuffer = content instanceof Buffer ? content : Buffer.from(content) const ecPrivate = ecurve.keyFromPrivate(privateKey, 'hex') const publicKey = getPublicKeyFromPrivate(privateKey) const contentHash = crypto.createHash('sha256').update(contentBuffer).digest() const signature = ecPrivate.sign(contentHash) const signatureString = signature.toDER('hex') return { signature: signatureString, publicKey } } /** * @ignore */ function getBuffer(content: string | ArrayBuffer | Buffer) { if (content instanceof Buffer) return content else if (content instanceof ArrayBuffer) return Buffer.from(content) else return Buffer.from(content) } /** * Verify content using ECDSA * @param {String | Buffer} content - Content to verify was signed * @param {String} publicKey - secp256k1 private key hex string * @param {String} signature - Hex encoded DER signature * @return {Boolean} returns true when signature matches publickey + content, false if not * @private * @ignore */ export function verifyECDSA(content: string | ArrayBuffer | Buffer, publicKey: string, signature: string) { const contentBuffer = getBuffer(content) const ecPublic = ecurve.keyFromPublic(publicKey, 'hex') const contentHash = crypto.createHash('sha256').update(contentBuffer).digest() return ecPublic.verify(contentHash, <any>signature) }