aladinnetwork-blockstack
Version:
The Aladin Javascript library for authentication, identity, and storage.
231 lines (202 loc) • 7.12 kB
text/typescript
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)
}