From 0acf05ef3375383225ea07908f3bc6aa1fd38ac1 Mon Sep 17 00:00:00 2001 From: Jordan Gensler Date: Fri, 27 Jun 2025 12:35:32 -0500 Subject: [PATCH] Redacted + private keys --- .../typescript/src/cryptography/keypair.ts | 8 +++ .../src/keypairs/ed25519/keypair.ts | 28 +++++--- .../src/keypairs/secp256k1/keypair.ts | 29 ++++++-- .../src/keypairs/secp256r1/keypair.ts | 29 ++++++-- packages/typescript/src/utils/index.ts | 2 + packages/typescript/src/utils/redacted.ts | 71 +++++++++++++++++++ 6 files changed, 145 insertions(+), 22 deletions(-) create mode 100644 packages/typescript/src/utils/redacted.ts diff --git a/packages/typescript/src/cryptography/keypair.ts b/packages/typescript/src/cryptography/keypair.ts index caa7196b7..0ac32d783 100644 --- a/packages/typescript/src/cryptography/keypair.ts +++ b/packages/typescript/src/cryptography/keypair.ts @@ -13,6 +13,7 @@ import type { SignatureScheme } from './signature-scheme.js'; import { toSerializedSignature } from './signature.js'; import type { Transaction } from '../transactions/Transaction.js'; import type { ClientWithCoreApi, Experimental_SuiClientTypes } from '../experimental/index.js'; +import { Redacted } from '../utils/redacted.js'; export const PRIVATE_KEY_SIZE = 32; export const LEGACY_PRIVATE_KEY_SIZE = 64; @@ -112,8 +113,15 @@ export abstract class Signer { export abstract class Keypair extends Signer { /** * This returns the Bech32 secret key string for this keypair. + * + * We recommend using `getSecretKeyRedacted()` instead, to ensure the secret key is not logged. */ abstract getSecretKey(): string; + + /** + * Get the secret key for this keypair, with the value redacted so that it cannot be logged. + */ + abstract getSecretKeyRedacted(): Redacted; } /** diff --git a/packages/typescript/src/keypairs/ed25519/keypair.ts b/packages/typescript/src/keypairs/ed25519/keypair.ts index 90f6da6a4..609954c27 100644 --- a/packages/typescript/src/keypairs/ed25519/keypair.ts +++ b/packages/typescript/src/keypairs/ed25519/keypair.ts @@ -13,6 +13,8 @@ import { isValidHardenedPath, mnemonicToSeedHex } from '../../cryptography/mnemo import type { SignatureScheme } from '../../cryptography/signature-scheme.js'; import { derivePath } from './ed25519-hd-key.js'; import { Ed25519PublicKey } from './publickey.js'; +import type { Redacted } from '../../utils/redacted.js'; +import { redacted, getRedactedOrPlainValue } from '../../utils/redacted.js'; export const DEFAULT_ED25519_DERIVATION_PATH = "m/44'/784'/0'/0'/0'"; @@ -23,14 +25,17 @@ export const DEFAULT_ED25519_DERIVATION_PATH = "m/44'/784'/0'/0'/0'"; */ export interface Ed25519KeypairData { publicKey: Uint8Array; - secretKey: Uint8Array; + secretKey: Uint8Array | Redacted; } /** * An Ed25519 Keypair used for signing transactions. */ export class Ed25519Keypair extends Keypair { - private keypair: Ed25519KeypairData; + #keypair: { + publicKey: Uint8Array; + secretKey: Uint8Array; + }; /** * Create a new Ed25519 keypair instance. @@ -41,13 +46,13 @@ export class Ed25519Keypair extends Keypair { constructor(keypair?: Ed25519KeypairData) { super(); if (keypair) { - this.keypair = { + this.#keypair = { publicKey: keypair.publicKey, - secretKey: keypair.secretKey.slice(0, 32), + secretKey: getRedactedOrPlainValue(keypair.secretKey).slice(0, 32), }; } else { const privateKey = ed25519.utils.randomPrivateKey(); - this.keypair = { + this.#keypair = { publicKey: ed25519.getPublicKey(privateKey), secretKey: privateKey, }; @@ -122,7 +127,7 @@ export class Ed25519Keypair extends Keypair { * The public key for this Ed25519 keypair */ getPublicKey(): Ed25519PublicKey { - return new Ed25519PublicKey(this.keypair.publicKey); + return new Ed25519PublicKey(this.#keypair.publicKey); } /** @@ -130,16 +135,23 @@ export class Ed25519Keypair extends Keypair { */ getSecretKey(): string { return encodeSuiPrivateKey( - this.keypair.secretKey.slice(0, PRIVATE_KEY_SIZE), + this.#keypair.secretKey.slice(0, PRIVATE_KEY_SIZE), this.getKeyScheme(), ); } + /** + * The Bech32 secret key string for this Ed25519 keypair, with the value redacted so that it cannot be logged. + */ + getSecretKeyRedacted(): Redacted { + return redacted(this.getSecretKey()); + } + /** * Return the signature for the provided data using Ed25519. */ async sign(data: Uint8Array) { - return ed25519.sign(data, this.keypair.secretKey); + return ed25519.sign(data, this.#keypair.secretKey); } /** diff --git a/packages/typescript/src/keypairs/secp256k1/keypair.ts b/packages/typescript/src/keypairs/secp256k1/keypair.ts index 27c74719d..c15714072 100644 --- a/packages/typescript/src/keypairs/secp256k1/keypair.ts +++ b/packages/typescript/src/keypairs/secp256k1/keypair.ts @@ -12,6 +12,8 @@ import { isValidBIP32Path, mnemonicToSeed } from '../../cryptography/mnemonics.j import type { PublicKey } from '../../cryptography/publickey.js'; import type { SignatureScheme } from '../../cryptography/signature-scheme.js'; import { Secp256k1PublicKey } from './publickey.js'; +import type { Redacted } from '../../utils/redacted.js'; +import { getRedactedOrPlainValue, redacted } from '../../utils/redacted.js'; export const DEFAULT_SECP256K1_DERIVATION_PATH = "m/54'/784'/0'/0/0"; @@ -20,14 +22,17 @@ export const DEFAULT_SECP256K1_DERIVATION_PATH = "m/54'/784'/0'/0/0"; */ export interface Secp256k1KeypairData { publicKey: Uint8Array; - secretKey: Uint8Array; + secretKey: Uint8Array | Redacted; } /** * An Secp256k1 Keypair used for signing transactions. */ export class Secp256k1Keypair extends Keypair { - private keypair: Secp256k1KeypairData; + #keypair: { + publicKey: Uint8Array; + secretKey: Uint8Array; + }; /** * Create a new keypair instance. @@ -38,12 +43,15 @@ export class Secp256k1Keypair extends Keypair { constructor(keypair?: Secp256k1KeypairData) { super(); if (keypair) { - this.keypair = keypair; + this.#keypair = { + publicKey: keypair.publicKey, + secretKey: getRedactedOrPlainValue(keypair.secretKey), + }; } else { const secretKey: Uint8Array = secp256k1.utils.randomPrivateKey(); const publicKey: Uint8Array = secp256k1.getPublicKey(secretKey, true); - this.keypair = { publicKey, secretKey }; + this.#keypair = { publicKey, secretKey }; } } @@ -115,13 +123,20 @@ export class Secp256k1Keypair extends Keypair { * The public key for this keypair */ getPublicKey(): PublicKey { - return new Secp256k1PublicKey(this.keypair.publicKey); + return new Secp256k1PublicKey(this.#keypair.publicKey); } /** * The Bech32 secret key string for this Secp256k1 keypair */ getSecretKey(): string { - return encodeSuiPrivateKey(this.keypair.secretKey, this.getKeyScheme()); + return encodeSuiPrivateKey(this.#keypair.secretKey, this.getKeyScheme()); + } + + /** + * The Bech32 secret key string for this Secp256k1 keypair, with the value redacted so that it cannot be logged. + */ + getSecretKeyRedacted(): Redacted { + return redacted(this.getSecretKey()); } /** @@ -129,7 +144,7 @@ export class Secp256k1Keypair extends Keypair { */ async sign(data: Uint8Array) { const msgHash = sha256(data); - const sig = secp256k1.sign(msgHash, this.keypair.secretKey, { + const sig = secp256k1.sign(msgHash, this.#keypair.secretKey, { lowS: true, }); return sig.toCompactRawBytes(); diff --git a/packages/typescript/src/keypairs/secp256r1/keypair.ts b/packages/typescript/src/keypairs/secp256r1/keypair.ts index 7124b6a5e..6b7c45655 100644 --- a/packages/typescript/src/keypairs/secp256r1/keypair.ts +++ b/packages/typescript/src/keypairs/secp256r1/keypair.ts @@ -12,6 +12,8 @@ import { isValidBIP32Path, mnemonicToSeed } from '../../cryptography/mnemonics.j import type { PublicKey } from '../../cryptography/publickey.js'; import type { SignatureScheme } from '../../cryptography/signature-scheme.js'; import { Secp256r1PublicKey } from './publickey.js'; +import type { Redacted } from '../../utils/redacted.js'; +import { getRedactedOrPlainValue, redacted } from '../../utils/redacted.js'; export const DEFAULT_SECP256R1_DERIVATION_PATH = "m/74'/784'/0'/0/0"; @@ -20,14 +22,17 @@ export const DEFAULT_SECP256R1_DERIVATION_PATH = "m/74'/784'/0'/0/0"; */ export interface Secp256r1KeypairData { publicKey: Uint8Array; - secretKey: Uint8Array; + secretKey: Uint8Array | Redacted; } /** * An Secp256r1 Keypair used for signing transactions. */ export class Secp256r1Keypair extends Keypair { - private keypair: Secp256r1KeypairData; + #keypair: { + publicKey: Uint8Array; + secretKey: Uint8Array; + }; /** * Create a new keypair instance. @@ -38,12 +43,15 @@ export class Secp256r1Keypair extends Keypair { constructor(keypair?: Secp256r1KeypairData) { super(); if (keypair) { - this.keypair = keypair; + this.#keypair = { + publicKey: keypair.publicKey, + secretKey: getRedactedOrPlainValue(keypair.secretKey), + }; } else { const secretKey: Uint8Array = secp256r1.utils.randomPrivateKey(); const publicKey: Uint8Array = secp256r1.getPublicKey(secretKey, true); - this.keypair = { publicKey, secretKey }; + this.#keypair = { publicKey, secretKey }; } } @@ -115,14 +123,21 @@ export class Secp256r1Keypair extends Keypair { * The public key for this keypair */ getPublicKey(): PublicKey { - return new Secp256r1PublicKey(this.keypair.publicKey); + return new Secp256r1PublicKey(this.#keypair.publicKey); } /** * The Bech32 secret key string for this Secp256r1 keypair */ getSecretKey(): string { - return encodeSuiPrivateKey(this.keypair.secretKey, this.getKeyScheme()); + return encodeSuiPrivateKey(this.#keypair.secretKey, this.getKeyScheme()); + } + + /** + * The Bech32 secret key string for this Secp256r1 keypair, with the value redacted so that it cannot be logged. + */ + getSecretKeyRedacted(): Redacted { + return redacted(this.getSecretKey()); } /** @@ -130,7 +145,7 @@ export class Secp256r1Keypair extends Keypair { */ async sign(data: Uint8Array) { const msgHash = sha256(data); - const sig = secp256r1.sign(msgHash, this.keypair.secretKey, { + const sig = secp256r1.sign(msgHash, this.#keypair.secretKey, { lowS: true, }); return sig.toCompactRawBytes(); diff --git a/packages/typescript/src/utils/index.ts b/packages/typescript/src/utils/index.ts index bc52bceea..2883f3d83 100644 --- a/packages/typescript/src/utils/index.ts +++ b/packages/typescript/src/utils/index.ts @@ -42,3 +42,5 @@ export { export { isValidNamedPackage, isValidNamedType } from './move-registry.js'; export { deriveDynamicFieldID } from './dynamic-fields.js'; + +export { redacted, isRedacted, getRedactedValue } from './redacted.js'; diff --git a/packages/typescript/src/utils/redacted.ts b/packages/typescript/src/utils/redacted.ts new file mode 100644 index 000000000..ee73f0aae --- /dev/null +++ b/packages/typescript/src/utils/redacted.ts @@ -0,0 +1,71 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +type RedactedRegistry = WeakMap, any>; + +const moduleRedactedRegistry: RedactedRegistry = new WeakMap(); + +const REDACTED_REGISTRY_KEY = Symbol.for('@mysten/redacted/registry'); +function getRedactedRegistry() { + try { + const target = globalThis as { + [REDACTED_REGISTRY_KEY]?: RedactedRegistry; + }; + + if (!target[REDACTED_REGISTRY_KEY]) { + target[REDACTED_REGISTRY_KEY] = moduleRedactedRegistry; + } + + return target[REDACTED_REGISTRY_KEY]; + } catch (e) { + return moduleRedactedRegistry; + } +} + +const RedactedType: unique symbol = Symbol.for('@mysten/redacted'); +export interface Redacted { + [RedactedType]: T; +} + +export function getRedactedValue(redacted: Redacted): T { + const redactedRegistry = getRedactedRegistry(); + + if (redactedRegistry.has(redacted)) { + return redactedRegistry.get(redacted) as T; + } else { + throw new Error('Unable to get redacted value'); + } +} + +export function redacted(value: T): Redacted { + const redactedRegistry = getRedactedRegistry(); + + const redacted = { + [RedactedType]: true, + [Symbol.toStringTag]: 'Redacted', + [Symbol.for('nodejs.util.inspect.custom')]: () => '', + toString() { + return ''; + }, + toJSON() { + return ''; + }, + }; + + redactedRegistry.set(redacted, value); + + return redacted as never; +} + +export function isRedacted(value: any): value is Redacted { + return typeof value === 'object' && Object.hasOwn(value, RedactedType); +} + +/** @internal */ +export function getRedactedOrPlainValue(value: T | Redacted): T { + if (isRedacted(value)) { + return getRedactedValue(value); + } + + return value; +}