Skip to content
This repository has been archived by the owner on Aug 2, 2022. It is now read-only.

eosjs-ecc methods and migration (release/21.0.x branch) #709

Merged
merged 12 commits into from
Apr 1, 2020
25 changes: 22 additions & 3 deletions src/PrivateKey.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { BNInput, ec as EC } from 'elliptic';
import {
Key,
KeyType,
privateKeyToLegacyString,
privateKeyToString,
stringToPrivateKey,
} from './eosjs-numeric';
Expand Down Expand Up @@ -36,6 +37,10 @@ export class PrivateKey {
return this.ec.keyFromPrivate(this.key.data);
}

public toLegacyString(): string {
return privateKeyToLegacyString(this.key);
}

/** Export private key as EOSIO-format private key */
public toString(): string {
return privateKeyToString(this.key);
Expand All @@ -52,16 +57,19 @@ export class PrivateKey {
return PublicKey.fromElliptic(ellipticPrivateKey, this.getType(), this.ec);
}

/** Sign a message digest with private key */
public sign(digest: BNInput): Signature {
/** Sign a message or hashed message digest with private key */
public sign(data: BNInput, shouldHash: boolean = true, encoding: string = 'utf8'): Signature {
if (shouldHash) {
data = this.ec.hash().update(data, encoding).digest();
}
let tries = 0;
let signature: Signature;
const isCanonical = (sigData: Uint8Array) =>
!(sigData[1] & 0x80) && !(sigData[1] === 0 && !(sigData[2] & 0x80))
&& !(sigData[33] & 0x80) && !(sigData[33] === 0 && !(sigData[34] & 0x80));
const constructSignature = (options: EC.SignOptions) => {
const ellipticPrivateKey = this.toElliptic();
const ellipticSignature = ellipticPrivateKey.sign(digest, options);
const ellipticSignature = ellipticPrivateKey.sign(data, options);
return Signature.fromElliptic(ellipticSignature, this.getType(), this.ec);
};

Expand All @@ -74,4 +82,15 @@ export class PrivateKey {
}
return signature;
}

/** Validate a private key */
public isValid(): boolean {
try {
const ellipticPrivateKey = this.toElliptic();
const validationObj = ellipticPrivateKey.validate();
return validationObj.result;
} catch {
return false;
}
}
}
22 changes: 16 additions & 6 deletions src/PublicKey.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { BNInput, ec as EC } from 'elliptic';
import { ec as EC } from 'elliptic';
import {
Key,
KeyType,
publicKeyToLegacyString,
publicKeyToString,
stringToPublicKey,
} from './eosjs-numeric';
import { constructElliptic, PrivateKey, Signature } from './eosjs-key-conversions';
import { constructElliptic } from './eosjs-key-conversions';

/** Represents/stores a public key and provides easy conversion for use with `elliptic` lib */
export class PublicKey {
Expand Down Expand Up @@ -38,6 +39,11 @@ export class PublicKey {
return publicKeyToString(this.key);
}

/** Export public key as Legacy EOSIO-format public key */
public toLegacyString(): string {
return publicKeyToLegacyString(this.key);
}

/** Export public key as `elliptic`-format public key */
public toElliptic(): EC.KeyPair {
return this.ec.keyPair({
Expand All @@ -51,9 +57,13 @@ export class PublicKey {
}

/** Validate a public key */
public validate(): boolean {
const ellipticPublicKey = this.toElliptic();
const validationObj = ellipticPublicKey.validate();
return validationObj.result;
public isValid(): boolean {
try {
const ellipticPublicKey = this.toElliptic();
const validationObj = ellipticPublicKey.validate();
return validationObj.result;
} catch {
return false;
}
}
}
18 changes: 12 additions & 6 deletions src/Signature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,24 @@ export class Signature {
return this.signature.type;
}

/** Verify a signature with a message digest and public key */
public verify(digest: BNInput, publicKey: PublicKey, encoding?: string): boolean {
/** Verify a signature with a message or hashed message digest and public key */
public verify(data: BNInput, publicKey: PublicKey, shouldHash: boolean = true, encoding: string = 'utf8'): boolean {
if (shouldHash) {
data = this.ec.hash().update(data, encoding).digest();
}
const ellipticSignature = this.toElliptic();
const ellipticPublicKey = publicKey.toElliptic();
return this.ec.verify(digest, ellipticSignature, ellipticPublicKey, encoding);
return this.ec.verify(data, ellipticSignature, ellipticPublicKey, encoding);
}

/** Recover a public key from a message digest and signature */
public recoverPublicKey(digest: BNInput, encoding?: string): PublicKey {
/** Recover a public key from a message or hashed message digest and signature */
public recover(data: BNInput, shouldHash: boolean = true, encoding: string = 'utf8'): PublicKey {
if (shouldHash) {
data = this.ec.hash().update(data, encoding).digest();
}
const ellipticSignature = this.toElliptic();
const recoveredPublicKey = this.ec.recoverPubKey(
digest,
data,
ellipticSignature,
ellipticSignature.recoveryParam,
encoding
Expand Down
86 changes: 86 additions & 0 deletions src/eosjs-ecc-migration.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import {PrivateKey, PublicKey, Signature} from './eosjs-jssig';
import {generateKeyPair} from './eosjs-key-conversions';
import {KeyType} from './eosjs-numeric';

export const ecc = {
initialize: () => console.error('Method deprecated'),
unsafeRandomKey: () => console.error('Method deprecated'),
randomKey: (cpuEntropyBits?: number): Promise<string> => {
if (cpuEntropyBits !== undefined) {
console.warn('Argument `cpuEntropyBits` is deprecated, ' +
'use the options argument instead');
}

const { privateKey } = generateKeyPair(KeyType.k1);
return Promise.resolve(privateKey.toLegacyString());
},
seedPrivate: () => console.error('Method deprecated'),
privateToPublic: (key: string, pubkey_prefix?: string): string => { // tslint:disable-line
if (pubkey_prefix !== undefined) {
console.warn('Argument `pubkey_prefix` is deprecated, ' +
'keys prefixed with PUB_K1_/PUB_R1_/PUB_WA_ going forward');
}

const privateKey = PrivateKey.fromString(key);
const publicKey = privateKey.getPublicKey();
return publicKey.toLegacyString();
},
isValidPublic: (pubkey: string, pubkey_prefix?: string): boolean => { // tslint:disable-line
if (pubkey_prefix !== undefined) {
console.warn('Argument `pubkey_prefix` is deprecated, ' +
'keys prefixed with PUB_K1_/PUB_R1_/PUB_WA_ going forward');
}

try {
const publicKey = PublicKey.fromString(pubkey);
return publicKey.isValid();
} catch {
return false;
}
},
isValidPrivate: (wif: string): boolean => {
try {
const privateKey = PrivateKey.fromString(wif);
return privateKey.isValid();
} catch {
return false;
}
},
sign: (data: string|Buffer, privateKey: string|PrivateKey, encoding: string = 'utf8'): string => {
const privKey = typeof privateKey === 'string' ? PrivateKey.fromString(privateKey) : privateKey;
const signature = privKey.sign(data, true, encoding);
return signature.toString();
},
signHash: (dataSha256: string|Buffer, privateKey: string|PrivateKey, encoding: string = 'hex') => {
const privKey = typeof privateKey === 'string' ? PrivateKey.fromString(privateKey) : privateKey;
const signature = privKey.sign(dataSha256, false, encoding);
return signature.toString();
},
verify: (
signature: string, data: string, pubKey: string|PublicKey, encoding: string = 'utf8', hashData: boolean = true
): boolean => {
const publicKey = typeof pubKey === 'string' ? PublicKey.fromString(pubKey) : pubKey;
const sig = Signature.fromString(signature);
return sig.verify(data, publicKey, hashData, encoding);
},
recover: (signature: string, data: string, encoding: string = 'utf8'): string => {
const sig = Signature.fromString(signature);
const publicKey = sig.recover(data, true, encoding);
return publicKey.toLegacyString();
},
recoverHash: (signature: string, dataSha256: string|Buffer, encoding: string = 'hex'): string => {
const sig = Signature.fromString(signature);
const publicKey = sig.recover(dataSha256, false, encoding);
return publicKey.toLegacyString();
},
sha256: (data: string|Buffer, resultEncoding?: string, encoding?: string): string|Buffer => {
if (encoding !== undefined) {
console.warn('Argument `encoding` is deprecated');
}
if (resultEncoding !== undefined) {
console.warn('Argument `resultEncoding` is deprecated');
}

return require('./eosjs-key-conversions').sha256(data);
}
};
27 changes: 27 additions & 0 deletions src/eosjs-key-conversions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import {ec as EC} from 'elliptic';
import * as hash from 'hash.js';
import {KeyType} from './eosjs-numeric';
import { PublicKey } from './PublicKey';
import { PrivateKey } from './PrivateKey';

export { PrivateKey } from './PrivateKey';
export { PublicKey } from './PublicKey';
Expand All @@ -12,3 +15,27 @@ export const constructElliptic = (type: KeyType): EC => {
}
return new EC('p256') as any;
};

export const generateKeyPair = (type: KeyType, options?: EC.GenKeyPairOptions):
{publicKey: PublicKey, privateKey: PrivateKey} => {
if (process.env.EOSJS_KEYGEN_ALLOWED !== 'true') {
throw new Error('Key generation is completely INSECURE in production environments in the browser. ' +
'If you are absolutely certain this does NOT describe your environment, add an environment variable ' +
'`EOSJS_KEYGEN_ALLOWED` set to \'true\'. If this does describe your environment and you add the ' +
'environment variable, YOU DO SO AT YOUR OWN RISK AND THE RISK OF YOUR USERS.');
}
let ec;
if (type === KeyType.k1) {
ec = new EC('secp256k1') as any;
} else {
ec = new EC('p256') as any;
}
const ellipticKeyPair = ec.genKeyPair(options);
const publicKey = PublicKey.fromElliptic(ellipticKeyPair, type, ec);
const privateKey = PrivateKey.fromElliptic(ellipticKeyPair, type, ec);
return {publicKey, privateKey};
};

export const sha256 = (data: string|Buffer) => {
return hash.sha256().update(data).digest();
};
40 changes: 40 additions & 0 deletions src/eosjs-numeric.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
/**
* @module Numeric
*/
import { sha256 } from 'hash.js';

// copyright defined in eosjs/LICENSE.txt

const ripemd160 = require('./ripemd').RIPEMD160.hash as (a: Uint8Array) => ArrayBuffer;
Expand Down Expand Up @@ -327,6 +329,17 @@ export function stringToPublicKey(s: string): Key {
}
}

/** Convert public `key` to legacy string (base-58) form */
export function publicKeyToLegacyString(key: Key) {
if (key.type === KeyType.k1 && key.data.length === publicKeyDataSize) {
return keyToString(key, '', 'EOS');
} else if (key.type === KeyType.r1 || key.type === KeyType.wa) {
throw new Error('Key format not supported in legacy conversion');
} else {
throw new Error('unrecognized public key format');
}
}

/** Convert `key` to string (base-58) form */
export function publicKeyToString(key: Key) {
if (key.type === KeyType.k1 && key.data.length === publicKeyDataSize) {
Expand Down Expand Up @@ -382,6 +395,33 @@ export function stringToPrivateKey(s: string): Key {
}
}

/** Convert private `key` to legacy string (base-58) form */
export function privateKeyToLegacyString(key: Key) {
if (key.type === KeyType.k1 && key.data.length === privateKeyDataSize) {
const whole = [] as number[];
whole.push(128);
key.data.forEach((byte) => whole.push(byte));
const digest = new Uint8Array(
sha256().update(
sha256().update(whole).digest()
).digest()
);

const result = new Uint8Array(privateKeyDataSize + 5);
for (let i = 0; i < whole.length; i++) {
result[i] = whole[i];
}
for (let i = 0; i < 4; i++) {
result[i + whole.length] = digest[i];
}
return binaryToBase58(result);
} else if (key.type === KeyType.r1 || key.type === KeyType.wa) {
throw new Error('Key format not supported in legacy conversion');
} else {
throw new Error('unrecognized public key format');
}
}

/** Convert `key` to string (base-58) form */
export function privateKeyToString(key: Key) {
if (key.type === KeyType.r1) {
Expand Down
Loading