Skip to content

Commit

Permalink
Merge pull request #1355 from LimeChain/secp256k1-support-new
Browse files Browse the repository at this point in the history
Add Secp256k1 support
  • Loading branch information
andy-haynes authored Jul 22, 2024
2 parents 8888668 + ac393c7 commit cc93136
Show file tree
Hide file tree
Showing 24 changed files with 6,324 additions and 4,597 deletions.
12 changes: 12 additions & 0 deletions .changeset/wet-seals-end.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
"@near-js/crypto": minor
"@near-js/accounts": patch
"@near-js/biometric-ed25519": patch
"@near-js/keystores": patch
"near-api-js": patch
"@near-js/signers": patch
"@near-js/transactions": patch
"@near-js/wallet-account": patch
---

Add Secp256k1 support
21 changes: 21 additions & 0 deletions packages/accounts/test/account.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { TypedError } = require('@near-js/types');
const fs = require('fs');

const { Account, Contract } = require('../lib');
const { KeyType } = require( '@near-js/crypto' );
const testUtils = require('./test-utils');

let nearjs;
Expand Down Expand Up @@ -38,6 +39,26 @@ test('create account and then view account returns the created account', async (
expect(state.amount).toEqual(newAmount.toString());
});

test('create account with a secp256k1 key and then view account returns the created account', async () => {
const newAccountName = testUtils.generateUniqueString('test');
const newAccountPublicKey = 'secp256k1:45KcWwYt6MYRnnWFSxyQVkuu9suAzxoSkUMEnFNBi9kDayTo5YPUaqMWUrf7YHUDNMMj3w75vKuvfAMgfiFXBy28';
const { amount } = await workingAccount.state();
const newAmount = BigInt(amount) / BigInt(10);
await nearjs.accountCreator.masterAccount.createAccount(newAccountName, newAccountPublicKey, newAmount);
const newAccount = new Account(nearjs.connection, newAccountName);
const state = await newAccount.state();
expect(state.amount).toEqual(newAmount.toString());
});

test('Secp256k1 send money', async() => {
const sender = await testUtils.createAccount(nearjs, KeyType.SECP256K1);
const receiver = await testUtils.createAccount(nearjs, KeyType.SECP256K1);
const { amount: receiverAmount } = await receiver.state();
await sender.sendMoney(receiver.accountId, BigInt(10000));
const state = await receiver.state();
expect(state.amount).toEqual((BigInt(receiverAmount) + BigInt(10000)).toString());
});

test('send money', async() => {
const sender = await testUtils.createAccount(nearjs);
const receiver = await testUtils.createAccount(nearjs);
Expand Down
6 changes: 3 additions & 3 deletions packages/accounts/test/test-utils.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const { KeyPair } = require('@near-js/crypto');
const { KeyPair, KeyType } = require('@near-js/crypto');
const { InMemoryKeyStore } = require('@near-js/keystores');
const fs = require('fs').promises;
const path = require('path');
Expand Down Expand Up @@ -81,9 +81,9 @@ function generateUniqueString(prefix) {
return result + '.test.near';
}

async function createAccount({ accountCreator, connection }) {
async function createAccount({ accountCreator, connection }, keyType = KeyType.ED25519) {
const newAccountName = generateUniqueString('test');
const newPublicKey = await connection.signer.createKey(newAccountName, networkId);
const newPublicKey = await connection.signer.createKey(newAccountName, networkId, keyType);
await accountCreator.createAccount(newAccountName, newPublicKey);
return new Account(connection, newAccountName);
}
Expand Down
7 changes: 4 additions & 3 deletions packages/biometric-ed25519/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
} from './utils';
import { Fido2 } from './fido2';
import { AssertionResponse } from './index.d';
import { KeyPairString } from '@near-js/crypto';

const CHALLENGE_TIMEOUT_MS = 90 * 1000;
const RP_NAME = 'NEAR_API_JS_WEBAUTHN';
Expand Down Expand Up @@ -86,7 +87,7 @@ export const createKey = async (username: string): Promise<KeyPair> => {
const publicKeyBytes = get64BytePublicKeyFromPEM(publicKey);
const secretKey = sha256.create().update(Buffer.from(publicKeyBytes)).digest();
const pubKey = ed25519.getPublicKey(secretKey);
return KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secretKey), Buffer.from(pubKey)]))));
return KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secretKey), Buffer.from(pubKey)]))) as KeyPairString);
});
};

Expand Down Expand Up @@ -129,8 +130,8 @@ export const getKeys = async (username: string): Promise<[KeyPair, KeyPair]> =>
const firstEDPublic = ed25519.getPublicKey(firstEDSecret);
const secondEDSecret = sha256.create().update(Buffer.from(correctPKs[1])).digest();
const secondEDPublic = ed25519.getPublicKey(secondEDSecret);
const firstKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(firstEDSecret), Buffer.from(firstEDPublic)]))));
const secondKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secondEDSecret), Buffer.from(secondEDPublic)]))));
const firstKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(firstEDSecret), Buffer.from(firstEDPublic)]))) as KeyPairString);
const secondKeyPair = KeyPair.fromString(baseEncode(new Uint8Array(Buffer.concat([Buffer.from(secondEDSecret), Buffer.from(secondEDPublic)]))) as KeyPairString);
return [firstKeyPair, secondKeyPair];
});
};
Expand Down
5 changes: 3 additions & 2 deletions packages/crypto/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,10 @@
"dependencies": {
"@near-js/types": "workspace:*",
"@near-js/utils": "workspace:*",
"borsh": "1.0.0",
"@noble/curves": "1.2.0",
"randombytes": "2.1.0"
"borsh": "1.0.0",
"randombytes": "2.1.0",
"secp256k1": "5.0.0"
},
"devDependencies": {
"@types/node": "18.11.18",
Expand Down
5 changes: 4 additions & 1 deletion packages/crypto/src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
/** All supported key types */
export enum KeyType {
ED25519 = 0,
SECP256K1 = 1,
}

export enum KeySize {
SECRET_KEY = 32
SECRET_KEY = 32,
ED25519_PUBLIC_KEY = 32,
SECP256k1_PUBLIC_KEY = 64,
}
3 changes: 2 additions & 1 deletion packages/crypto/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export { KeyType } from './constants';
export { KeyPair } from './key_pair';
export { KeyPair, KeyPairString } from './key_pair';
export { Signature } from './key_pair_base';
export { KeyPairEd25519 } from './key_pair_ed25519';
export { KeyPairSecp256k1 } from './key_pair_secp256k1';
export { PublicKey } from './public_key';
13 changes: 8 additions & 5 deletions packages/crypto/src/key_pair.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { KeyPairBase } from './key_pair_base';
import { KeyPairEd25519 } from './key_pair_ed25519';
import { KeyPairSecp256k1 } from './key_pair_secp256k1';

export type KeyPairString = `ed25519:${string}` | `secp256k1:${string}`;

export abstract class KeyPair extends KeyPairBase {
/**
* @param curve Name of elliptical curve, case-insensitive
* @returns Random KeyPair based on the curve
*/
static fromRandom(curve: string): KeyPair {
static fromRandom(curve: 'ed25519' | 'secp256k1'): KeyPair {
switch (curve.toUpperCase()) {
case 'ED25519': return KeyPairEd25519.fromRandom();
case 'SECP256K1': return KeyPairSecp256k1.fromRandom();
default: throw new Error(`Unknown curve ${curve}`);
}
}
Expand All @@ -18,13 +22,12 @@ export abstract class KeyPair extends KeyPairBase {
* @param encodedKey The encoded key string.
* @returns {KeyPair} The key pair created from the encoded key string.
*/
static fromString(encodedKey: string): KeyPair {
static fromString(encodedKey: KeyPairString): KeyPair {
const parts = encodedKey.split(':');
if (parts.length === 1) {
return new KeyPairEd25519(parts[0]);
} else if (parts.length === 2) {
if (parts.length === 2) {
switch (parts[0].toUpperCase()) {
case 'ED25519': return new KeyPairEd25519(parts[1]);
case 'SECP256K1': return new KeyPairSecp256k1(parts[1]);
default: throw new Error(`Unknown curve: ${parts[0]}`);
}
} else {
Expand Down
3 changes: 2 additions & 1 deletion packages/crypto/src/key_pair_base.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { KeyPairString } from './key_pair';
import { PublicKey } from './public_key';

export interface Signature {
Expand All @@ -8,6 +9,6 @@ export interface Signature {
export abstract class KeyPairBase {
abstract sign(message: Uint8Array): Signature;
abstract verify(message: Uint8Array, signature: Uint8Array): boolean;
abstract toString(): string;
abstract toString(): KeyPairString;
abstract getPublicKey(): PublicKey;
}
3 changes: 2 additions & 1 deletion packages/crypto/src/key_pair_ed25519.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import randombytes from 'randombytes';
import { KeySize, KeyType } from './constants';
import { KeyPairBase, Signature } from './key_pair_base';
import { PublicKey } from './public_key';
import { KeyPairString } from './key_pair';

/**
* This class provides key pair functionality for Ed25519 curve:
Expand Down Expand Up @@ -71,7 +72,7 @@ export class KeyPairEd25519 extends KeyPairBase {
* Returns a string representation of the key pair in the format 'ed25519:[extendedSecretKey]'.
* @returns {string} The string representation of the key pair.
*/
toString(): string {
toString(): KeyPairString {
return `ed25519:${this.extendedSecretKey}`;
}

Expand Down
78 changes: 78 additions & 0 deletions packages/crypto/src/key_pair_secp256k1.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { KeySize, KeyType } from './constants';
import { KeyPairBase, Signature } from './key_pair_base';
import { PublicKey } from './public_key';
import secp256k1 from 'secp256k1';
import randombytes from 'randombytes';
import { KeyPairString } from './key_pair';
import { baseDecode, baseEncode } from '@near-js/utils';
/**
* This class provides key pair functionality for secp256k1 curve:
* generating key pairs, encoding key pairs, signing and verifying.
* nearcore expects secp256k1 public keys to be 64 bytes at all times,
* even when string encoded the secp256k1 library returns 65 byte keys
* (including a 1 byte header that indicates how the pubkey was encoded).
* We'll force the secp256k1 library to always encode uncompressed
* keys with the corresponding 0x04 header byte, then manually
* insert/remove that byte as needed.
*/
export class KeyPairSecp256k1 extends KeyPairBase {
readonly publicKey: PublicKey;
readonly secretKey: string;
readonly extendedSecretKey: string;

/**
* Construct an instance of key pair given a secret key.
* It's generally assumed that these are encoded in base58.
* @param {string} extendedSecretKey
*/
constructor(extendedSecretKey: string) {
super();
const decoded = baseDecode(extendedSecretKey);
const secretKey = new Uint8Array(decoded.slice(0, KeySize.SECRET_KEY));
const withHeader = secp256k1.publicKeyCreate(new Uint8Array(secretKey), false);
const data = withHeader.subarray(1, withHeader.length); // remove the 0x04 header byte
this.publicKey = new PublicKey({
keyType: KeyType.SECP256K1,
data
});
this.secretKey = baseEncode(secretKey);
this.extendedSecretKey = extendedSecretKey;
}

/**
* Generate a new random keypair.
* @example
* const keyRandom = KeyPair.fromRandom();
* keyRandom.publicKey
* // returns [PUBLIC_KEY]
*
* keyRandom.secretKey
* // returns [SECRET_KEY]
*/
static fromRandom() {
// TODO: find better way to generate PK
const secretKey = randombytes(KeySize.SECRET_KEY);
const withHeader = secp256k1.publicKeyCreate(new Uint8Array(secretKey), false);
const publicKey = withHeader.subarray(1, withHeader.length);
const extendedSecretKey = new Uint8Array([...secretKey, ...publicKey]);
return new KeyPairSecp256k1(baseEncode(extendedSecretKey));
}

sign(message: Uint8Array): Signature {
// nearcore expects 65 byte signatures formed by appending the recovery id to the 64 byte signature
const { signature, recid } = secp256k1.ecdsaSign(message, baseDecode(this.secretKey));
return { signature: new Uint8Array([...signature, recid]), publicKey: this.publicKey };
}

verify(message: Uint8Array, signature: Uint8Array): boolean {
return this.publicKey.verify(message, signature);
}

toString(): KeyPairString {
return `secp256k1:${this.extendedSecretKey}`;
}

getPublicKey(): PublicKey {
return this.publicKey;
}
}
58 changes: 48 additions & 10 deletions packages/crypto/src/public_key.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,44 @@
import { Assignable } from '@near-js/types';
import { baseEncode, baseDecode } from '@near-js/utils';
import { ed25519 } from '@noble/curves/ed25519';
import secp256k1 from 'secp256k1';

import { KeySize, KeyType } from './constants';
import { Assignable } from '@near-js/types';

function key_type_to_str(keyType: KeyType): string {
switch (keyType) {
case KeyType.ED25519: return 'ed25519';
case KeyType.SECP256K1: return 'secp256k1';
default: throw new Error(`Unknown key type ${keyType}`);
}
}

function str_to_key_type(keyType: string): KeyType {
switch (keyType.toLowerCase()) {
case 'ed25519': return KeyType.ED25519;
case 'secp256k1': return KeyType.SECP256K1;
default: throw new Error(`Unknown key type ${keyType}`);
}
}

class ED25519PublicKey extends Assignable { keyType: KeyType = KeyType.ED25519; data: Uint8Array; }
class SECP256K1PublicKey extends Assignable { keyType: KeyType = KeyType.SECP256K1; data: Uint8Array; }

/**
* PublicKey representation that has type and bytes of the key.
*/
export class PublicKey extends Assignable {
keyType: KeyType;
data: Uint8Array;
ed25519Key?: ED25519PublicKey;
secp256k1Key?: SECP256K1PublicKey;

constructor({ keyType, data }: { keyType: KeyType, data: Uint8Array }) {
super({});
if (keyType === KeyType.ED25519) {
this.ed25519Key = { keyType, data };
} else if (keyType === KeyType.SECP256K1) {
this.secp256k1Key = { keyType, data };
}
}

/**
* Creates a PublicKey instance from a string or an existing PublicKey instance.
Expand All @@ -45,7 +60,7 @@ export class PublicKey extends Assignable {
static fromString(encodedKey: string): PublicKey {
const parts = encodedKey.split(':');
let publicKey: string;
let keyType = KeyType.ED25519;
let keyType;
if (parts.length === 1) {
publicKey = parts[0];
} else if (parts.length === 2) {
Expand All @@ -55,8 +70,12 @@ export class PublicKey extends Assignable {
throw new Error('Invalid encoded key format, must be <curve>:<encoded key>');
}
const decodedPublicKey = baseDecode(publicKey);
if(decodedPublicKey.length !== KeySize.SECRET_KEY) {
throw new Error(`Invalid public key size (${decodedPublicKey.length}), must be ${KeySize.SECRET_KEY}`);
if (!keyType) {
keyType = decodedPublicKey.length === KeySize.SECP256k1_PUBLIC_KEY ? KeyType.SECP256K1 : KeyType.ED25519;
}
const keySize = keyType === KeyType.ED25519 ? KeySize.ED25519_PUBLIC_KEY : KeySize.SECP256k1_PUBLIC_KEY;
if (decodedPublicKey.length !== keySize) {
throw new Error(`Invalid public key size (${decodedPublicKey.length}), must be ${keySize}`);
}
return new PublicKey({ keyType, data: decodedPublicKey });
}
Expand All @@ -66,7 +85,8 @@ export class PublicKey extends Assignable {
* @returns {string} The string representation of the public key.
*/
toString(): string {
return `${key_type_to_str(this.keyType)}:${baseEncode(this.data)}`;
const encodedKey = baseEncode(this.data);
return `${key_type_to_str(this.keyType)}:${encodedKey}`;
}

/**
Expand All @@ -76,9 +96,27 @@ export class PublicKey extends Assignable {
* @returns {boolean} `true` if the signature is valid, otherwise `false`.
*/
verify(message: Uint8Array, signature: Uint8Array): boolean {
switch (this.keyType) {
case KeyType.ED25519: return ed25519.verify(signature, message, this.data);
default: throw new Error(`Unknown key type ${this.keyType}`);
const keyType = this.keyType;
const data = this.data;
switch (keyType) {
case KeyType.ED25519:
return ed25519.verify(signature, message, data);
case KeyType.SECP256K1:
return secp256k1.ecdsaVerify(signature.subarray(0, 64), message, new Uint8Array([0x04, ...data]));
default:
throw new Error(`Unknown key type: ${keyType}`);
}
}

get keyPair() {
return this.ed25519Key || this.secp256k1Key;
}

get keyType(): KeyType {
return this.keyPair.keyType;
}

get data(): Uint8Array {
return this.keyPair.data;
}
}
Loading

0 comments on commit cc93136

Please sign in to comment.