From 92a383ff0dad4587e44953efca3c6ab795a1b1bd Mon Sep 17 00:00:00 2001 From: Richard Moore Date: Sat, 18 Jan 2020 21:09:02 -0500 Subject: [PATCH] Abstracting mnemonic phrases (#685). --- packages/abstract-signer/src.ts/index.ts | 2 - packages/ethers/src.ts/utils.ts | 5 +- packages/hdnode/src.ts/index.ts | 90 ++++++++++++++++++----- packages/json-wallets/src.ts/keystore.ts | 94 +++++++++++++++--------- packages/tests/src.ts/test-hdnode.ts | 6 +- packages/tests/src.ts/test-wallet.ts | 8 +- packages/wallet/src.ts/index.ts | 34 +++++---- 7 files changed, 163 insertions(+), 76 deletions(-) diff --git a/packages/abstract-signer/src.ts/index.ts b/packages/abstract-signer/src.ts/index.ts index 509a8fe73c..5e939b5874 100644 --- a/packages/abstract-signer/src.ts/index.ts +++ b/packages/abstract-signer/src.ts/index.ts @@ -18,8 +18,6 @@ const allowedTransactionKeys: Array = [ export interface ExternallyOwnedAccount { readonly address: string; readonly privateKey: string; - readonly mnemonic?: string; - readonly path?: string; } // Sub-Class Notes: diff --git a/packages/ethers/src.ts/utils.ts b/packages/ethers/src.ts/utils.ts index ac1b611f8e..048a8b6374 100644 --- a/packages/ethers/src.ts/utils.ts +++ b/packages/ethers/src.ts/utils.ts @@ -33,8 +33,9 @@ import { UnicodeNormalizationForm } from "@ethersproject/strings"; import { CoerceFunc } from "@ethersproject/abi"; import { Bytes, BytesLike, Hexable } from "@ethersproject/bytes" -import { ConnectionInfo, FetchJsonResponse, OnceBlockable, PollOptions } from "@ethersproject/web"; +import { Mnemonic } from "@ethersproject/hdnode"; import { EncryptOptions, ProgressCallback } from "@ethersproject/json-wallets"; +import { ConnectionInfo, FetchJsonResponse, OnceBlockable, PollOptions } from "@ethersproject/web"; //////////////////////// // Exports @@ -162,6 +163,8 @@ export { Indexed, + Mnemonic, + ConnectionInfo, OnceBlockable, PollOptions, diff --git a/packages/hdnode/src.ts/index.ts b/packages/hdnode/src.ts/index.ts index 479ac0140f..2be84fd850 100644 --- a/packages/hdnode/src.ts/index.ts +++ b/packages/hdnode/src.ts/index.ts @@ -46,10 +46,32 @@ function base58check(data: Uint8Array): string { return Base58.encode(concat([ data, hexDataSlice(sha256(sha256(data)), 0, 4) ])); } +function getWordlist(wordlist: string | Wordlist): Wordlist { + if (wordlist == null) { + return wordlists["en"]; + } + + if (typeof(wordlist) === "string") { + const words = wordlists[wordlist]; + if (words == null) { + logger.throwArgumentError("unknown locale", "wordlist", wordlist); + } + return words; + } + + return wordlist; +} + const _constructorGuard: any = {}; export const defaultPath = "m/44'/60'/0'/0/0"; +export interface Mnemonic { + readonly phrase: string; + readonly path: string; + readonly locale: string; +}; + export class HDNode implements ExternallyOwnedAccount { readonly privateKey: string; readonly publicKey: string; @@ -59,7 +81,7 @@ export class HDNode implements ExternallyOwnedAccount { readonly address: string; - readonly mnemonic: string; + readonly mnemonic?: Mnemonic; readonly path: string; readonly chainCode: string; @@ -74,7 +96,7 @@ export class HDNode implements ExternallyOwnedAccount { * - fromMnemonic * - fromSeed */ - constructor(constructorGuard: any, privateKey: string, publicKey: string, parentFingerprint: string, chainCode: string, index: number, depth: number, mnemonic: string, path: string) { + constructor(constructorGuard: any, privateKey: string, publicKey: string, parentFingerprint: string, chainCode: string, index: number, depth: number, mnemonicOrPath: Mnemonic | string) { logger.checkNew(new.target, HDNode); if (constructorGuard !== _constructorGuard) { @@ -100,8 +122,21 @@ export class HDNode implements ExternallyOwnedAccount { defineReadOnly(this, "index", index); defineReadOnly(this, "depth", depth); - defineReadOnly(this, "mnemonic", mnemonic); - defineReadOnly(this, "path", path); + if (mnemonicOrPath == null) { + // From a source that does not preserve the path (e.g. extended keys) + defineReadOnly(this, "mnemonic", null); + defineReadOnly(this, "path", null); + + } else if (typeof(mnemonicOrPath) === "string") { + // From a source that does not preserve the mnemonic (e.g. neutered) + defineReadOnly(this, "mnemonic", null); + defineReadOnly(this, "path", mnemonicOrPath); + + } else { + // From a fully qualified source + defineReadOnly(this, "mnemonic", mnemonicOrPath); + defineReadOnly(this, "path", mnemonicOrPath.path); + } } get extendedKey(): string { @@ -124,7 +159,7 @@ export class HDNode implements ExternallyOwnedAccount { } neuter(): HDNode { - return new HDNode(_constructorGuard, null, this.publicKey, this.parentFingerprint, this.chainCode, this.index, this.depth, null, this.path); + return new HDNode(_constructorGuard, null, this.publicKey, this.parentFingerprint, this.chainCode, this.index, this.depth, this.path); } private _derive(index: number): HDNode { @@ -172,7 +207,18 @@ export class HDNode implements ExternallyOwnedAccount { Ki = ek._addPoint(this.publicKey); } - return new HDNode(_constructorGuard, ki, Ki, this.fingerprint, bytes32(IR), index, this.depth + 1, this.mnemonic, path); + let mnemonicOrPath: Mnemonic | string = path; + + const srcMnemonic = this.mnemonic; + if (srcMnemonic) { + mnemonicOrPath = Object.freeze({ + phrase: srcMnemonic.phrase, + path: path, + locale: (srcMnemonic.locale || "en") + }); + } + + return new HDNode(_constructorGuard, ki, Ki, this.fingerprint, bytes32(IR), index, this.depth + 1, mnemonicOrPath); } derivePath(path: string): HDNode { @@ -204,20 +250,28 @@ export class HDNode implements ExternallyOwnedAccount { } - static _fromSeed(seed: BytesLike, mnemonic: string): HDNode { + static _fromSeed(seed: BytesLike, mnemonic: Mnemonic): HDNode { const seedArray: Uint8Array = arrayify(seed); if (seedArray.length < 16 || seedArray.length > 64) { throw new Error("invalid seed"); } const I: Uint8Array = arrayify(computeHmac(SupportedAlgorithms.sha512, MasterSecret, seedArray)); - return new HDNode(_constructorGuard, bytes32(I.slice(0, 32)), null, "0x00000000", bytes32(I.slice(32)), 0, 0, mnemonic, "m"); + return new HDNode(_constructorGuard, bytes32(I.slice(0, 32)), null, "0x00000000", bytes32(I.slice(32)), 0, 0, mnemonic); } - static fromMnemonic(mnemonic: string, password?: string, wordlist?: Wordlist): HDNode { + static fromMnemonic(mnemonic: string, password?: string, wordlist?: string | Wordlist): HDNode { + + // If a locale name was passed in, find the associated wordlist + wordlist = getWordlist(wordlist); + // Normalize the case and spacing in the mnemonic (throws if the mnemonic is invalid) mnemonic = entropyToMnemonic(mnemonicToEntropy(mnemonic, wordlist), wordlist); - return HDNode._fromSeed(mnemonicToSeed(mnemonic, password), mnemonic); + return HDNode._fromSeed(mnemonicToSeed(mnemonic, password), { + phrase: mnemonic, + path: "m", + locale: wordlist.locale + }); } static fromSeed(seed: BytesLike): HDNode { @@ -240,12 +294,12 @@ export class HDNode implements ExternallyOwnedAccount { switch (hexlify(bytes.slice(0, 4))) { // Public Key case "0x0488b21e": case "0x043587cf": - return new HDNode(_constructorGuard, null, hexlify(key), parentFingerprint, chainCode, index, depth, null, null); + return new HDNode(_constructorGuard, null, hexlify(key), parentFingerprint, chainCode, index, depth, null); // Private Key case "0x0488ade4": case "0x04358394 ": if (key[0] !== 0) { break; } - return new HDNode(_constructorGuard, hexlify(key.slice(1)), null, parentFingerprint, chainCode, index, depth, null, null); + return new HDNode(_constructorGuard, hexlify(key.slice(1)), null, parentFingerprint, chainCode, index, depth, null); } return logger.throwError("invalid extended key", "extendedKey", "[REDACTED]"); @@ -260,8 +314,8 @@ export function mnemonicToSeed(mnemonic: string, password?: string): string { return pbkdf2(toUtf8Bytes(mnemonic, UnicodeNormalizationForm.NFKD), salt, 2048, 64, "sha512"); } -export function mnemonicToEntropy(mnemonic: string, wordlist?: Wordlist): string { - if (!wordlist) { wordlist = wordlists["en"]; } +export function mnemonicToEntropy(mnemonic: string, wordlist?: string | Wordlist): string { + wordlist = getWordlist(wordlist); logger.checkNormalize(); @@ -297,7 +351,9 @@ export function mnemonicToEntropy(mnemonic: string, wordlist?: Wordlist): string return hexlify(entropy.slice(0, entropyBits / 8)); } -export function entropyToMnemonic(entropy: BytesLike, wordlist?: Wordlist): string { +export function entropyToMnemonic(entropy: BytesLike, wordlist?: string | Wordlist): string { + wordlist = getWordlist(wordlist); + entropy = arrayify(entropy); if ((entropy.length % 4) !== 0 || entropy.length < 16 || entropy.length > 32) { @@ -336,9 +392,7 @@ export function entropyToMnemonic(entropy: BytesLike, wordlist?: Wordlist): stri indices[indices.length - 1] <<= checksumBits; indices[indices.length - 1] |= (checksum >> (8 - checksumBits)); - if (!wordlist) { wordlist = wordlists["en"]; } - - return wordlist.join(indices.map((index) => wordlist.getWord(index))); + return wordlist.join(indices.map((index) => (wordlist).getWord(index))); } export function isValidMnemonic(mnemonic: string, wordlist?: Wordlist): boolean { diff --git a/packages/json-wallets/src.ts/keystore.ts b/packages/json-wallets/src.ts/keystore.ts index bf944ac0b2..e09483fdcf 100644 --- a/packages/json-wallets/src.ts/keystore.ts +++ b/packages/json-wallets/src.ts/keystore.ts @@ -7,7 +7,7 @@ import uuid from "uuid"; import { ExternallyOwnedAccount } from "@ethersproject/abstract-signer"; import { getAddress } from "@ethersproject/address"; import { arrayify, Bytes, BytesLike, concat, hexlify } from "@ethersproject/bytes"; -import { defaultPath, entropyToMnemonic, HDNode, mnemonicToEntropy } from "@ethersproject/hdnode"; +import { defaultPath, entropyToMnemonic, HDNode, Mnemonic, mnemonicToEntropy } from "@ethersproject/hdnode"; import { keccak256 } from "@ethersproject/keccak256"; import { pbkdf2 } from "@ethersproject/pbkdf2"; import { randomBytes } from "@ethersproject/random"; @@ -16,13 +16,20 @@ import { computeAddress } from "@ethersproject/transactions"; import { getPassword, looseArrayify, searchPath, zpad } from "./utils"; +import { Logger } from "@ethersproject/logger"; +import { version } from "./_version"; +const logger = new Logger(version); + // Exported Types +function hasMnemonic(value: any): value is { mnemonic: Mnemonic } { + return (value != null && value.mnemonic && value.mnemonic.phrase); +} + interface _KeystoreAccount { address: string; privateKey: string; - mnemonic?: string; - path?: string; + mnemonic?: Mnemonic; _isKeystoreAccount: boolean; } @@ -30,8 +37,7 @@ interface _KeystoreAccount { export class KeystoreAccount extends Description<_KeystoreAccount> implements ExternallyOwnedAccount { readonly address: string; readonly privateKey: string; - readonly mnemonic?: string; - readonly path?: string; + readonly mnemonic?: Mnemonic; readonly _isKeystoreAccount: boolean; @@ -90,7 +96,9 @@ export async function decrypt(json: string, password: Bytes | string, progressCa const mnemonicKey = key.slice(32, 64); if (!privateKey) { - throw new Error("unsupported cipher"); + logger.throwError("unsupported cipher", Logger.errors.UNSUPPORTED_OPERATION, { + operation: "decrypt" + }); } const address = computeAddress(privateKey); @@ -118,18 +126,28 @@ export async function decrypt(json: string, password: Bytes | string, progressCa const mnemonicAesCtr = new aes.ModeOfOperation.ctr(mnemonicKey, mnemonicCounter); const path = searchPath(data, "x-ethers/path") || defaultPath; + const locale = searchPath(data, "x-ethers/locale") || "en"; const entropy = arrayify(mnemonicAesCtr.decrypt(mnemonicCiphertext)); - const mnemonic = entropyToMnemonic(entropy); - const node = HDNode.fromMnemonic(mnemonic).derivePath(path); + try { + const mnemonic = entropyToMnemonic(entropy, locale); + const node = HDNode.fromMnemonic(mnemonic, null, locale).derivePath(path); - if (node.privateKey != account.privateKey) { - throw new Error("mnemonic mismatch"); - } + if (node.privateKey != account.privateKey) { + throw new Error("mnemonic mismatch"); + } - account.mnemonic = node.mnemonic; - account.path = node.path; + account.mnemonic = node.mnemonic; + + } catch (error) { + // If we don't have the locale wordlist installed to + // read this mnemonic, just bail and don't set the + // mnemonic + if (error.code !== Logger.errors.INVALID_ARGUMENT || error.argument !== "wordlist") { + throw error; + } + } } return new KeystoreAccount(account); @@ -138,24 +156,24 @@ export async function decrypt(json: string, password: Bytes | string, progressCa const kdf = searchPath(data, "crypto/kdf"); if (kdf && typeof(kdf) === "string") { + const throwError = function(name: string, value: any): never { + return logger.throwArgumentError("invalid key-derivation function parameters", name, value); + } + if (kdf.toLowerCase() === "scrypt") { const salt = looseArrayify(searchPath(data, "crypto/kdfparams/salt")); const N = parseInt(searchPath(data, "crypto/kdfparams/n")); const r = parseInt(searchPath(data, "crypto/kdfparams/r")); const p = parseInt(searchPath(data, "crypto/kdfparams/p")); - if (!N || !r || !p) { - throw new Error("unsupported key-derivation function parameters"); - } + + // Check for all required parameters + if (!N || !r || !p) { throwError("kdf", kdf); } // Make sure N is a power of 2 - if ((N & (N - 1)) !== 0) { - throw new Error("unsupported key-derivation function parameter value for N"); - } + if ((N & (N - 1)) !== 0) { throwError("N", N); } const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); - if (dkLen !== 32) { - throw new Error("unsupported key-derivation derived-key length"); - } + if (dkLen !== 32) { throwError("dklen", dkLen); } const key = await scrypt.scrypt(passwordBytes, salt, N, r, p, 64, progressCallback); //key = arrayify(key); @@ -173,46 +191,46 @@ export async function decrypt(json: string, password: Bytes | string, progressCa } else if (prf === "hmac-sha512") { prfFunc = "sha512"; } else { - throw new Error("unsupported prf"); + throwError("prf", prf); } const c = parseInt(searchPath(data, "crypto/kdfparams/c")); const dkLen = parseInt(searchPath(data, "crypto/kdfparams/dklen")); - if (dkLen !== 32) { - throw new Error("unsupported key-derivation derived-key length"); - } + if (dkLen !== 32) { throwError("dklen", dkLen); } const key = arrayify(pbkdf2(passwordBytes, salt, c, dkLen, prfFunc)); return getAccount(key); } } - throw new Error("unsupported key-derivation function"); + + return logger.throwArgumentError("unsupported key-derivation function", "kdf", kdf); } export function encrypt(account: ExternallyOwnedAccount, password: Bytes | string, options?: EncryptOptions, progressCallback?: ProgressCallback): Promise { try { + // Check the address matches the private key if (getAddress(account.address) !== computeAddress(account.privateKey)) { throw new Error("address/privateKey mismatch"); } - if (account.mnemonic != null){ - const node = HDNode.fromMnemonic(account.mnemonic).derivePath(account.path || defaultPath); + // Check the mnemonic (if any) matches the private key + if (hasMnemonic(account)) { + const mnemonic = account.mnemonic; + const node = HDNode.fromMnemonic(mnemonic.phrase, null, mnemonic.locale).derivePath(mnemonic.path || defaultPath); if (node.privateKey != account.privateKey) { throw new Error("mnemonic mismatch"); } - } else if (account.path != null) { - throw new Error("cannot specify path without mnemonic"); } } catch (e) { return Promise.reject(e); } - // the options are optional, so adjust the call as needed + // The options are optional, so adjust the call as needed if (typeof(options) === "function" && !progressCallback) { progressCallback = options; options = {}; @@ -223,10 +241,13 @@ export function encrypt(account: ExternallyOwnedAccount, password: Bytes | strin const passwordBytes = getPassword(password); let entropy: Uint8Array = null - let path: string = account.path; - if (account.mnemonic) { - entropy = arrayify(mnemonicToEntropy(account.mnemonic)); - if (!path) { path = defaultPath; } + let path: string = null; + let locale: string = null; + if (hasMnemonic(account)) { + const srcMnemonic = account.mnemonic; + entropy = arrayify(mnemonicToEntropy(srcMnemonic.phrase, srcMnemonic.locale || "en")); + path = srcMnemonic.path || defaultPath; + locale = srcMnemonic.locale || "en"; } let client = options.client; @@ -330,6 +351,7 @@ export function encrypt(account: ExternallyOwnedAccount, password: Bytes | strin mnemonicCounter: hexlify(mnemonicIv).substring(2), mnemonicCiphertext: hexlify(mnemonicCiphertext).substring(2), path: path, + locale: locale, version: "0.1" }; } diff --git a/packages/tests/src.ts/test-hdnode.ts b/packages/tests/src.ts/test-hdnode.ts index 01d4eb29b1..b87edda392 100644 --- a/packages/tests/src.ts/test-hdnode.ts +++ b/packages/tests/src.ts/test-hdnode.ts @@ -77,10 +77,12 @@ describe('Test HD Node Derivation from Mnemonic', function() { assert.equal(node.privateKey, nodeTest.privateKey, 'Matches privateKey - ' + nodeTest.privateKey); - assert.equal(node.mnemonic, test.mnemonic, - 'Matches mnemonic - ' + nodeTest.privateKey); assert.equal(node.path, nodeTest.path, 'Matches path - ' + nodeTest.privateKey); + assert.equal(node.mnemonic.phrase, test.mnemonic, + 'Matches mnemonic.phrase - ' + nodeTest.privateKey); + assert.equal(node.mnemonic.path, nodeTest.path, + 'Matches mnemonic.path - ' + nodeTest.privateKey); let wallet = new ethers.Wallet(node.privateKey); assert.equal(wallet.address.toLowerCase(), nodeTest.address, diff --git a/packages/tests/src.ts/test-wallet.ts b/packages/tests/src.ts/test-wallet.ts index ca86808771..b34d85ed34 100644 --- a/packages/tests/src.ts/test-wallet.ts +++ b/packages/tests/src.ts/test-wallet.ts @@ -26,8 +26,8 @@ describe('Test JSON Wallets', function() { assert.equal(wallet.address.toLowerCase(), test.address, 'generate correct address - ' + wallet.address); if (test.mnemonic) { - assert.equal(wallet.mnemonic, test.mnemonic, - 'mnemonic enabled encrypted wallet has a mnemonic'); + assert.equal(wallet.mnemonic.phrase, test.mnemonic, + 'mnemonic enabled encrypted wallet has a mnemonic phrase'); } }); }); @@ -45,9 +45,9 @@ describe('Test JSON Wallets', function() { return ethers.Wallet.fromEncryptedJson(json, password).then((decryptedWallet) => { assert.equal(decryptedWallet.address, wallet.address, 'decrypted wallet - ' + wallet.privateKey); - assert.equal(decryptedWallet.mnemonic, wallet.mnemonic, + assert.equal(decryptedWallet.mnemonic.phrase, wallet.mnemonic.phrase, "decrypted wallet menonic - " + wallet.privateKey); - assert.equal(decryptedWallet.path, wallet.path, + assert.equal(decryptedWallet.mnemonic.path, wallet.mnemonic.path, "decrypted wallet path - " + wallet.privateKey); return decryptedWallet.encrypt(password).then((encryptedWallet) => { let parsedWallet = JSON.parse(encryptedWallet); diff --git a/packages/wallet/src.ts/index.ts b/packages/wallet/src.ts/index.ts index 8cbf271ee5..7a04937b04 100644 --- a/packages/wallet/src.ts/index.ts +++ b/packages/wallet/src.ts/index.ts @@ -5,7 +5,7 @@ import { Provider, TransactionRequest } from "@ethersproject/abstract-provider"; import { ExternallyOwnedAccount, Signer } from "@ethersproject/abstract-signer"; import { arrayify, Bytes, BytesLike, concat, hexDataSlice, isHexString, joinSignature, SignatureLike } from "@ethersproject/bytes"; import { hashMessage } from "@ethersproject/hash"; -import { defaultPath, HDNode, entropyToMnemonic } from "@ethersproject/hdnode"; +import { defaultPath, HDNode, entropyToMnemonic, Mnemonic } from "@ethersproject/hdnode"; import { keccak256 } from "@ethersproject/keccak256"; import { defineReadOnly, resolveProperties } from "@ethersproject/properties"; import { randomBytes } from "@ethersproject/random"; @@ -22,17 +22,20 @@ function isAccount(value: any): value is ExternallyOwnedAccount { return (value != null && isHexString(value.privateKey, 32) && value.address != null); } +function hasMnemonic(value: any): value is { mnemonic: Mnemonic } { + const mnemonic = value.mnemonic; + return (mnemonic && mnemonic.phrase); +} + export class Wallet extends Signer implements ExternallyOwnedAccount { readonly address: string; readonly provider: Provider; - readonly path: string; - // Wrapping the _signingKey and _mnemonic in a getter function prevents // leaking the private key in console.log; still, be careful! :) readonly _signingKey: () => SigningKey; - readonly _mnemonic: () => string; + readonly _mnemonic: () => Mnemonic; constructor(privateKey: BytesLike | ExternallyOwnedAccount | SigningKey, provider?: Provider) { logger.checkNew(new.target, Wallet); @@ -48,17 +51,22 @@ export class Wallet extends Signer implements ExternallyOwnedAccount { logger.throwArgumentError("privateKey/address mismatch", "privateKey", "[REDCACTED]"); } - if (privateKey.mnemonic != null) { - const mnemonic = privateKey.mnemonic; - const path = privateKey.path || defaultPath; - defineReadOnly(this, "_mnemonic", () => mnemonic); - defineReadOnly(this, "path", privateKey.path); - const node = HDNode.fromMnemonic(mnemonic).derivePath(path); + if (hasMnemonic(privateKey)) { + const srcMnemonic = privateKey.mnemonic; + defineReadOnly(this, "_mnemonic", () => ( + { + phrase: srcMnemonic.phrase, + path: srcMnemonic.path || defaultPath, + locale: srcMnemonic.locale || "en" + } + )); + const mnemonic = this.mnemonic; + const node = HDNode.fromMnemonic(mnemonic.phrase, null, mnemonic.locale).derivePath(mnemonic.path); if (computeAddress(node.privateKey) !== this.address) { logger.throwArgumentError("mnemonic/address mismatch", "privateKey", "[REDCACTED]"); } } else { - defineReadOnly(this, "_mnemonic", (): string => null); + defineReadOnly(this, "_mnemonic", (): Mnemonic => null); defineReadOnly(this, "path", null); } @@ -73,7 +81,7 @@ export class Wallet extends Signer implements ExternallyOwnedAccount { const signingKey = new SigningKey(privateKey); defineReadOnly(this, "_signingKey", () => signingKey); } - defineReadOnly(this, "_mnemonic", (): string => null); + defineReadOnly(this, "_mnemonic", (): Mnemonic => null); defineReadOnly(this, "path", null); defineReadOnly(this, "address", computeAddress(this.publicKey)); } @@ -85,7 +93,7 @@ export class Wallet extends Signer implements ExternallyOwnedAccount { defineReadOnly(this, "provider", provider || null); } - get mnemonic(): string { return this._mnemonic(); } + get mnemonic(): Mnemonic { return this._mnemonic(); } get privateKey(): string { return this._signingKey().privateKey; } get publicKey(): string { return this._signingKey().publicKey; }