import { Buffer } from '@stacks/common' ; import { ec as EllipticCurve } from 'elliptic' ; import * as BN from 'bn.js' ; import { randomBytes } from './cryptoRandom' ; import { FailedDecryptionError } from '@stacks/common' ; import { getPublicKeyFromPrivate } from './keys' ; import { hashSha256Sync , hashSha512Sync } from './sha2Hash' ; import { createHmacSha256 } from './hmacSha256' ; import { createCipher } from './aesCipher' ; import { getAesCbcOutputLength , getBase64OutputLength } from './utils' ; const ecurve = new EllipticCurve ( 'secp256k1' ) ; ignore export type CipherTextEncoding = 'hex' | 'base64' ; ignore export type CipherObject = { iv : string ; ephemeralPK : string ; cipherText : string ; /** If undefined then hex encoding is used for the `cipherText` string. */ cipherTextEncoding ? : CipherTextEncoding ; mac : string ; wasString : boolean ; } ; ignore export type SignedCipherObject = { /** Hex encoded DER signature (up to 144 chars) */ signature : string ; /** Hex encoded public key (66 char length) */ publicKey : string ; /** The stringified json of a `CipherObject` */ cipherText : string ; } ; ignore export enum InvalidPublicKeyReason { InvalidFormat = 'InvalidFormat' , IsNotPoint = 'IsNotPoint' } ignore export async function aes256CbcEncrypt ( iv , key , plaintext ) { const cipher = await createCipher ( ) ; const result = await cipher . encrypt ( 'aes-256-cbc' , key , iv , plaintext ) ; return result ; } ignore async function aes256CbcDecrypt ( iv , key , ciphertext ) { const cipher = await createCipher ( ) ; const result = await cipher . decrypt ( 'aes-256-cbc' , key , iv , ciphertext ) ; return result ; } ignore async function hmacSha256 ( key , content ) { const hmacSha256 = await createHmacSha256 ( ) ; return hmacSha256 . digest ( key , content ) ; } ignore function equalConstTime ( b1 , b2 ) { if ( b1 . length !== b2 . length ) { return false ; } let res = 0 ; for ( let i = 0 ; i < b1 . length ; i ++ ) { res |= b1 [ i ] ^ b2 [ i ] ; } return res === 0 ; } ignore function sharedSecretToKeys ( sharedSecret ) { const hashedSecret = hashSha512Sync ( sharedSecret ) ; return { encryptionKey : hashedSecret . slice ( 0 , 32 ) , hmacKey : hashedSecret . slice ( 32 ) , } ; } ignore function allHexChars ( maybe ) { return maybe . match ( /^[0-9a-f]+$/i ) !== null ; } ignore function isValidPublicKey ( pub ) { const invalidFormat = { result : false , reason_data : 'Invalid public key format' , reason : InvalidPublicKeyReason . InvalidFormat , } ; const invalidPoint = { result : false , reason_data : 'Public key is not a point' , reason : InvalidPublicKeyReason . IsNotPoint , } ; if ( pub . length !== 66 && pub . length !== 130 ) return invalidFormat ; const firstByte = pub . slice ( 0 , 2 ) ; if ( pub . length === 130 && firstByte !== '04' ) return invalidFormat ; if ( pub . length === 66 && firstByte !== '02' && firstByte !== '03' ) return invalidFormat ; if ( ! allHexChars ( pub ) ) return invalidFormat ; const secp256k1 = new EllipticCurve ( 'secp256k1' ) ; try { const keyPair = secp256k1 . keyFromPublic ( Buffer . from ( pub , 'hex' ) ) ; const result = keyPair . validate ( ) ; return { result : result . result , reason_data : result . reason , reason : result . result ? null : InvalidPublicKeyReason . IsNotPoint , } ; } catch ( e ) { return invalidPoint ; } } ignore export function getHexFromBN ( bnInput ) { const hexOut = bnInput . toString ( 'hex' , 64 ) ; if ( hexOut . length === 64 ) { return hexOut ; } else if ( hexOut . length < 64 ) { const padding = '0' . repeat ( 64 - hexOut . length ) ; return `${ padding }${ hexOut }` ; } else { throw new Error ( 'Generated a > 32-byte BN for encryption. Failing.' ) ; } } ignore export function getBufferFromBN ( bnInput ) { const result = bnInput . toArrayLike ( Buffer , 'be' , 32 ) ; if ( result . byteLength !== 32 ) { throw new Error ( 'Failed to generate a 32-byte BN' ) ; } return result ; } ignore export function getCipherObjectWrapper ( opts ) { const shell = { iv : '' , ephemeralPK : '' , mac : '' , cipherText : '' , wasString : ! ! opts . wasString , } ; if ( opts . cipherTextEncoding === 'base64' ) { shell . cipherTextEncoding = 'base64' ; } const ivLength = 32 ; const ephemeralPKLength = 66 ; const macLength = 64 ; return { payloadValuesLength : ivLength + ephemeralPKLength + macLength , payloadShell : JSON . stringify ( shell ) , } ; } param payloadShell ignore export function getSignedCipherObjectWrapper ( payloadShell ) { const shell = { signature : '' , publicKey : '' , cipherText : payloadShell , } ; const signatureLength = 144 ; const publicKeyLength = 66 ; return { signedPayloadValuesLength : signatureLength + publicKeyLength , signedPayloadShell : JSON . stringify ( shell ) , } ; } ignore export function eciesGetJsonStringLength ( opts ) { const { payloadShell , payloadValuesLength } = getCipherObjectWrapper ( opts ) ; const cipherTextLength = getAesCbcOutputLength ( opts . contentLength ) ; let encodedCipherTextLength ; if ( ! opts . cipherTextEncoding || opts . cipherTextEncoding === 'hex' ) { encodedCipherTextLength = cipherTextLength * 2 ; } else if ( opts . cipherTextEncoding === 'base64' ) { encodedCipherTextLength = getBase64OutputLength ( cipherTextLength ) ; } else { throw new Error ( `Unexpected cipherTextEncoding "${ opts . cipherTextEncoding }"` ) ; } if ( ! opts . sign ) { return payloadShell . length + payloadValuesLength + encodedCipherTextLength ; } else { const { signedPayloadShell , signedPayloadValuesLength } = getSignedCipherObjectWrapper ( payloadShell ) ; return ( signedPayloadShell . length + signedPayloadValuesLength + payloadValuesLength + encodedCipherTextLength ) ; } } param publicKey param content return private ignore export async function encryptECIES ( publicKey , content , wasString , cipherTextEncoding ? ) { const validity = isValidPublicKey ( publicKey ) ; if ( ! validity . result ) { throw validity ; } const ecPK = ecurve . keyFromPublic ( publicKey , 'hex' ) . getPublic ( ) ; const ephemeralSK = ecurve . genKeyPair ( ) ; const ephemeralPK = Buffer . from ( ephemeralSK . getPublic ( ) . encodeCompressed ( ) ) ; const sharedSecret = ephemeralSK . derive ( ecPK ) as BN ; const sharedSecretBuffer = getBufferFromBN ( sharedSecret ) ; const sharedKeys = sharedSecretToKeys ( sharedSecretBuffer ) ; const initializationVector = randomBytes ( 16 ) ; const cipherText = await aes256CbcEncrypt ( initializationVector , sharedKeys . encryptionKey , content ) ; const macData = Buffer . concat ( [ initializationVector , ephemeralPK , cipherText ] ) ; const mac = await hmacSha256 ( sharedKeys . hmacKey , macData ) ; let cipherTextString ; if ( ! cipherTextEncoding || cipherTextEncoding === 'hex' ) { cipherTextString = cipherText . toString ( 'hex' ) ; } else if ( cipherTextEncoding === 'base64' ) { cipherTextString = cipherText . toString ( 'base64' ) ; } else { throw new Error ( `Unexpected cipherTextEncoding "${ cipherTextEncoding }"` ) ; } const result = { iv : initializationVector . toString ( 'hex' ) , ephemeralPK : ephemeralPK . toString ( 'hex' ) , cipherText : cipherTextString , mac : mac . toString ( 'hex' ) , wasString : ! ! wasString , } ; if ( cipherTextEncoding && cipherTextEncoding !== 'hex' ) { result . cipherTextEncoding = cipherTextEncoding ; } return result ; } param { String } privateKey param { Object } cipherObject return { Buffer } throws private ignore export async function decryptECIES ( privateKey , cipherObject ) { const ecSK = ecurve . keyFromPrivate ( privateKey , 'hex' ) ; let ephemeralPK = null ; try { ephemeralPK = ecurve . keyFromPublic ( cipherObject . ephemeralPK , 'hex' ) . getPublic ( ) ; } catch ( error ) { throw new FailedDecryptionError ( 'Unable to get public key from cipher object. ' + 'You might be trying to decrypt an unencrypted object.' ) ; } const sharedSecret = ecSK . derive ( ephemeralPK ) as BN ; const sharedSecretBuffer = getBufferFromBN ( sharedSecret ) ; const sharedKeys = sharedSecretToKeys ( sharedSecretBuffer ) ; const ivBuffer = Buffer . from ( cipherObject . iv , 'hex' ) ; let cipherTextBuffer ; if ( ! cipherObject . cipherTextEncoding || cipherObject . cipherTextEncoding === 'hex' ) { cipherTextBuffer = Buffer . from ( cipherObject . cipherText , 'hex' ) ; } else if ( cipherObject . cipherTextEncoding === 'base64' ) { cipherTextBuffer = Buffer . from ( cipherObject . cipherText , 'base64' ) ; } else { throw new Error ( `Unexpected cipherTextEncoding "${ cipherObject . cipherText }"` ) ; } const macData = Buffer . concat ( [ ivBuffer , Buffer . from ( ephemeralPK . encodeCompressed ( ) ) , cipherTextBuffer , ] ) ; const actualMac = await hmacSha256 ( sharedKeys . hmacKey , macData ) ; const expectedMac = Buffer . from ( cipherObject . mac , 'hex' ) ; if ( ! equalConstTime ( expectedMac , actualMac ) ) { throw new FailedDecryptionError ( 'Decryption failed: failure in MAC check' ) ; } const plainText = await aes256CbcDecrypt ( ivBuffer , sharedKeys . encryptionKey , cipherTextBuffer ) ; if ( cipherObject . wasString ) { return plainText . toString ( ) ; } else { return plainText ; } } param { String } privateKey param { Object } content return { Object } private ignore export function signECDSA ( privateKey , content ) { const contentBuffer = content instanceof Buffer ? content : Buffer . from ( content ) ; const ecPrivate = ecurve . keyFromPrivate ( privateKey , 'hex' ) ; const publicKey = getPublicKeyFromPrivate ( privateKey ) ; const contentHash = hashSha256Sync ( contentBuffer ) ; const signature = ecPrivate . sign ( contentHash ) ; const signatureString = signature . toDER ( 'hex' ) ; return { signature : signatureString , publicKey , } ; } ignore function getBuffer ( content ) { if ( content instanceof Buffer ) return content ; else if ( content instanceof ArrayBuffer ) return Buffer . from ( content ) ; else return Buffer . from ( content ) ; } param { String | Buffer } content param { String } publicKey param { String } signature return { Boolean } private ignore export function verifyECDSA ( content , publicKey , signature ) { const contentBuffer = getBuffer ( content ) ; const ecPublic = ecurve . keyFromPublic ( publicKey , 'hex' ) ; const contentHash = hashSha256Sync ( contentBuffer ) ; return ecPublic . verify ( contentHash , < any > signature ) ; }