-
Notifications
You must be signed in to change notification settings - Fork 463
Web Crypto Signature Provider and conversion methods #937
Changes from 19 commits
9b880ef
b8999f3
73e0c62
57ea202
a3c7189
10c4f26
561be1e
adca5aa
1d98c3e
69385c4
3652757
132e8f5
fd0d029
c18433b
9f8eef5
4c1c1f0
93532da
caa965a
3a7cca5
83b272e
8cb9d6b
dedc6e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
import {ec as EC} from 'elliptic'; | ||
import { KeyType } from './eosjs-numeric'; | ||
import { PublicKey } from './PublicKey'; | ||
import { PrivateKey } from './PrivateKey'; | ||
|
||
const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto); | ||
|
||
export const generateKeyPair = ( | ||
type: KeyType, options: { secureEnv?: boolean, ecOptions?: EC.GenKeyPairOptions } = {} | ||
): { publicKey: PublicKey, privateKey: PrivateKey } => { | ||
if (!options.secureEnv) { | ||
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, set `secureEnv` in your ' + | ||
'options to `true`. If this does describe your environment and you set `secureEnv` to `true`, ' + | ||
'YOU DO SO AT YOUR OWN RISK AND THE RISK OF YOUR USERS.'); | ||
} | ||
let ec; | ||
if (type === KeyType.k1) { | ||
ec = new EC('secp256k1'); | ||
} else { | ||
ec = new EC('p256'); | ||
} | ||
const ellipticKeyPair = ec.genKeyPair(options.ecOptions); | ||
const publicKey = PublicKey.fromElliptic(ellipticKeyPair, type, ec); | ||
const privateKey = PrivateKey.fromElliptic(ellipticKeyPair, type, ec); | ||
return {publicKey, privateKey}; | ||
}; | ||
|
||
/** Construct a p256/secp256r1 CryptoKeyPair from Web Crypto | ||
* Note: While creating a key that is not extractable means that it would not be possible | ||
* to convert the private key to string, it is not necessary to have the key extractable | ||
* for the Web Crypto Signature Provider. Additionally, creating a key that is extractable | ||
* introduces security concerns. For this reason, this function only creates CryptoKeyPairs | ||
* where the private key is not extractable and the public key is extractable. | ||
*/ | ||
export const generateWebCryptoKeyPair = async ( | ||
keyUsage: KeyUsage[] = ['sign', 'verify'] | ||
): Promise<CryptoKeyPair> => { | ||
return await crypto.subtle.generateKey( | ||
{ | ||
name: 'ECDSA', | ||
namedCurve: 'P-256' | ||
}, | ||
false, | ||
keyUsage | ||
); | ||
}; |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,8 +5,11 @@ import { | |
publicKeyToLegacyString, | ||
publicKeyToString, | ||
stringToPublicKey, | ||
arrayToString, | ||
stringToArray, | ||
} from './eosjs-numeric'; | ||
import { constructElliptic } from './eosjs-key-conversions'; | ||
|
||
const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto); | ||
|
||
/** Represents/stores a public key and provides easy conversion for use with `elliptic` lib */ | ||
export class PublicKey { | ||
|
@@ -16,7 +19,11 @@ export class PublicKey { | |
public static fromString(publicKeyStr: string, ec?: EC): PublicKey { | ||
const key = stringToPublicKey(publicKeyStr); | ||
if (!ec) { | ||
ec = constructElliptic(key.type); | ||
if (key.type === KeyType.k1) { | ||
ec = new EC('secp256k1'); | ||
} else { | ||
ec = new EC('p256'); | ||
} | ||
} | ||
return new PublicKey(key, ec); | ||
} | ||
|
@@ -26,14 +33,33 @@ export class PublicKey { | |
const x = publicKey.getPublic().getX().toArray('be', 32); | ||
const y = publicKey.getPublic().getY().toArray('be', 32); | ||
if (!ec) { | ||
ec = constructElliptic(keyType); | ||
if (keyType === KeyType.k1) { | ||
ec = new EC('secp256k1'); | ||
} else { | ||
ec = new EC('p256'); | ||
} | ||
} | ||
return new PublicKey({ | ||
type: keyType, | ||
data: new Uint8Array([(y[31] & 1) ? 3 : 2].concat(x)), | ||
}, ec); | ||
} | ||
|
||
/** Instantiate public key from a `CryptoKey`-format public key */ | ||
public static async fromWebCrypto(publicKey: CryptoKey): Promise<PublicKey> { | ||
if (publicKey.extractable === false) { | ||
throw new Error('Crypto Key is not extractable'); | ||
} | ||
const ec = new EC('p256'); | ||
|
||
const extractedArrayBuffer = await crypto.subtle.exportKey('spki', publicKey); | ||
const extractedDecoded = arrayToString(extractedArrayBuffer); | ||
const derHex = Buffer.from(extractedDecoded, 'binary').toString('hex'); | ||
const publicKeyHex = derHex.replace('3059301306072a8648ce3d020106082a8648ce3d030107034200', ''); | ||
const publicKeyEc = ec.keyFromPublic(publicKeyHex, 'hex'); | ||
return PublicKey.fromElliptic(publicKeyEc, KeyType.r1, ec); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just to confirm, could There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Only p256 is supported for the Web Crypto API but it is possible that someone could put a k1 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually, turns out the There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks! This looks more clear too me. |
||
} | ||
|
||
/** Export public key as EOSIO-format public key */ | ||
public toString(): string { | ||
return publicKeyToString(this.key); | ||
|
@@ -51,6 +77,26 @@ export class PublicKey { | |
}); | ||
} | ||
|
||
/** Export public key as `CryptoKey`-format public key */ | ||
public async toWebCrypto(extractable: boolean = false): Promise<CryptoKey> { | ||
const publicKeyEc = this.toElliptic(); | ||
const publicKeyHex = publicKeyEc.getPublic('hex'); | ||
|
||
const derHex = `3059301306072a8648ce3d020106082a8648ce3d030107034200${publicKeyHex}`; | ||
const derBase64 = Buffer.from(derHex, 'hex').toString('binary'); | ||
const spkiArrayBuffer = stringToArray(derBase64); | ||
return await crypto.subtle.importKey( | ||
'spki', | ||
spkiArrayBuffer, | ||
{ | ||
name: 'ECDSA', | ||
namedCurve: 'P-256' | ||
}, | ||
extractable, | ||
['verify'] | ||
); | ||
} | ||
|
||
/** Get key type from key */ | ||
public getType(): KeyType { | ||
return this.key.type; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,13 +1,15 @@ | ||
import { BNInput, ec as EC } from 'elliptic'; | ||
import BN = require('bn.js'); | ||
|
||
import { | ||
Key, | ||
KeyType, | ||
signatureToString, | ||
stringToSignature, | ||
} from './eosjs-numeric'; | ||
import { constructElliptic, PublicKey } from './eosjs-key-conversions'; | ||
import { PublicKey } from './PublicKey'; | ||
|
||
const crypto = (typeof(window) !== 'undefined' ? window.crypto : require('crypto').webcrypto); | ||
type WebCryptoSignatureData = Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array | DataView | ArrayBuffer; | ||
|
||
/** Represents/stores a Signature and provides easy conversion for use with `elliptic` lib */ | ||
export class Signature { | ||
|
@@ -17,13 +19,17 @@ export class Signature { | |
public static fromString(sig: string, ec?: EC): Signature { | ||
const signature = stringToSignature(sig); | ||
if (!ec) { | ||
ec = constructElliptic(signature.type); | ||
if (signature.type === KeyType.k1) { | ||
ec = new EC('secp256k1'); | ||
} else { | ||
ec = new EC('p256'); | ||
} | ||
} | ||
return new Signature(signature, ec); | ||
} | ||
|
||
/** Instantiate Signature from an `elliptic`-format Signature */ | ||
public static fromElliptic(ellipticSig: EC.Signature, keyType: KeyType, ec?: EC): Signature { | ||
public static fromElliptic(ellipticSig: EC.Signature | {r: BN, s: BN, recoveryParam: number | null}, keyType: KeyType, ec?: EC): Signature { | ||
const r = ellipticSig.r.toArray('be', 32); | ||
const s = ellipticSig.s.toArray('be', 32); | ||
let eosioRecoveryParam; | ||
|
@@ -37,14 +43,50 @@ export class Signature { | |
} | ||
const sigData = new Uint8Array([eosioRecoveryParam].concat(r, s)); | ||
if (!ec) { | ||
ec = constructElliptic(keyType); | ||
if (keyType === KeyType.k1) { | ||
ec = new EC('secp256k1'); | ||
} else { | ||
ec = new EC('p256'); | ||
} | ||
} | ||
return new Signature({ | ||
type: keyType, | ||
data: sigData, | ||
}, ec); | ||
} | ||
|
||
/** Instantiate Signature from a Web Crypto Signature */ | ||
public static async fromWebCrypto(data: WebCryptoSignatureData, webCryptoSig: ArrayBuffer, publicKey: PublicKey) { | ||
const ec = new EC('p256'); | ||
|
||
const hash = await crypto.subtle.digest('SHA-256', data); | ||
const r = new BN(new Uint8Array(webCryptoSig.slice(0, 32)), 32); | ||
let s = new BN(new Uint8Array(webCryptoSig.slice(32)), 32); | ||
const halforder = ec.curve.n.ushrn(1); // shift right 1 bit -- division by two | ||
if (s.ucmp(halforder) === 1) { | ||
s = ec.curve.n.sub(s); | ||
} | ||
const recoveryParam = this.getRecoveryParam(Buffer.from(hash), {r, s}, publicKey.toString(), ec); | ||
return Signature.fromElliptic({r, s, recoveryParam}, KeyType.r1, ec); | ||
} | ||
|
||
/** Replaced version of getRecoveryParam from `elliptic` library */ | ||
private static getRecoveryParam = (digest: BNInput, signature: EC.SignatureOptions, publicKey: string, ec: EC) => { | ||
let recoveredKey: any; | ||
for (let i = 0; i < 4; i++) { | ||
try { | ||
const keyPair = ec.recoverPubKey(digest, signature, i); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Name There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Actually the |
||
recoveredKey = PublicKey.fromElliptic(ec.keyFromPublic(keyPair), KeyType.r1, ec).toString(); | ||
} catch (e) { | ||
continue; | ||
} | ||
if (recoveredKey === publicKey) { | ||
return i; | ||
} | ||
} | ||
throw new Error('Unable to find valid recovery factor'); | ||
}; | ||
|
||
/** Export Signature as `elliptic`-format Signature | ||
* NOTE: This isn't an actual elliptic-format Signature, as ec.Signature is not exported by the library. | ||
* That's also why the return type is `any`. We're *actually* returning an object with the 3 params | ||
|
@@ -98,6 +140,22 @@ export class Signature { | |
return this.ec.verify(data, ellipticSignature, ellipticPublicKey, encoding); | ||
} | ||
|
||
/** Verify a Web Crypto signature with data (that matches types) and public key */ | ||
public async webCryptoVerify(data: WebCryptoSignatureData, webCryptoSig: ArrayBuffer, publicKey: PublicKey): Promise<boolean> { | ||
const webCryptoPub = await publicKey.toWebCrypto(); | ||
return await crypto.subtle.verify( | ||
{ | ||
name: 'ECDSA', | ||
hash: { | ||
name: 'SHA-256' | ||
} | ||
}, | ||
webCryptoPub, | ||
webCryptoSig, | ||
data | ||
); | ||
} | ||
|
||
/** Recover a public key from a message or hashed message digest and signature */ | ||
public recover(data: BNInput, shouldHash: boolean = true, encoding: BufferEncoding = 'utf8'): PublicKey { | ||
if (shouldHash) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please remove this section and provide a stub in its place and once the documentation is created and signed off by Product and Developer Relations - they can PR those changes in via a separate PR. Other than that this PR is now ready to merge per BLU-20451