UNPKG

@onflow/flow-js-testing

Version:

This package will expose a set of utility methods, to allow Cadence code testing with libraries like Jest

349 lines (303 loc) 10.9 kB
/* * Flow JS Testing * * Copyright 2020-2021 Dapper Labs, Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import * as rlp from "rlp" import {config, account} from "@onflow/fcl" import {ec as EC} from "elliptic" import {getServiceAddress, isObject, isString} from "./utils" import {invariant} from "./invariant" import {sha3_256} from "js-sha3" import {sha256 as sha2_256} from "js-sha256" /** * Represents a signature for an arbitrary message generated using a particular key * @typedef {Object} SignatureObject * @property {string} addr address of account whose key was used to sign the message * @property {number} keyId key index on the account of they key used to sign the message * @property {string} signature signature corresponding to the signed message hash as hex-encoded string */ /** * Represents a private key object which may be used to generate a public key * @typedef {Object} KeyObject * @property {string | Buffer} privateKey private key for this key object * @property {SignatureAlgorithm} [signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used with this key * @property {HashAlgorithm} [hashAlgorithm=HashAlgorithm.SHA3_256] hash algorithm used with this key * @property {weight} [weight=1000] desired weight of this key (default full weight) */ /** * Represents a signer of a message or transaction * @typedef {Object} SignerInfoObject * @property {string} addr address of the signer * @property {HashAlgorithm} [hashAlgorithm=HashAlgorithm.SHA3_256] hash algorithm used to hash the message before signing * @property {SignatureAlgorithm} [signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used to generate the signature * @property {number} [keyId=0] index of the key on the signers account to use * @property {string | Buffer} [privateKey=SERVICE_KEY] private key of the signer (defaults to universal private key/service key from config) */ /** * Enum for signing algorithms * @readonly * @enum {number} */ export const SignatureAlgorithm = { ECDSA_P256: 1, ECDSA_secp256k1: 2, } /** * Enum for hasing algorithms * @readonly * @enum {number} */ export const HashAlgorithm = { SHA2_256: 1, SHA3_256: 3, } /** * Enum for mapping hash algorithm name to hashing function * @readonly * @enum {function} */ export const HashFunction = { SHA2_256: sha2_256, SHA3_256: sha3_256, } /** * Enum for mapping signature algorithm to elliptic instance * @readonly * @enum {EC} */ export const ec = { ECDSA_P256: new EC("p256"), ECDSA_secp256k1: new EC("secp256k1"), } export const resolveHashAlgoKey = hashAlgorithm => { const hashAlgorithmKey = Object.keys(HashAlgorithm).find( x => HashAlgorithm[x] === hashAlgorithm || (isString(hashAlgorithm) && x.toLowerCase() === hashAlgorithm.toLowerCase()) ) if (!hashAlgorithmKey) throw new Error( `Provided hash algorithm "${hashAlgorithm}" is not currently supported` ) return hashAlgorithmKey } export const resolveSignAlgoKey = signatureAlgorithm => { const signatureAlgorithmKey = Object.keys(SignatureAlgorithm).find( x => SignatureAlgorithm[x] === signatureAlgorithm || (isString(signatureAlgorithm) && x.toLowerCase() === signatureAlgorithm.toLowerCase()) ) if (!signatureAlgorithmKey) throw new Error( `Provided signature algorithm "${signatureAlgorithm}" is not currently supported` ) return signatureAlgorithmKey } const hashMsgHex = (msgHex, hashAlgorithm = HashAlgorithm.SHA3_256) => { const hashAlgorithmKey = resolveHashAlgoKey(hashAlgorithm) const hashFn = HashFunction[hashAlgorithmKey] const hash = hashFn.create() hash.update(Buffer.from(msgHex, "hex")) return Buffer.from(hash.arrayBuffer()) } export const signWithKey = ( privateKey, msgHex, hashAlgorithm = HashAlgorithm.SHA3_256, signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 ) => { const signAlgo = resolveSignAlgoKey(signatureAlgorithm) const key = ec[signAlgo].keyFromPrivate(Buffer.from(privateKey, "hex")) const sig = key.sign(hashMsgHex(msgHex, hashAlgorithm)) const n = 32 // half of signature length? const r = sig.r.toArrayLike(Buffer, "be", n) const s = sig.s.toArrayLike(Buffer, "be", n) return Buffer.concat([r, s]).toString("hex") } export const resolveSignerKey = async signer => { let addr = await getServiceAddress(), keyId = 0, privateKey = await config().get("PRIVATE_KEY"), hashAlgorithm = HashAlgorithm.SHA3_256, signatureAlgorithm = SignatureAlgorithm.ECDSA_P256 if (isObject(signer)) { ;({ addr = addr, keyId = keyId, privateKey = privateKey, hashAlgorithm = hashAlgorithm, signatureAlgorithm = signatureAlgorithm, } = signer) } else { addr = signer || addr } return { addr, keyId, privateKey, hashAlgorithm, signatureAlgorithm, } } export const authorization = signer => async (account = {}) => { const {addr, keyId, privateKey, hashAlgorithm, signatureAlgorithm} = await resolveSignerKey(signer) const signingFunction = async data => ({ keyId, addr: addr, signature: signWithKey( privateKey, data.message, hashAlgorithm, signatureAlgorithm ), }) return { ...account, addr, keyId, signingFunction, } } /** * Returns an RLP-encoded public key for a particular private key as a hex-encoded string * @param {KeyObject} keyObject * @param {string | Buffer} keyObject.privateKey private key as hex-encoded string or Buffer * @param {HashAlgorithm | string} [keyObject.hashAlgorithm=HashAlgorithm.SHA3_256] hasing algorithnm used to hash messages using this key * @param {SignatureAlgorithm | string} [keyObject.signatureAlgorithm=SignatureAlgorithm.ECDSA_P256] signing algorithm used to generate signatures using this key * @param {number} [keyObject.weight=1000] weight of the key * @returns {string} */ export const pubFlowKey = async (keyObject = {}) => { let { privateKey = await config().get("PRIVATE_KEY"), hashAlgorithm = HashAlgorithm.SHA3_256, signatureAlgorithm = SignatureAlgorithm.ECDSA_P256, weight = 1000, // give key full weight } = keyObject // Convert hex string private key to buffer if not buffer already if (!Buffer.isBuffer(privateKey)) privateKey = Buffer.from(privateKey, "hex") const hashAlgoName = resolveHashAlgoKey(hashAlgorithm) const sigAlgoName = resolveSignAlgoKey(signatureAlgorithm) const keys = ec[sigAlgoName].keyFromPrivate(privateKey) const publicKey = keys.getPublic("hex").replace(/^04/, "") return rlp .encode([ Buffer.from(publicKey, "hex"), // publicKey hex to binary SignatureAlgorithm[sigAlgoName], HashAlgorithm[hashAlgoName], weight, ]) .toString("hex") } export const prependDomainTag = (msgHex, domainTag) => { const rightPaddedBuffer = buffer => Buffer.concat([Buffer.alloc(32 - buffer.length, 0), buffer]) let domainTagBuffer = rightPaddedBuffer(Buffer.from(domainTag, "utf-8")) return domainTagBuffer.toString("hex") + msgHex } /** * Signs a user message for a given signer * @param {string | Buffer} msgHex hex-encoded string or Buffer of the message to sign * @param {string | SignerInfoObject} signer signer address provided as string and JS Testing signs with universal private key/service key or signer info provided manually via SignerInfoObject * @param {string} domainTag utf-8 domain tag to use when hashing message * @returns {SignatureObject} signature object which can be validated using verifyUserSignatures */ export const signUserMessage = async (msgHex, signer, domainTag) => { if (Buffer.isBuffer(msgHex)) msgHex.toString("hex") const {addr, keyId, privateKey, hashAlgorithm, signatureAlgorithm} = await resolveSignerKey(signer, true) if (domainTag) { msgHex = prependDomainTag(msgHex, domainTag) } return { keyId, addr: addr, signature: signWithKey( privateKey, msgHex, hashAlgorithm, signatureAlgorithm ), } } /** * Verifies whether user signatures were valid for a particular message hex * @param {string | Buffer} msgHex hex-encoded string or buffer of message to verify * @param {[SignatureObject]} signatures array of signatures to verify against msgHex * @param {string} [domainTag=""] utf-8 domain tag to use when hashing message * @returns {boolean} true if signatures are valid and total weight >= 1000 */ export const verifyUserSignatures = async ( msgHex, signatures, domainTag = "" ) => { if (Buffer.isBuffer(msgHex)) msgHex = msgHex.toString("hex") invariant(signatures, "One or mores signatures must be provided") // convert to array signatures = [].concat(signatures) invariant(signatures.length > 0, "One or mores signatures must be provided") invariant( signatures.reduce( (valid, sig) => valid && sig.signature != null && sig.keyId != null && sig.addr != null, true ), "One or more signature is invalid. Valid signatures have the following keys: addr, keyId, siganture" ) const address = signatures[0].addr invariant( signatures.reduce((same, sig) => same && sig.addr === address, true), "Signatures must belong to the same address" ) const keys = (await account(address)).keys const largestKeyId = Math.max(...signatures.map(sig => sig.keyId)) invariant( largestKeyId < keys.length, `Key index ${largestKeyId} does not exist on account ${address}` ) // Apply domain tag if needed if (domainTag) { msgHex = prependDomainTag(msgHex, domainTag) } let totalWeight = 0 for (let i in signatures) { const {signature, keyId} = signatures[i] const { hashAlgoString: hashAlgo, signAlgoString: signAlgo, weight, publicKey, revoked, } = keys[keyId] const key = ec[signAlgo].keyFromPublic(Buffer.from("04" + publicKey, "hex")) if (revoked) return false const msgHash = hashMsgHex(msgHex, hashAlgo) const sigBuffer = Buffer.from(signature, "hex") const signatureInput = { r: sigBuffer.slice(0, 32), s: sigBuffer.slice(-32), } if (!key.verify(msgHash, signatureInput)) return false totalWeight += weight } return totalWeight >= 1000 }