From 5c59d0abfe47cdaae5757f27a36bb0d4b4ee80bd Mon Sep 17 00:00:00 2001 From: Dmytro Vynnyk Date: Mon, 23 Dec 2024 13:22:44 +0100 Subject: [PATCH] Add DeployUtils, AsymmetricKey and ClPublicKey wrappers --- src/utils/deprecated-clPublicKey.ts | 196 ++++++++ src/utils/deprecated-deploy-utils.ts | 500 +++++++++++++++++++ src/utils/deprecated-keys.ts | 720 +++++++++++++++++++++++++++ src/utils/index.ts | 6 + 4 files changed, 1422 insertions(+) create mode 100644 src/utils/deprecated-clPublicKey.ts create mode 100644 src/utils/deprecated-deploy-utils.ts create mode 100644 src/utils/deprecated-keys.ts diff --git a/src/utils/deprecated-clPublicKey.ts b/src/utils/deprecated-clPublicKey.ts new file mode 100644 index 000000000..f9ece2f92 --- /dev/null +++ b/src/utils/deprecated-clPublicKey.ts @@ -0,0 +1,196 @@ +import { concat } from '@ethersproject/bytes'; +import { + CLType, + CLTypePublicKey, + CLValue, + Conversions, + PublicKey +} from '../types'; +import { SignatureAlgorithm } from './deprecated-keys'; +import { byteHash } from '../types/ByteConverters'; + +const ED25519_LENGTH = 32; +const SECP256K1_LENGTH = 33; + +/** @deprecated */ +export enum CLPublicKeyTag { + ED25519 = 1, + SECP256K1 = 2 +} + +/** @deprecated use {@link PublicKey} */ +export class CLPublicKey extends CLValue { + data: Uint8Array; + tag: CLPublicKeyTag; + private _publicKey: PublicKey; + + get pk(): PublicKey { + return this._publicKey; + } + + /** @deprecated */ + constructor( + rawPublicKey: Uint8Array, + tag: CLPublicKeyTag | SignatureAlgorithm + ) { + super(CLTypePublicKey); + + // NOTE Two ifs because of the legacy indentifiers in ./Keys + if (tag === CLPublicKeyTag.ED25519 || tag === SignatureAlgorithm.Ed25519) { + if (rawPublicKey.length !== ED25519_LENGTH) { + throw new Error( + `Wrong length of ED25519 key. Expected ${ED25519_LENGTH}, but got ${rawPublicKey.length}.` + ); + } + this.data = rawPublicKey; + this.tag = CLPublicKeyTag.ED25519; + + const algorithmIdentifier = CLPublicKeyTag[this.tag]; + const separator = Uint8Array.from([0]); + const prefix = Buffer.concat([ + Buffer.from(algorithmIdentifier.toLowerCase()), + separator + ]); + + this._publicKey = PublicKey.fromBytes( + // TODO check if this.data contains prefix. If yes -> remove concat + byteHash(concat([prefix, this.data])) + ).result; + return; + } + + if ( + tag === CLPublicKeyTag.SECP256K1 || + tag === SignatureAlgorithm.Secp256K1 + ) { + if (rawPublicKey.length !== SECP256K1_LENGTH) { + throw new Error( + `Wrong length of SECP256K1 key. Expected ${SECP256K1_LENGTH}, but got ${rawPublicKey.length}.` + ); + } + this.data = rawPublicKey; + this.tag = CLPublicKeyTag.SECP256K1; + + const algorithmIdentifier = CLPublicKeyTag[this.tag]; + const separator = Uint8Array.from([0]); + const prefix = Buffer.concat([ + Buffer.from(algorithmIdentifier.toLowerCase()), + separator + ]); + + this._publicKey = PublicKey.fromBytes( + byteHash(concat([prefix, this.data])) // TODO check if this.data contains prefix. If yes -> remove concat + ).result; + return; + } + + throw new Error('Unsupported type of public key'); + } + + /** @deprecated */ + clType(): CLType { + return CLTypePublicKey; + } + + /** @deprecated */ + isEd25519(): boolean { + return this.tag === CLPublicKeyTag.ED25519; + } + + /** @deprecated */ + isSecp256K1(): boolean { + return this.tag === CLPublicKeyTag.SECP256K1; + } + + /** @deprecated */ + toHex(checksummed = true): string { + return this._publicKey.toHex(checksummed); + } + + /** @deprecated */ + toAccountHash(): Uint8Array { + return this._publicKey.accountHash().toBytes(); + } + + /** @deprecated */ + toAccountHashStr(): string { + const bytes = this.toAccountHash(); + const hashHex = Buffer.from(bytes).toString('hex'); + return `account-hash-${hashHex}`; + } + + /** @deprecated */ + toAccountRawHashStr(): string { + const bytes = this.toAccountHash(); + const hashHex = Buffer.from(bytes).toString('hex'); + return hashHex; + } + + /** @deprecated */ + value(): Uint8Array { + return this.data; + } + + /** @deprecated */ + static fromEd25519(publicKey: Uint8Array): CLPublicKey { + return new CLPublicKey(publicKey, CLPublicKeyTag.ED25519); + } + + /** @deprecated */ + static fromSecp256K1(publicKey: Uint8Array): CLPublicKey { + return new CLPublicKey(publicKey, CLPublicKeyTag.SECP256K1); + } + + /** + * @deprecated + * Tries to decode PublicKey from its hex-representation. + * The hex format should be as produced by CLPublicKey.toHex + * @param publicKeyHex public key hex string contains key tag + * @param checksummed throws an Error if true and given string is not checksummed + */ + static fromHex(publicKeyHex: string, checksummed = false): CLPublicKey { + if (publicKeyHex.length < 2) { + throw new Error('Asymmetric key error: too short'); + } + + if (!/^0(1[0-9a-fA-F]{64}|2[0-9a-fA-F]{66})$/.test(publicKeyHex)) { + throw new Error('Invalid public key'); + } + + if (!PublicKey.isChecksummed(publicKeyHex)) { + console.warn( + 'Provided public key is not checksummed. Please check if you provide valid public key. You can generate checksummed public key from CLPublicKey.toHex(true).' + ); + if (checksummed) throw Error('Provided public key is not checksummed.'); + } + + const publicKeyHexBytes = Conversions.decodeBase16(publicKeyHex); + + return new CLPublicKey(publicKeyHexBytes.subarray(1), publicKeyHexBytes[0]); + } + + /** @deprecated */ + getTag(): CLPublicKeyTag { + return this.tag; + } + + /** @deprecated */ + getSignatureAlgorithm(): SignatureAlgorithm { + const mapTagToSignatureAlgorithm = ( + tag: CLPublicKeyTag + ): SignatureAlgorithm => { + const signatureAlgorithm = { + [CLPublicKeyTag.ED25519]: SignatureAlgorithm.Ed25519, + [CLPublicKeyTag.SECP256K1]: SignatureAlgorithm.Secp256K1 + }[tag]; + + if (signatureAlgorithm === undefined) { + throw Error('Unknown tag to signature algo mapping.'); + } + + return signatureAlgorithm; + }; + + return mapTagToSignatureAlgorithm(this.tag); + } +} diff --git a/src/utils/deprecated-deploy-utils.ts b/src/utils/deprecated-deploy-utils.ts new file mode 100644 index 000000000..7d163312d --- /dev/null +++ b/src/utils/deprecated-deploy-utils.ts @@ -0,0 +1,500 @@ +import { Result, Ok, Err } from 'ts-results'; +import { concat } from '@ethersproject/bytes'; +import { BigNumber, BigNumberish } from '@ethersproject/bignumber'; +import humanizeDuration from 'humanize-duration'; +import fetch from 'node-fetch'; +import { + Approval, + CLValue, + Conversions, + DEFAULT_DEPLOY_TTL, + Deploy, + DeployHeader, + Duration, + ExecutableDeployItem, + Hash, + HexBytes, + Timestamp +} from '../types'; +import { CLPublicKey } from './deprecated-clPublicKey'; +import { byteHash, toBytesU32 } from '../types/ByteConverters'; +import { AsymmetricKey, validateSignature } from './deprecated-keys'; + +const shortEnglishHumanizer = humanizeDuration.humanizer({ + spacer: '', + serialComma: false, + conjunction: ' ', + delimiter: ' ', + language: 'shortEn', + languages: { + // https://docs.rs/humantime/2.0.1/humantime/fn.parse_duration.html + shortEn: { + d: () => 'day', + h: () => 'h', + m: () => 'm', + s: () => 's', + ms: () => 'ms' + } + } +}); + +/** + * @deprecated + * Returns a humanizer duration + * @param ttl in milliseconds + * @returns A human-readable time in days, hours, minutes, seconds, then milliseconds + */ +export const humanizerTTL = (ttl: number) => { + return shortEnglishHumanizer(ttl); +}; + +/** + * @deprecated + * Returns duration in milliseconds + * @param ttl Human-readable string generated by [humanizerTTL](#L91) + * @returns The time-to-live in milliseconds + */ +export const dehumanizerTTL = (ttl: string): number => { + const dehumanizeUnit = (s: string): number => { + if (s.includes('ms')) { + return Number(s.replace('ms', '')); + } + if (s.includes('s') && !s.includes('m')) { + return Number(s.replace('s', '')) * 1000; + } + if (s.includes('m') && !s.includes('s')) { + return Number(s.replace('m', '')) * 60 * 1000; + } + if (s.includes('h')) { + return Number(s.replace('h', '')) * 60 * 60 * 1000; + } + if (s.includes('day')) { + return Number(s.replace('day', '')) * 24 * 60 * 60 * 1000; + } + throw Error('Unsuported TTL unit'); + }; + + return ttl + .split(' ') + .map(dehumanizeUnit) + .reduce((acc, val) => (acc += val)); +}; + +/** + * @deprecated + * An object containing a unique address constructed from the `transferId` of a `Deploy` + */ +export class UniqAddress { + /** The `CLPublicKey` representation of the transacting account */ + publicKey: CLPublicKey; + + /** A transaction nonce */ + transferId: BigNumber; + + /** + * Constructs UniqAddress from the transacting account's `CLPublicKey` and unique transferId. + * @param publicKey CLPublicKey instance + * @param transferId BigNumberish value (can be also string representing number). Max U64. + */ + constructor(publicKey: CLPublicKey, transferId: BigNumberish) { + if (!(publicKey instanceof CLPublicKey)) { + throw new Error('publicKey is not an instance of CLPublicKey'); + } + const bigNum = BigNumber.from(transferId); + if (bigNum.gt('18446744073709551615')) { + throw new Error('transferId max value is U64'); + } + this.transferId = bigNum; + this.publicKey = publicKey; + } + + /** + * Stringifies the `UniqAddress` + * @returns string with the format "accountHex-transferIdHex" + */ + toString(): string { + return `${this.publicKey.toHex()}-${this.transferId.toHexString()}`; + } + + /** + * @deprecated + * Builds UniqAddress from string + * @param value `UniqAddress` string representation in the format "accountHex-transferIdHex" + * @returns A new `UniqAddress` + */ + static fromString(value: string): UniqAddress { + const [accountHex, transferHex] = value.split('-'); + const publicKey = CLPublicKey.fromHex(accountHex); + return new UniqAddress(publicKey, transferHex); + } +} + +/** + * @deprecated use {@link DeployHeader.toBytes} + * Serializes a `DeployHeader` into an array of bytes + * @param deployHeader + * @returns A serialized representation of the provided `DeployHeader` + */ +export const serializeHeader = (deployHeader: DeployHeader): Uint8Array => { + return deployHeader.toBytes(); +}; + +/** + * @deprecated + * Serializes the body of a deploy into an array of bytes + * @param payment Payment logic for use in a deployment + * @param session Session logic of a deploy + * @returns `Uint8Array` typed byte array, containing the payment and session logic of a deploy + */ +export const serializeBody = ( + payment: ExecutableDeployItem, + session: ExecutableDeployItem +): Uint8Array => { + return concat([payment.bytes(), session.bytes()]); +}; + +/** + * @deprecated + * Serializes an array of `Approval`s into a `Uint8Array` typed byte array + * @param approvals An array of `Approval`s to be serialized + * @returns `Uint8Array` typed byte array that can be deserialized to an array of `Approval`s + */ +export const serializeApprovals = (approvals: Approval[]): Uint8Array => { + const len = toBytesU32(approvals.length); + const bytes = concat( + approvals.map(approval => { + return concat([ + approval.signer.bytes(), + approval.signature.bytes, + ]); + }) + ); + return concat([len, bytes]); +}; + +/** + * @deprecated + * enum of supported contract types + * @enum + */ +export enum ContractType { + /** A pure WebAssembly representation of a smart contract */ + WASM = 'WASM', + /** A linked contract by hash */ + Hash = 'Hash', + /** A linked contract by name */ + Name = 'Name' +} + +/** + * The parameters of a `Deploy` object + * @deprecated use {@link Deploy.makeDeploy} + * */ +export class DeployParams { + /** + * Container for `Deploy` construction options. + * @param accountPublicKey The public key of the deploying account as a `CLPublicKey` + * @param chainName Name of the chain, to avoid the `Deploy` from being accidentally or maliciously included in a different chain. + * @param gasPrice Conversion rate between the cost of Wasm opcodes and the motes sent by the payment code, where 1 mote = 1 * 10^-9 CSPR + * @param ttl Time that the `Deploy` will remain valid for, in milliseconds. The default value is 1800000, which is 30 minutes + * @param dependencies Hex-encoded `Deploy` hashes of deploys which must be executed before this one. + * @param timestamp Note that timestamp is UTC, not local. + */ + constructor( + public accountPublicKey: CLPublicKey, + public chainName: string, + public gasPrice: number = 1, + public ttl: number = DEFAULT_DEPLOY_TTL, + public dependencies: Uint8Array[] = [], + public timestamp?: number + ) { + this.dependencies = dependencies.filter( + d => + dependencies.filter( + t => Conversions.encodeBase16(d) === Conversions.encodeBase16(t) + ).length < 2 + ); + } +} + +/** + * @deprecated use {@link Deploy.makeDeploy} + * Builds a `Deploy` object from `DeployParams`, session logic, and payment logic + * @param deployParam The parameters of the deploy, see [DeployParams](#L1323) + * @param session The session logic of the deploy + * @param payment The payment logic of the deploy + * @returns A new `Deploy` object + */ +export function makeDeploy( + deployParam: DeployParams, + session: ExecutableDeployItem, + payment: ExecutableDeployItem +): Deploy { + const serializedBody = serializeBody(payment, session); + const bodyHash = byteHash(serializedBody); + + if (!deployParam.timestamp) { + deployParam.timestamp = Date.now(); + } + + const header: DeployHeader = new DeployHeader( + deployParam.chainName, + deployParam.dependencies.map(d => new Hash(d)), + deployParam.gasPrice, + new Timestamp(new Date(deployParam.timestamp!)), + new Duration(deployParam.ttl), + deployParam.accountPublicKey.pk, + new Hash(bodyHash) + ); + + const serializedHeader = serializeHeader(header); + const deployHash = byteHash(serializedHeader); + + return new Deploy(new Hash(deployHash), header, payment, session, []); +} + +type TimeJSON = { + unixtime: number; +}; + + +class TimeService { + constructor(public url: string) {} + + async getTime(): Promise { + const result = await fetch(this.url); + const json = await result.json(); + + return json as TimeJSON; + } +} + +const TIME_API_URL = `worldtimeapi.org/api/timezone/UTC`; + +/** + * @deprecated + * Builds a `Deploy` object from `DeployParams`, session logic, and payment logic. + * If there is no timestamp in `DeployParams` it fetches it from the TimeService. + * Recommened to use in browser environment. + * @param deployParam The parameters of the deploy, see [DeployParams](#L1323) + * @param session The session logic of the deploy + * @param payment The payment logic of the deploy + * @returns A new `Deploy` object + */ +export async function makeDeployWithAutoTimestamp( + deployParam: DeployParams, + session: ExecutableDeployItem, + payment: ExecutableDeployItem +): Promise { + if (!deployParam.timestamp && typeof window !== 'undefined') { + const timeService = new TimeService( + `${location.protocol}//${TIME_API_URL}` + ); + const { unixtime } = await timeService.getTime(); + deployParam.timestamp = unixtime; + } + + return makeDeploy(deployParam, session, payment); +} + +/** + * @deprecated use {@link Deploy.sign} + * Uses the provided key pair to sign the Deploy message + * @param deploy Either an unsigned `Deploy` object or one with other signatures + * @param signingKey The keypair used to sign the `Deploy` + */ +export const signDeploy = ( + deploy: Deploy, + signingKey: AsymmetricKey +): Deploy => { + const approval = new Approval( + signingKey.publicKey.pk, + new HexBytes(signingKey.sign(deploy.hash.toBytes())) + ); + + deploy.approvals.push(approval); + + return deploy; +}; + +/** + * @deprecated use {@link Deploy.setSignature} + * Sets the algorithm of the already generated signature + * + * @param deploy A `Deploy` to be signed with `sig` + * @param sig the Ed25519 or Secp256K1 signature + * @param publicKey the public key used to generate the signature + */ +export const setSignature = ( + deploy: Deploy, + sig: Uint8Array, + publicKey: CLPublicKey +): Deploy => { + const approval = new Approval(publicKey.pk, new HexBytes(sig)); + + deploy.approvals.push(approval); + return deploy; +}; + +/** + * @deprecated use {@link ExecutableDeployItem.standardPayment} + * Creates an instance of standard payment logic + * + * @param paymentAmount The amount of motes to be used to pay for gas + * @returns A standard payment, as an `ExecutableDeployItem` to be attached to a `Deploy` + */ +export const standardPayment = ExecutableDeployItem.standardPayment; + +/** + * @deprecated use {@link Deploy.toJson} + * Convert the deploy object to a JSON representation + * + * @param deploy The `Deploy` object to convert to JSON + * @returns A JSON version of the `Deploy`, which can be converted back later + */ +export const deployToJson = (deploy: Deploy) => { + return { + deploy: Deploy.toJson(deploy) + }; +}; + +/** + * @deprecated use {@link Deploy.fromJson} + * Convert a JSON representation of a deploy to a `Deploy` object + * + * @param json A JSON representation of a `Deploy` + * @returns A `Result` that collapses to a `Deploy` or an error string + */ +export const deployFromJson = (json: any): Result => { + if (json.deploy === undefined) { + return new Err(new Error("The Deploy JSON doesn't have 'deploy' field.")); + } + let deploy = null; + try { + deploy = Deploy.fromJSON(json.deploy); + } catch (serializationError) { + return new Err(serializationError); + } + + if (deploy === undefined || deploy === null) { + return Err(new Error("The JSON can't be parsed as a Deploy.")); + } + + const valid = validateDeploy(deploy); + + if (valid.err) { + return new Err(new Error(valid.val)); + } + + return new Ok(deploy); +}; + +/** + * @deprecated use {@link Deploy.session}.setArg + * Adds a runtime argument to a `Deploy` object + * @param deploy The `Deploy` object for which to add the runtime argument + * @param name The name of the runtime argument + * @param value The value of the runtime argument + * @returns The original `Deploy` with the additional runtime argument + * @remarks Will fail if the `Deploy` has already been signed + */ +export const addArgToDeploy = ( + deploy: Deploy, + name: string, + value: CLValue +): Deploy => { + if (deploy.approvals.length !== 0) { + throw Error('Can not add argument to already signed deploy.'); + } + + deploy.session.setArg(name, value); + + return deploy; +}; + +/** + * @deprecated + * Gets the byte-size of a deploy + * @param deploy The `Deploy` for which to calculate the size + * @returns The size of the `Deploy` in its serialized representation + */ +export const deploySizeInBytes = (deploy: Deploy): number => { + const hashSize = deploy.hash.toBytes().length; + const bodySize = serializeBody(deploy.payment, deploy.session).length; + const headerSize = serializeHeader(deploy.header).length; + const approvalsSize = deploy.approvals + .map(approval => { + return ( + (approval.signature.bytes.length + approval.signer.bytes().length) / 2 + ); + }) + .reduce((a, b) => a + b, 0); + + return hashSize + headerSize + bodySize + approvalsSize; +}; + +/** + * @deprecated use {@link Deploy.validate} + * Validate a `Deploy` by calculating and comparing its stored blake2b hash + * @param deploy A `Deploy` to be validated + * @returns A `Result` that collapses to a `Deploy` or an error string + */ +export const validateDeploy = (deploy: Deploy): Result => { + if (!(deploy instanceof Deploy)) { + return new Err("'deploy' is not an instance of Deploy class."); + } + + const serializedBody = serializeBody(deploy.payment, deploy.session); + const bodyHash = byteHash(serializedBody); + + if (!arrayEquals(deploy.header.bodyHash!.toBytes(), bodyHash)) { + return Err(`Invalid deploy: bodyHash mismatch. Expected: ${bodyHash}, + got: ${deploy.header.bodyHash}.`); + } + + const serializedHeader = serializeHeader(deploy.header); + const deployHash = byteHash(serializedHeader); + + if (!arrayEquals(deploy.hash.toBytes(), deployHash)) { + return Err(`Invalid deploy: hash mismatch. Expected: ${deployHash}, + got: ${deploy.hash}.`); + } + + const isProperlySigned = deploy.approvals.every(({ signer, signature }) => { + const pk = CLPublicKey.fromHex(signer.toHex(), false); + const signatureRaw = Conversions.decodeBase16(signature.toHex().slice(2)); + return validateSignature(deploy.hash.toBytes(), signatureRaw, pk); + }); + + if (!isProperlySigned) { + return Err('Invalid signature.'); + } else { + return Ok(deploy); + } +}; + +/** + * @deprecated + * Compares two `Uint8Array`s + * @param a The first `Uint8Array` + * @param b The second `Uint8Array` + * @returns `true` if the two `Uint8Array`s match, and `false` otherwise + */ +export const arrayEquals = (a: Uint8Array, b: Uint8Array): boolean => { + return a.length === b.length && a.every((val, index) => val === b[index]); +}; + +/** + * @deprecated + * Serializes a `Deploy` to a `Uint8Array` + * @param deploy The `Deploy` to be serialized + * @returns A `Uint8Array` serialization of the provided `Deploy` + */ +export const deployToBytes = (deploy: Deploy): Uint8Array => { + return concat([ + serializeHeader(deploy.header), + deploy.hash.toBytes(), + serializeBody(deploy.payment, deploy.session), + serializeApprovals(deploy.approvals) + ]); +}; diff --git a/src/utils/deprecated-keys.ts b/src/utils/deprecated-keys.ts new file mode 100644 index 000000000..4ff1c8cc7 --- /dev/null +++ b/src/utils/deprecated-keys.ts @@ -0,0 +1,720 @@ +import { KeyAlgorithm, PrivateKey, PublicKey } from '../types/keypair'; +import { Conversions } from '../types'; +import KeyEncoder from 'key-encoder'; +import * as fs from 'fs'; +import { CLPublicKey } from "./deprecated-clPublicKey"; + +/** + * @deprecated use {@link KeyAlgorithm} instead + * @enum + */ +export enum SignatureAlgorithm { + Ed25519 = 'ed25519', + Secp256K1 = 'secp256k1' +} + +const keyEncoder = new KeyEncoder('secp256k1'); + +const mapSignatureAlgorithmToKeyAlgorithm: Record< + SignatureAlgorithm, + KeyAlgorithm +> = { + [SignatureAlgorithm.Ed25519]: KeyAlgorithm.ED25519, + [SignatureAlgorithm.Secp256K1]: KeyAlgorithm.SECP256K1 +}; + +const ED25519_PEM_SECRET_KEY_TAG = 'PRIVATE KEY'; +const ED25519_PEM_PUBLIC_KEY_TAG = 'PUBLIC KEY'; + +/** @deprecated */ +export interface SignKeyPair { + publicKey: Uint8Array; // Array with 32-byte public key + secretKey: Uint8Array; // Array with 32-byte secret key +} + +/** @deprecated */ +export const getKeysFromHexPrivKey = ( + key: string, + variant: SignatureAlgorithm +): AsymmetricKey => { + const privateKey = PrivateKey.fromHex( + key, + mapSignatureAlgorithmToKeyAlgorithm[variant] + ); + + let keyPair: AsymmetricKey; + + if (variant === SignatureAlgorithm.Secp256K1) { + keyPair = new Secp256K1(privateKey.toBytes(), privateKey.publicKey.bytes()); + return keyPair; + } + + if (variant === SignatureAlgorithm.Ed25519) { + keyPair = new Ed25519({ + publicKey: privateKey.publicKey.bytes(), + secretKey: privateKey.toBytes() + }); + return keyPair; + } + + throw Error('Unsupported key type'); +}; + +/** + * @deprecated + * Reads in a base64 private key, ignoring the header: `-----BEGIN PUBLIC KEY-----` + * and footer: `-----END PUBLIC KEY-----` + * @param {string} content A .pem private key string with a header and footer + * @returns A base64 private key as a `Uint8Array` + * @remarks + * If the provided base64 `content` string does not include a header/footer, + * it will pass through this function unaffected + * @example + * Example PEM: + * + * ``` + * -----BEGIN PUBLIC KEY-----\r\n + * MFYwEAYHKoZIzj0CAQYFK4EEAAoDQgAEj1fgdbpNbt06EY/8C+wbBXq6VvG+vCVD\r\n + * Nl74LvVAmXfpdzCWFKbdrnIlX3EFDxkd9qpk35F/kLcqV3rDn/u3dg==\r\n + * -----END PUBLIC KEY-----\r\n + * ``` + */ +export function readBase64WithPEM(content: string): Uint8Array { + const base64 = content + // there are two kinks of line-endings, CRLF(\r\n) and LF(\n) + // we need handle both + .split(/\r?\n/) + .filter(x => !x.startsWith('---')) + .join('') + // remove the line-endings in the end of content + .trim(); + return Conversions.decodeBase64(base64); +} + +/** @deprecated use {@link PublicKey.verifySignature} instead */ +export const validateSignature = ( + msg: Uint8Array, + signature: Uint8Array, + pk: CLPublicKey +): boolean => { + return pk.pk.verifySignature(msg, signature); +}; + +/** @deprecated use {@link PrivateKey} and {@link PublicKey} instead */ +export abstract class AsymmetricKey { + public readonly publicKey: CLPublicKey; + public readonly privateKey: Uint8Array; + protected readonly _privateKey: PrivateKey; + public readonly signatureAlgorithm: SignatureAlgorithm; + + /** + * @deprecated use {@link PrivateKey} and {@link PublicKey} instead + * Constructs an `AsymmetricKey` inherited object + * @param {Uint8Array} publicKey An account's public key as a byte array + * @param {Uint8Array} privateKey An account's private key as a byte array + * @param {SignatureAlgorithm} signatureAlgorithm The signature algorithm of the key. Currently supported are Ed25519 and Secp256k1 + */ + constructor( + publicKey: Uint8Array, + privateKey: Uint8Array, + signatureAlgorithm: SignatureAlgorithm + ) { + this.publicKey = new CLPublicKey(publicKey, signatureAlgorithm); + this.privateKey = privateKey; + this._privateKey = PrivateKey.fromHex( + Conversions.encodeBase16(privateKey), + mapSignatureAlgorithmToKeyAlgorithm[signatureAlgorithm] + ); + this.signatureAlgorithm = signatureAlgorithm; + } + + /** + * @deprecated use {@link PublicKey}.accountHash().toBytes() instead + * Computes the blake2b account hash of the public key + * @returns The account hash as a byte array + */ + public accountHash(): Uint8Array { + return this.publicKey.pk.accountHash().toBytes(); + } + + /** + * @deprecated use {@link PublicKey}.toHex() instead + * Gets the hexadecimal public key of the account + * @param {boolean} checksummed Indicates whether the public key should be checksummed, default: `true` + * @returns The public key of the `AsymmetricKey` as a hexadecimal string + */ + public accountHex(checksummed = true): string { + return this.publicKey.pk.toHex(checksummed); + } + + /** + * @deprecated + * Inserts the provided `content` and `tag` into a .pem compliant string + * @param tag The tag inserted on the END line + * @param content The base-64 PEM compliant private key + */ + protected toPem(tag: string, content: string) { + // prettier-ignore + return `-----BEGIN ${tag}-----\n` + + `${content}\n` + + `-----END ${tag}-----\n`; + } + + /** + * @deprecated + * Export the public key encoded as a .pem + */ + public abstract exportPublicKeyInPem(): string; + + /** + * @deprecated use {@link PrivateKey.toPem} + * Export the private key encoded as a .pem + */ + public abstract exportPrivateKeyInPem(): string; + + /** + * @deprecated use {@link PrivateKey.sign} + * Sign a message using this `AsymmetricKey`'s private key + * @param {Uint8Array} msg The message to be signed, as a byte array + * @returns A byte array containing the signed message + */ + public abstract sign(msg: Uint8Array): Uint8Array; + + /** + * @deprecated use {@link PublicKey.verifySignature} + * Validate the signature by comparing it to the provided message + * @param {Uint8Array} signature The signature as a byte array + * @param {Uint8Array} msg The original message to be validated + * @returns `true` if the signature is valid, `false` otherwise + */ + public abstract verify(signature: Uint8Array, msg: Uint8Array): boolean; +} + +/** + * @deprecated + * Ed25519 variant of `AsymmetricKey` + * @remarks + * Based on SignatureAlgorithm.scala + * @see [Documentation](https://docs.casper.network/concepts/accounts-and-keys/#eddsa-keys) + */ +export class Ed25519 extends AsymmetricKey { + /** + * @deprecated + * Constructs a new Ed25519 object from a `SignKeyPair` + * @param {SignKeyPair} keyPair An object containing the keys "publicKey" and "secretKey" with corresponding `ByteArray` values + */ + constructor(keyPair: SignKeyPair) { + if (keyPair.secretKey.length != 32) { + console.warn( + `You're using private key from old version, please use newly formatted key with 32 bytes length.` + ); + } + + super( + keyPair.publicKey, + Ed25519.parsePrivateKey(keyPair.secretKey), + SignatureAlgorithm.Ed25519 + ); + } + + /** + * @deprecated use {@link PrivateKey}.generate(KeyAlgorithm.ED25519) + * Generates a new Ed25519 key pair + * @returns A new `Ed25519` object + */ + public static new() { + const privateKey = PrivateKey.generate(KeyAlgorithm.ED25519); + const publicKey = privateKey.publicKey; + return new Ed25519({ + secretKey: privateKey.toBytes(), + publicKey: publicKey.bytes() + }); + } + + /** + * @deprecated use {@link PublicKey}.accountHash().toHex() + * Generate the accountHex for the Ed25519 public key + * @param publicKey + */ + public static accountHex(publicKey: Uint8Array): string { + return PublicKey.fromBytes(publicKey) + .result.accountHash() + .toHex(); + } + + /** + * @deprecated + * Parse the key pair from a public key file and the corresponding private key file + * @param {string} publicKeyPath Path of public key file + * @param {string} privateKeyPath Path of private key file + * @returns A new `AsymmetricKey` + */ + public static parseKeyFiles( + publicKeyPath: string, + privateKeyPath: string + ): AsymmetricKey { + const publicKey = Ed25519.parsePublicKeyFile(publicKeyPath); + const privateKey = Ed25519.parsePrivateKeyFile(privateKeyPath); + return new Ed25519({ + publicKey, + secretKey: privateKey + }); + } + + /** + * @deprecated use {@link PublicKey}.accountHash().toBytes() instead + * Generates the account hash of a Ed25519 public key + * @param {Uint8Array} publicKey An Ed25519 public key + * @returns The blake2b account hash of the public key + */ + public static accountHash(publicKey: Uint8Array): Uint8Array { + return PublicKey.fromBytes(publicKey) + .result.accountHash() + .toBytes(); + } + + /** + * @deprecated + * Construct a keypair from a public key and corresponding private key + * @param {Uint8Array} publicKey The public key of an Ed25519 account + * @param {Uint8Array} privateKey The private key of the same Ed25519 account + * @returns A new `Ed25519` keypair + */ + public static parseKeyPair( + publicKey: Uint8Array, + privateKey: Uint8Array + ): Ed25519 { + const keyPair = new Ed25519({ + publicKey: PublicKey.fromBytes(publicKey).result.bytes(), + secretKey: PrivateKey.fromHex( + Conversions.encodeBase16(privateKey), + KeyAlgorithm.ED25519 + ).toBytes() + }); + + return keyPair; + } + + /** @deprecated */ + public static parsePrivateKeyFile(path: string): Uint8Array { + return Ed25519.parsePrivateKey(Ed25519.readBase64File(path)); + } + + /** + * @deprecated + * Parses a file containing an Ed25519 public key + * @param {string} path The path to the public key file + * @returns A `Uint8Array` typed representation of the public key + * @see {@link Ed25519.parsePublicKey} + */ + public static parsePublicKeyFile(path: string): Uint8Array { + return Ed25519.parsePublicKey(Ed25519.readBase64File(path)); + } + + /** + * @deprecated + * Parses a byte array containing an Ed25519 private key + * @param {Uint8Array} bytes A private key as a byte array + * @returns A validated byte array containing the provided Ed25519 private key + * @see {@link Ed25519.parseKey} + */ + public static parsePrivateKey(bytes: Uint8Array) { + return Ed25519.parseKey(bytes, 0, 32); + } + + /** + * @deprecated + * Parses a byte array containing an Ed25519 public key + * @param {Uint8Array} bytes A public key in bytes + * @returns A validated byte array containing the provided Ed25519 public key + * @see {@link Ed25519.parseKey} + */ + public static parsePublicKey(bytes: Uint8Array) { + return Ed25519.parseKey(bytes, 32, 64); + } + + /** + * @deprecated + * Calls global {@link readBase64WithPEM} and returns the result + * @param {string} content A .pem private key string with a header and footer + * @returns The result of global `readBase64WithPEM` + * @see {@link readBase64WithPEM} + */ + public static readBase64WithPEM(content: string) { + return readBase64WithPEM(content); + } + + /** + * @deprecated + * Read the Base64 content of a file, ignoring PEM frames + * @param {string} path The path to the PEM file + * @returns The result of {@link Ed25519.readBase64WithPEM} after reading in the content as a `string` with `fs` + */ + private static readBase64File(path: string): Uint8Array { + const content = fs.readFileSync(path).toString(); + return Ed25519.readBase64WithPEM(content); + } + + /** + * @deprecated + * Parses and validates a key in a certain range "from" to "to" + * @param {Uint8Array} bytes The key to be parsed and validated + * @param {number} from The starting index from which to parse the key + * @param {number} to The ending index from which to parse the key + * @returns The parsed key + * @throws `Error` if the key is of an unexpected length + */ + private static parseKey(bytes: Uint8Array, from: number, to: number) { + const len = bytes.length; + // prettier-ignore + const key = + (len === 32) ? bytes : + (len === 64) ? Buffer.from(bytes).slice(from, to) : + (len > 32 && len < 64) ? Buffer.from(bytes).slice(len % 32) : + null; + if (key == null || key.length !== 32) { + throw Error(`Unexpected key length: ${len}`); + } + return key; + } + + /** + * @deprecated + * Convert this instance's private key to PEM format + * @returns A PEM compliant string containing this instance's private key + * @see {@link AsymmetricKey.toPem} + */ + public exportPrivateKeyInPem() { + // prettier-ignore + const derPrefix = Buffer.from([48, 46, 2, 1, 0, 48, 5, 6, 3, 43, 101, 112, 4, 34, 4, 32]); + const encoded = Conversions.encodeBase64( + Buffer.concat([derPrefix, Buffer.from(this.privateKey)]) + ); + + return this.toPem(ED25519_PEM_SECRET_KEY_TAG, encoded); + } + + /** + * @deprecated + * Convert this instance's public key to PEM format + * @returns A PEM compliant string containing this instance's public key + * @see {@link AsymmetricKey.toPem} + */ + public exportPublicKeyInPem() { + // prettier-ignore + const derPrefix = Buffer.from([48, 42, 48, 5, 6, 3, 43, 101, 112, 3, 33, 0]); + const encoded = Conversions.encodeBase64( + Buffer.concat([derPrefix, Buffer.from(this.publicKey.bytes())]) + ); + return this.toPem(ED25519_PEM_PUBLIC_KEY_TAG, encoded); + } + + /** + * @deprecated + * Sign a message by using this instance's keypair + * @param {Uint8Array} msg The message to be signed, as a byte array + * @returns `Uint8Array` typed signature of the provided `msg` + */ + public sign(msg: Uint8Array): Uint8Array { + return this._privateKey.sign(msg); + } + + /** + * @deprecated + * Verifies a signature given the signature and the original message + * @param {Uint8Array} signature The signed message as a byte array + * @param {Uint8Array} msg The original message as a byte array + * @returns 'true' if the message if valid, `false` otherwise + */ + public verify(signature: Uint8Array, msg: Uint8Array) { + return this.publicKey.pk.verifySignature(msg, signature); + } + + /** + * @deprecated + * Derive a public key from private key or seed phrase + * @param {Uint8Array} privateKey The private key or seed phrase from which to derive the public key + * @returns A `Uint8Array` public key generated deterministically from the provided private key or seed phrase + * @remarks Both secret keys and seed phrases may be used to derive the public key + */ + public static privateToPublicKey(privateKey: Uint8Array): Uint8Array { + return PrivateKey.fromHex( + Conversions.encodeBase16(privateKey), + KeyAlgorithm.ED25519 + ).publicKey.bytes(); + } + + /** + * @deprecated + * Restore Ed25519 keyPair from private key file + * @param {string} privateKeyPath The path to the private key file + * @returns An Ed25519 `AsymmetricKey` + * @see {@link Ed25519.parsePrivateKeyFile} + * @see {@link Ed25519.privateToPublicKey} + * @see {@link Ed25519.parseKeyPair} + */ + public static loadKeyPairFromPrivateFile(privateKeyPath: string) { + const privateKey = Ed25519.parsePrivateKeyFile(privateKeyPath); + const publicKey = Ed25519.privateToPublicKey(privateKey); + return Ed25519.parseKeyPair(publicKey, privateKey); + } +} + +/** + * @deprecated + * Secp256k1 variant of `AsymmetricKey` + * @privateRemarks + * Orignated from [Secp256k1](https://en.bitcoin.it/wiki/Secp256k1) to support Ethereum keys on the Casper. + * @see [Documentation](https://docs.casper.network/concepts/accounts-and-keys/#ethereum-keys) + */ +export class Secp256K1 extends AsymmetricKey { + /** + * @deprecated + * Constructs a new Secp256K1 object from a public key and a private key + * @param {Uint8Array} publicKey A secp256k1 public key + * @param {Uint8Array} privateKey A secp256k1 private key + */ + constructor(publicKey: Uint8Array, privateKey: Uint8Array) { + super(publicKey, privateKey, SignatureAlgorithm.Secp256K1); + } + + /** + * @deprecated + * Generate a new pseudorandom Secp256k1 key pair + * @returns A new `Secp256K1` object + */ + public static new() { + const privateKey = PrivateKey.generate(KeyAlgorithm.SECP256K1); + const publicKey = privateKey.publicKey; + return new Secp256K1(publicKey.bytes(), privateKey.toBytes()); + } + + /** + * @deprecated + * Parse the key pair from a public key file and the corresponding private key file + * @param {string} publicKeyPath Path of public key file + * @param {string} privateKeyPath Path of private key file + * @returns A new `Secp256K1` object + */ + public static parseKeyFiles( + publicKeyPath: string, + privateKeyPath: string + ): AsymmetricKey { + const publicKey = Secp256K1.parsePublicKeyFile(publicKeyPath); + const privateKey = Secp256K1.parsePrivateKeyFile(privateKeyPath); + return new Secp256K1(publicKey, privateKey); + } + + /** + * @deprecated use {@link PublicKey}.accountHash().toBytes() instead + * Generates the account hash of a secp256k1 public key + * @param {Uint8Array} publicKey A secp256k1 public key + * @returns The blake2b account hash of the public key + */ + public static accountHash(publicKey: Uint8Array): Uint8Array { + return PublicKey.fromBytes(publicKey).result.bytes(); + } + + /** + * @deprecated + * Converts a `Uint8Array` public key to hexadecimal format + * @param publicKey + * @remarks + * The returned public key hex will be prefixed with a "02" to indicate that it is of the secp256k1 variety + */ + public static accountHex(publicKey: Uint8Array): string { + return PublicKey.fromBytes(publicKey).result.toHex(); + } + + /** + * @deprecated + * Construct a keypair from a public key and corresponding private key + * @param {Uint8Array} publicKey The public key of a secp256k1 account + * @param {Uint8Array} privateKey The private key of the same secp256k1 account + * @returns A new `AsymmetricKey` keypair + */ + public static parseKeyPair( + publicKey: Uint8Array, + privateKey: Uint8Array, + originalFormat: 'raw' | 'der' + ): AsymmetricKey { + const publ = Secp256K1.parsePublicKey(publicKey, originalFormat); + const priv = Secp256K1.parsePrivateKey(privateKey, originalFormat); + // nacl expects that the private key will contain both. + return new Secp256K1(publ, priv); + } + + /** + * @deprecated + * Parses a file containing a secp256k1 private key + * @param {string} path The path to the private key file + * @returns A `Uint8Array` typed representation of the private key + * @see {@link Secp256K1.parsePrivateKey} + */ + public static parsePrivateKeyFile(path: string): Uint8Array { + return Secp256K1.parsePrivateKey(Secp256K1.readBase64File(path)); + } + + /** + * @deprecated + * Parses a file containing a secp256k1 public key + * @param {string} path The path to the public key file + * @returns A `Uint8Array` typed representation of the private key + * @see {@link Secp256K1.parsePublicKey} + */ + public static parsePublicKeyFile(path: string): Uint8Array { + return Secp256K1.parsePublicKey(Secp256K1.readBase64File(path)); + } + + /** + * @deprecated + * Parses a byte array containing an Ed25519 public key + * @param {Uint8Array} bytes A public key in bytes + * @param {string} [originalFormat=der] The original format of the private key. + * Options are "der" or "raw", meaning "derived" or "raw", indicating a seed phrase and + * a raw private key respectively. + * @returns A validated byte array containing the provided Ed25519 public key + * @privateRemarks Validate that "der" means derived and "raw" means a raw public key + */ + public static parsePublicKey( + bytes: Uint8Array, + originalFormat: 'der' | 'raw' = 'der' + ) { + let rawKeyHex: string; + if (originalFormat === 'der') { + rawKeyHex = keyEncoder.encodePublic(Buffer.from(bytes), 'der', 'raw'); + } else { + rawKeyHex = Conversions.encodeBase16(bytes); + } + + const publicKey = Uint8Array.from(Buffer.from(rawKeyHex, 'hex')); + + return publicKey; + } + + /** + * @deprecated + * Parses a byte array containing a secp256k1 private key + * @param {Uint8Array} bytes A private key as a byte array + * @param {string} [originalFormat=der] The original format of the private key. + * Options are "der" or "raw", meaning "derived" or "raw", indicating a seed phrase and + * a raw private key respectively. + * @returns A validated byte array containing the provided secp256k1 private key + * @privateRemarks Validate that "der" means derived and "raw" means a raw private key + */ + public static parsePrivateKey( + bytes: Uint8Array, + originalFormat: 'der' | 'raw' = 'der' + ) { + let rawKeyHex: string; + if (originalFormat === 'der') { + rawKeyHex = keyEncoder.encodePrivate(Buffer.from(bytes), 'der', 'raw'); + } else { + rawKeyHex = Conversions.encodeBase16(bytes); + } + + const privateKey = Buffer.from(rawKeyHex, 'hex'); + return privateKey; + } + + /** + * @deprecated + * Calls global {@link readBase64WithPEM} and returns the result + * @param {string} content A .pem private key string with a header and footer + * @returns The result of global `readBase64WithPEM` + * @see {@link readBase64WithPEM} + */ + public static readBase64WithPEM(content: string) { + return readBase64WithPEM(content); + } + + /** + * @deprecated + * Read the Base64 content of a file, ignoring PEM frames + * @param {string} path The path to the PEM file + * @returns The result of {@link Secp256K1.readBase64WithPEM} after reading in the content as a `string` with `fs` + */ + private static readBase64File(path: string): Uint8Array { + const content = fs.readFileSync(path).toString(); + return Secp256K1.readBase64WithPEM(content); + } + + /** + * @deprecated + * Convert this instance's private key to PEM format + * @returns A PEM compliant string containing this instance's private key + */ + public exportPrivateKeyInPem(): string { + return keyEncoder.encodePrivate( + Conversions.encodeBase16(this.privateKey), + 'raw', + 'pem' + ); + } + + /** + * @deprecated + * Convert this instance's public key to PEM format + * @returns A PEM compliant string containing this instance's public key + */ + public exportPublicKeyInPem(): string { + return keyEncoder.encodePublic( + Conversions.encodeBase16(this.publicKey.bytes()), + 'raw', + 'pem' + ); + } + + /** + * @deprecated + * Sign a message by using this instance's keypair + * @param {Uint8Array} msg The message to be signed, as a byte array + * @returns `Uint8Array` typed signature of the provided `msg` + * @see [secp256k1.ecdsaSign](https://github.com/cryptocoinjs/secp256k1-node/blob/HEAD/API.md#ecdsasignmessage-uint8array-privatekey-uint8array--data-noncefn---data-uint8array-noncefn-message-uint8array-privatekey-uint8array-algo-null-data-uint8array-counter-number--uint8array----output-uint8array--len-number--uint8array--signature-uint8array-recid-number-) + */ + public sign(msg: Uint8Array): Uint8Array { + return this._privateKey.sign(msg); + } + + /** + * @deprecated + * Verifies a signature given the signature and the original message + * @param {Uint8Array} signature The signed message as a byte array + * @param {Uint8Array} msg The original message as a byte array + * @see [secp256k1.ecdsaVerify](https://github.com/cryptocoinjs/secp256k1-node/blob/HEAD/API.md#ecdsaverifysignature-uint8array-message-uint8array-publickey-uint8array-boolean) + * @returns 'true' if the message if valid, `false` otherwise + * @privateRemarks Need to document return and return type + */ + public verify(signature: Uint8Array, msg: Uint8Array) { + return this.publicKey.pk.verifySignature(msg, signature); + } + + /** + * @deprecated + * Derive a public key from private key + * @param {Uint8Array} privateKey The private key from which to derive the public key + * @returns A `Uint8Array` public key generated deterministically from the provided private key + * @see [secp256k1.publicKeyCreate](https://github.com/cryptocoinjs/secp256k1-node/blob/HEAD/API.md#publickeycreateprivatekey-uint8array-compressed-boolean--true-output-uint8array--len-number--uint8array--len--new-uint8arraylen-uint8array) + */ + public static privateToPublicKey(privateKey: Uint8Array): Uint8Array { + return PrivateKey.fromHex( + Conversions.encodeBase16(privateKey), + KeyAlgorithm.SECP256K1 + ).publicKey.bytes(); + } + + /** + * @deprecated + * Restore secp256k1 keyPair from private key file + * @param {string} privateKeyPath The path to the private key file + * @returns A secp256k1 `AsymmetricKey` + * @see {@link Secp256K1.parsePrivateKeyFile} + * @see {@link Secp256K1.privateToPublicKey} + * @see {@link Secp256K1.parseKeyPair} + */ + public static loadKeyPairFromPrivateFile(privateKeyPath: string) { + const privateKey = Secp256K1.parsePrivateKeyFile(privateKeyPath); + const publicKey = Secp256K1.privateToPublicKey(privateKey); + return Secp256K1.parseKeyPair(publicKey, privateKey, 'raw'); + } +} diff --git a/src/utils/index.ts b/src/utils/index.ts index eb8e7604d..d5d023fb1 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,5 +1,11 @@ +import * as Keys from './deprecated-keys'; +import * as DeployUtil from './deprecated-deploy-utils'; + export * from './cspr-transfer'; export * from './auction-manager'; export * from './constants'; export * from './cep-18-transfer'; export * from './cep-nft-transfer'; +export * from './deprecated-clPublicKey'; + +export { Keys, DeployUtil };