@dfinity/identity-secp256k1
Version:
JavaScript and TypeScript library to manage Secp256k1KeyIdentities for use with the Internet Computer
222 lines • 8.89 kB
JavaScript
import { SignIdentity, } from '@dfinity/agent';
import { secp256k1 } from '@noble/curves/secp256k1';
import { sha256 } from '@noble/hashes/sha2';
import { bytesToHex, hexToBytes, randomBytes } from '@noble/hashes/utils';
import * as bip39 from '@scure/bip39';
import { HDKey } from '@scure/bip32';
import { SECP256K1_OID, unwrapDER, wrapDER } from "./der.js";
import { pemToSecretKey } from "./pem.js";
import { uint8FromBufLike } from '@dfinity/candid';
function isObject(value) {
return value !== null && typeof value === 'object';
}
export class Secp256k1PublicKey {
static fromRaw(rawKey) {
return new Secp256k1PublicKey(rawKey);
}
static fromDer(derKey) {
return new Secp256k1PublicKey(this.derDecode(derKey));
}
/**
* Construct Secp256k1PublicKey from an existing PublicKey
* @param {unknown} maybeKey - existing PublicKey, ArrayBuffer, DerEncodedPublicKey, or hex string
* @returns {Secp256k1PublicKey} Instance of Secp256k1PublicKey
*/
static from(maybeKey) {
if (typeof maybeKey === 'string') {
const key = hexToBytes(maybeKey);
return this.fromRaw(key);
}
else if (isObject(maybeKey)) {
const key = maybeKey;
if (isObject(key) && Object.hasOwnProperty.call(key, '__derEncodedPublicKey__')) {
return this.fromDer(key);
}
else if (ArrayBuffer.isView(key)) {
const view = key;
return this.fromRaw(uint8FromBufLike(view.buffer));
}
else if (key instanceof ArrayBuffer) {
return this.fromRaw(uint8FromBufLike(key));
}
else if ('rawKey' in key && key['rawKey'] !== undefined) {
return this.fromRaw(key.rawKey);
}
else if ('derKey' in key) {
return this.fromDer(key.derKey);
}
else if ('toDer' in key) {
return this.fromDer(key.toDer());
}
}
throw new Error('Cannot construct Secp256k1PublicKey from the provided key.');
}
static derEncode(publicKey) {
const key = uint8FromBufLike(wrapDER(publicKey, SECP256K1_OID).buffer);
key.__derEncodedPublicKey__ = undefined;
return key;
}
static derDecode(key) {
return unwrapDER(key, SECP256K1_OID);
}
#rawKey;
get rawKey() {
return this.#rawKey;
}
#derKey;
get derKey() {
return this.#derKey;
}
// `fromRaw` and `fromDer` should be used for instantiation, not this constructor.
constructor(key) {
this.#rawKey = key;
this.#derKey = Secp256k1PublicKey.derEncode(key);
}
toDer() {
return this.derKey;
}
toRaw() {
return this.rawKey;
}
}
export class Secp256k1KeyIdentity extends SignIdentity {
/**
* Generates an identity. If a seed is provided, the keys are generated from the
* seed according to BIP 0032. Otherwise, the key pair is randomly generated.
* This method throws an error in case the seed is not 32 bytes long or invalid
* for use as a private key.
* @param {Uint8Array} seed the optional seed
* @returns {Secp256k1KeyIdentity} Secp256k1KeyIdentity
*/
static generate(seed) {
if (seed && seed.byteLength !== 32) {
throw new Error('Secp256k1 Seed needs to be 32 bytes long.');
}
let privateKey;
if (seed) {
// private key from seed according to https://en.bitcoin.it/wiki/BIP_0032
// master key generation:
privateKey = seed;
if (!secp256k1.utils.isValidPrivateKey(privateKey)) {
throw new Error('The seed is invalid.');
}
}
else {
privateKey = randomBytes(32);
while (!secp256k1.utils.isValidPrivateKey(privateKey)) {
privateKey = randomBytes(32);
}
}
const publicKeyRaw = secp256k1.getPublicKey(privateKey, false);
const publicKey = Secp256k1PublicKey.fromRaw(publicKeyRaw);
return new this(publicKey, privateKey);
}
static fromParsedJson(obj) {
const [publicKeyRaw, privateKeyRaw] = obj;
return new Secp256k1KeyIdentity(Secp256k1PublicKey.fromRaw(hexToBytes(publicKeyRaw)), hexToBytes(privateKeyRaw));
}
static fromJSON(json) {
const parsed = JSON.parse(json);
if (Array.isArray(parsed)) {
if (typeof parsed[0] === 'string' && typeof parsed[1] === 'string') {
return this.fromParsedJson([parsed[0], parsed[1]]);
}
throw new Error('Deserialization error: JSON must have at least 2 items.');
}
throw new Error(`Deserialization error: Invalid JSON type for string: ${JSON.stringify(json)}`);
}
/**
* generates an identity from a public and private key. Please ensure that you are generating these keys securely and protect the user's private key
* @param {Uint8Array} publicKey - Uint8Array
* @param {Uint8Array} privateKey - Uint8Array
* @returns {Secp256k1KeyIdentity} Secp256k1KeyIdentity
*/
static fromKeyPair(publicKey, privateKey) {
return new Secp256k1KeyIdentity(Secp256k1PublicKey.fromRaw(publicKey), privateKey);
}
/**
* generates an identity from an existing secret key, and is the correct method to generate an identity from a seed phrase. Please ensure you protect the user's private key.
* @param {Uint8Array} secretKey - Uint8Array
* @returns {Secp256k1KeyIdentity} - Secp256k1KeyIdentity
*/
static fromSecretKey(secretKey) {
const publicKey = secp256k1.getPublicKey(secretKey, false);
const identity = Secp256k1KeyIdentity.fromKeyPair(publicKey, secretKey);
return identity;
}
/**
* Generates an identity from a seed phrase. Use carefully - seed phrases should only be used in secure contexts, and you should avoid having users copying and pasting seed phrases as much as possible.
* @param {string | string[]} seedPhrase - either an array of words or a string of words separated by spaces.
* @param password - optional password to be used by bip39
* @returns Secp256k1KeyIdentity
*/
static fromSeedPhrase(seedPhrase, password) {
// Convert to string for convenience
const phrase = Array.isArray(seedPhrase) ? seedPhrase.join(' ') : seedPhrase;
// Warn if provided phrase is not conventional
if (phrase.split(' ').length < 12 || phrase.split(' ').length > 24) {
console.warn('Warning - an unusually formatted seed phrase has been provided. Decoding may not work as expected');
}
const seed = bip39.mnemonicToSeedSync(phrase, password);
// Ensure the seed is 64 bytes long
if (seed.byteLength !== 64) {
throw new Error('Derived seed must be 64 bytes long.');
}
const root = HDKey.fromMasterSeed(seed);
const addrnode = root.derive("m/44'/223'/0'/0/0");
if (!addrnode.privateKey) {
throw new Error('Failed to derive private key from seed phrase');
}
return Secp256k1KeyIdentity.fromSecretKey(addrnode.privateKey);
}
/**
* Utility method to create a Secp256k1KeyIdentity from a PEM-encoded key.
* @param pemKey - PEM-encoded key as a string
* @returns - Secp256k1KeyIdentity
*/
static fromPem(pemKey) {
const secretKey = pemToSecretKey(pemKey);
return this.fromSecretKey(secretKey);
}
constructor(publicKey, _privateKey) {
super();
this._privateKey = _privateKey;
this._publicKey = publicKey;
}
/**
* Serialize this key to JSON-serializable object.
* @returns {JsonableSecp256k1Identity} JsonableSecp256k1Identity
*/
toJSON() {
return [bytesToHex(this._publicKey.toRaw()), bytesToHex(this._privateKey)];
}
/**
* Return a copy of the key pair.
* @returns {KeyPair} KeyPair
*/
getKeyPair() {
return {
secretKey: this._privateKey,
publicKey: this._publicKey,
};
}
/**
* Return the public key.
* @returns {Required<PublicKey>} Required<PublicKey>
*/
getPublicKey() {
return this._publicKey;
}
/**
* Signs a blob of data, with this identity's private key.
* @param {Uint8Array} data - bytes to hash and sign with this identity's secretKey, producing a signature
* @returns {Promise<Signature>} signature
*/
async sign(data) {
const challenge = sha256(data);
const signature = secp256k1.sign(challenge, this._privateKey).toCompactRawBytes();
return signature;
}
}
export default Secp256k1KeyIdentity;
//# sourceMappingURL=secp256k1.js.map