Skip to content
This repository was archived by the owner on Aug 2, 2022. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
},
"devDependencies": {
"@blockone/tslint-config-blockone": "3.0.0",
"@types/elliptic": "^6.4.9",
"@types/jest": "24.0.6",
"@types/node": "11.9.4",
"@types/text-encoding": "0.0.35",
Expand All @@ -42,6 +43,7 @@
"babel-preset-env": "1.7.0",
"babel-preset-stage-1": "6.24.1",
"cypress": "3.1.5",
"elliptic": "^6.5.0",
"jest": "23.5.0",
"jest-fetch-mock": "2.1.1",
"json-loader": "0.5.7",
Expand All @@ -55,6 +57,11 @@
"webpack": "4.29.5",
"webpack-cli": "3.2.3"
},
"resolutions": {
"braces": "2.3.1",
"handlebars": "4.1.2",
"js-yaml": "3.13.1"
},
"jest": {
"automock": false,
"setupFiles": [
Expand Down
47 changes: 43 additions & 4 deletions src/eosjs-numeric.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,11 +124,41 @@ export function signedBinaryToDecimal(bignum: Uint8Array, minDigits = 1) {
return binaryToDecimal(bignum, minDigits);
}

function base58ToBinaryVarSize(s: string) {
const result = [] as number[];
for (let i = 0; i < s.length; ++i) {
let carry = base58Map[s.charCodeAt(i)];
if (carry < 0) {
throw new Error('invalid base-58 value');
}
for (let j = 0; j < result.length; ++j) {
const x = result[j] * 58 + carry;
result[j] = x & 0xff;
carry = x >> 8;
}
if (carry) {
result.push(carry);
}
}
for (const ch of s) {
if (ch === '1') {
result.push(0);
} else {
break;
}
}
result.reverse();
return new Uint8Array(result);
}

/**
* Convert an unsigned base-58 number in `s` to a bignum
* @param size bignum size (bytes)
*/
export function base58ToBinary(size: number, s: string) {
if (!size) {
return base58ToBinaryVarSize(s);
}
const result = new Uint8Array(size);
for (let i = 0; i < s.length; ++i) {
let carry = base58Map[s.charCodeAt(i)];
Expand Down Expand Up @@ -217,6 +247,7 @@ export function base64ToBinary(s: string) {
export enum KeyType {
k1 = 0,
r1 = 1,
wa = 2,
}

/** Public key data size, excluding type field */
Expand Down Expand Up @@ -246,11 +277,11 @@ function digestSuffixRipemd160(data: Uint8Array, suffix: string) {
}

function stringToKey(s: string, type: KeyType, size: number, suffix: string): Key {
const whole = base58ToBinary(size + 4, s);
const result = { type, data: new Uint8Array(whole.buffer, 0, size) };
const whole = base58ToBinary(size ? size + 4 : 0, s);
const result = { type, data: new Uint8Array(whole.buffer, 0, whole.length - 4) };
const digest = new Uint8Array(digestSuffixRipemd160(result.data, suffix));
if (digest[0] !== whole[size + 0] || digest[1] !== whole[size + 1]
|| digest[2] !== whole[size + 2] || digest[3] !== whole[size + 3]) {
if (digest[0] !== whole[whole.length - 4] || digest[1] !== whole[whole.length - 3]
|| digest[2] !== whole[whole.length - 2] || digest[3] !== whole[whole.length - 1]) {
throw new Error('checksum doesn\'t match');
}
return result;
Expand Down Expand Up @@ -289,6 +320,8 @@ export function stringToPublicKey(s: string): Key {
return stringToKey(s.substr(7), KeyType.k1, publicKeyDataSize, 'K1');
} else if (s.substr(0, 7) === 'PUB_R1_') {
return stringToKey(s.substr(7), KeyType.r1, publicKeyDataSize, 'R1');
} else if (s.substr(0, 7) === 'PUB_WA_') {
return stringToKey(s.substr(7), KeyType.wa, 0, 'WA');
} else {
throw new Error('unrecognized public key format');
}
Expand All @@ -300,6 +333,8 @@ export function publicKeyToString(key: Key) {
return keyToString(key, 'K1', 'PUB_K1_');
} else if (key.type === KeyType.r1 && key.data.length === publicKeyDataSize) {
return keyToString(key, 'R1', 'PUB_R1_');
} else if (key.type === KeyType.wa) {
return keyToString(key, 'WA', 'PUB_WA_');
} else {
throw new Error('unrecognized public key format');
}
Expand Down Expand Up @@ -352,6 +387,8 @@ export function stringToSignature(s: string): Key {
return stringToKey(s.substr(7), KeyType.k1, signatureDataSize, 'K1');
} else if (s.substr(0, 7) === 'SIG_R1_') {
return stringToKey(s.substr(7), KeyType.r1, signatureDataSize, 'R1');
} else if (s.substr(0, 7) === 'SIG_WA_') {
return stringToKey(s.substr(7), KeyType.wa, 0, 'WA');
} else {
throw new Error('unrecognized signature format');
}
Expand All @@ -363,6 +400,8 @@ export function signatureToString(signature: Key) {
return keyToString(signature, 'K1', 'SIG_K1_');
} else if (signature.type === KeyType.r1) {
return keyToString(signature, 'R1', 'SIG_R1_');
} else if (signature.type === KeyType.wa) {
return keyToString(signature, 'WA', 'SIG_WA_');
} else {
throw new Error('unrecognized signature format');
}
Expand Down
29 changes: 27 additions & 2 deletions src/eosjs-serialize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,14 @@ export class SerialBuffer { // tslint:disable-line max-classes-per-file
return result;
}

/** Skip `len` bytes */
public skip(len: number) {
if (this.readPos + len > this.length) {
throw new Error('Read past end of buffer');
}
this.readPos += len;
}

/** Append a `uint16` */
public pushUint16(v: number) {
this.push((v >> 0) & 0xff, (v >> 8) & 0xff);
Expand Down Expand Up @@ -489,7 +497,15 @@ export class SerialBuffer { // tslint:disable-line max-classes-per-file
/** Get a public key */
public getPublicKey() {
const type = this.get();
const data = this.getUint8Array(numeric.publicKeyDataSize);
let data: Uint8Array;
if (type === numeric.KeyType.wa) {
const begin = this.readPos;
this.skip(34);
this.skip(this.getVaruint32());
data = new Uint8Array(this.array.buffer, this.array.byteOffset + begin, this.readPos - begin);
} else {
data = this.getUint8Array(numeric.publicKeyDataSize);
}
return numeric.publicKeyToString({ type, data });
}

Expand Down Expand Up @@ -517,7 +533,16 @@ export class SerialBuffer { // tslint:disable-line max-classes-per-file
/** Get a signature */
public getSignature() {
const type = this.get();
const data = this.getUint8Array(numeric.signatureDataSize);
let data: Uint8Array;
if (type === numeric.KeyType.wa) {
const begin = this.readPos;
this.skip(65);
this.skip(this.getVaruint32());
this.skip(this.getVaruint32());
data = new Uint8Array(this.array.buffer, this.array.byteOffset + begin, this.readPos - begin);
} else {
data = this.getUint8Array(numeric.signatureDataSize);
}
return numeric.signatureToString({ type, data });
}
} // SerialBuffer
Expand Down
104 changes: 104 additions & 0 deletions src/eosjs-webauthn-sig.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/**
* @module WebAuthn-Sig
*/
// copyright defined in eosjs/LICENSE.txt

import { SignatureProvider, SignatureProviderArgs } from './eosjs-api-interfaces';
import * as ser from './eosjs-serialize';
import * as numeric from './eosjs-numeric';
import { ec } from 'elliptic';

/** Signs transactions using WebAuthn */
export class WebAuthnSignatureProvider implements SignatureProvider {
/** Map public key to credential ID (hex). User must populate this. */
public keys = new Map<string, string>();

/** Public keys that the `SignatureProvider` holds */
public async getAvailableKeys() {
return Array.from(this.keys.keys());
}

/** Sign a transaction */
public async sign(
{ chainId, requiredKeys, serializedTransaction, serializedContextFreeData }:
SignatureProviderArgs,
) {
const signBuf = new ser.SerialBuffer();
signBuf.pushArray(ser.hexToUint8Array(chainId));
signBuf.pushArray(serializedTransaction);
if (serializedContextFreeData) {
signBuf.pushArray(new Uint8Array(await crypto.subtle.digest('SHA-256', serializedContextFreeData.buffer)));
} else {
signBuf.pushArray(new Uint8Array(32));
}
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', signBuf.asUint8Array().slice().buffer));

const signatures = [] as string[];
for (const key of requiredKeys) {
const id = ser.hexToUint8Array(this.keys.get(key));
const assertion = await (navigator as any).credentials.get({
publicKey: {
timeout: 60000,
allowCredentials: [{
id,
type: 'public-key',
}],
challenge: digest.buffer,
},
});
const e = new ec('p256') as any;
const pubKey = e.keyFromPublic(numeric.stringToPublicKey(key).data.subarray(0, 33)).getPublic();

const fixup = (x: Uint8Array) => {
const a = Array.from(x);
while (a.length < 32) {
a.unshift(0);
}
while (a.length > 32) {
if (a.shift() !== 0) {
throw new Error('Signature has an r or s that is too big');
}
}
return new Uint8Array(a);
};

const der = new ser.SerialBuffer({ array: new Uint8Array(assertion.response.signature) });
if (der.get() !== 0x30) {
throw new Error('Signature missing DER prefix');
}
if (der.get() !== der.array.length - 2) {
throw new Error('Signature has bad length');
}
if (der.get() !== 0x02) {
throw new Error('Signature has bad r marker');
}
const r = fixup(der.getUint8Array(der.get()));
if (der.get() !== 0x02) {
throw new Error('Signature has bad s marker');
}
const s = fixup(der.getUint8Array(der.get()));

const whatItReallySigned = new ser.SerialBuffer();
whatItReallySigned.pushArray(new Uint8Array(assertion.response.authenticatorData));
whatItReallySigned.pushArray(new Uint8Array(
await crypto.subtle.digest('SHA-256', assertion.response.clientDataJSON)));
const hash = new Uint8Array(
await crypto.subtle.digest('SHA-256', whatItReallySigned.asUint8Array().slice()));
const recid = e.getKeyRecoveryParam(hash, new Uint8Array(assertion.response.signature), pubKey);

const sigData = new ser.SerialBuffer();
sigData.push(recid + 27 + 4);
sigData.pushArray(r);
sigData.pushArray(s);
sigData.pushBytes(new Uint8Array(assertion.response.authenticatorData));
sigData.pushBytes(new Uint8Array(assertion.response.clientDataJSON));

const sig = numeric.signatureToString({
type: numeric.KeyType.wa,
data: sigData.asUint8Array().slice(),
});
signatures.push(sig);
}
return { signatures, serializedTransaction, serializedContextFreeData };
}
}
Loading