|
| 1 | +/** |
| 2 | + * @prettier |
| 3 | + */ |
| 4 | +import { BitGoBase } from '../bitgoBase'; |
| 5 | +import { decryptKeychainPrivateKey, IKeychains, Keychain, KeychainWithEncryptedPrv } from '../keychain'; |
| 6 | +import { deriveSafeChildHardenedFromXprv } from '../safe/safeDerivation'; |
| 7 | +import { IncorrectPasswordError } from '../errors'; |
| 8 | + |
| 9 | +export class InvalidRootKeychainSourceError extends Error { |
| 10 | + constructor(id: string, source: string | undefined) { |
| 11 | + super( |
| 12 | + `Root keychain ${id} has source '${source ?? 'unknown'}'; expected 'user'. ` + |
| 13 | + `Using a backup or BitGo root would fail at signing.` |
| 14 | + ); |
| 15 | + this.name = 'InvalidRootKeychainSourceError'; |
| 16 | + } |
| 17 | +} |
| 18 | + |
| 19 | +/** Thrown when hardened derivation does not match the registered child public key. */ |
| 20 | +export class SafeDerivedPublicKeyMismatchError extends Error { |
| 21 | + constructor(walletId: string, expectedPub: string, derivedPub: string) { |
| 22 | + super( |
| 23 | + `Safe wallet ${walletId}: derived child public key does not match the registered user key. ` + |
| 24 | + `Expected ${expectedPub}, got ${derivedPub}.` |
| 25 | + ); |
| 26 | + this.name = 'SafeDerivedPublicKeyMismatchError'; |
| 27 | + } |
| 28 | +} |
| 29 | + |
| 30 | +/** Thrown when owner signing is not implemented for this safe slot (TSS, ed25519 multisig, …). */ |
| 31 | +export class SafeOwnerSigningNotImplementedError extends Error { |
| 32 | + constructor(walletId: string, detail: string) { |
| 33 | + super(`Safe wallet ${walletId}: ${detail}`); |
| 34 | + this.name = 'SafeOwnerSigningNotImplementedError'; |
| 35 | + } |
| 36 | +} |
| 37 | + |
| 38 | +/** ed25519 onchain multisig (slot ④). Needs SLIP-0010, not secp256k1 BIP32. */ |
| 39 | +const ED25519_ONCHAIN_FAMILIES = new Set(['algo', 'xlm', 'hbar']); |
| 40 | + |
| 41 | +/** |
| 42 | + * True when this is the safe minter's user key: wallet is in a safe, the key |
| 43 | + * has a parent root, and there is no child-level encryptedPrv (sharees have one). |
| 44 | + */ |
| 45 | +export function isSafeChildPublicOnlyKeychain( |
| 46 | + walletSafeId: string | undefined, |
| 47 | + keychain: Keychain | undefined |
| 48 | +): keychain is Keychain & { parent: string } { |
| 49 | + return !!(walletSafeId && keychain?.parent && !keychain.encryptedPrv); |
| 50 | +} |
| 51 | + |
| 52 | +/** |
| 53 | + * Fetch the root user keychain for a safe child key. |
| 54 | + * Requires `source === 'user'` so a misconfigured parent fails early. |
| 55 | + */ |
| 56 | +export async function fetchRootKeychainForSafeChild( |
| 57 | + keychains: IKeychains, |
| 58 | + childKeychain: Keychain |
| 59 | +): Promise<KeychainWithEncryptedPrv> { |
| 60 | + if (!childKeychain.parent) { |
| 61 | + throw new Error('childKeychain.parent is required to fetch the root keychain'); |
| 62 | + } |
| 63 | + const root = await keychains.get({ id: childKeychain.parent }); |
| 64 | + if (root.source !== 'user') { |
| 65 | + throw new InvalidRootKeychainSourceError(root.id, root.source); |
| 66 | + } |
| 67 | + if (!root.encryptedPrv) { |
| 68 | + throw new Error(`root keychain ${root.id} does not have property encryptedPrv`); |
| 69 | + } |
| 70 | + return root as KeychainWithEncryptedPrv; |
| 71 | +} |
| 72 | + |
| 73 | +export interface ResolveSafeOwnerSigningPrvParams { |
| 74 | + bitgo: BitGoBase; |
| 75 | + keychains: IKeychains; |
| 76 | + walletId: string; |
| 77 | + /** Onchain secp256k1: hardened-derive and verify pub. Other slots throw. */ |
| 78 | + multisigType: string | undefined; |
| 79 | + coinFamily: string; |
| 80 | + childKeychain: Keychain; |
| 81 | + walletPassphrase: string; |
| 82 | + /** When already fetched (passphrase preflight), skip a second GET. */ |
| 83 | + rootKeychain?: KeychainWithEncryptedPrv; |
| 84 | +} |
| 85 | + |
| 86 | +/** |
| 87 | + * Resolve signing material for a safe owner (child key has no encryptedPrv). |
| 88 | + * |
| 89 | + * Onchain secp256k1: decrypt root → hardened-derive at `derivedFromParentWithSeed` → |
| 90 | + * verify derived pub against the registered child pub. |
| 91 | + * TSS and ed25519 onchain: throw — do not return root material or BIP32-derive the wrong curve. |
| 92 | + * |
| 93 | + * Do not use for wallet sharing — that must not receive root key material. |
| 94 | + * Call only when `isSafeChildPublicOnlyKeychain` is true. |
| 95 | + */ |
| 96 | +export async function resolveSafeOwnerSigningPrv(params: ResolveSafeOwnerSigningPrvParams): Promise<string> { |
| 97 | + const { bitgo, keychains, walletId, multisigType, coinFamily, childKeychain, walletPassphrase } = params; |
| 98 | + |
| 99 | + if (multisigType !== 'onchain') { |
| 100 | + throw new SafeOwnerSigningNotImplementedError( |
| 101 | + walletId, |
| 102 | + 'TSS owner signing from the root keyshare is not implemented. ' + |
| 103 | + 'Returning the root private key would expose material that can derive every child in this slot.' |
| 104 | + ); |
| 105 | + } |
| 106 | + if (ED25519_ONCHAIN_FAMILIES.has(coinFamily)) { |
| 107 | + throw new SafeOwnerSigningNotImplementedError( |
| 108 | + walletId, |
| 109 | + `ed25519 multisig owner derivation (${coinFamily}) is not implemented; BIP32 would produce the wrong child key.` |
| 110 | + ); |
| 111 | + } |
| 112 | + |
| 113 | + const rootKeychain = params.rootKeychain ?? (await fetchRootKeychainForSafeChild(keychains, childKeychain)); |
| 114 | + const rootPrv = await decryptKeychainPrivateKey(bitgo, rootKeychain, walletPassphrase); |
| 115 | + if (!rootPrv) { |
| 116 | + throw new IncorrectPasswordError(); |
| 117 | + } |
| 118 | + |
| 119 | + if (childKeychain.derivedFromParentWithSeed === undefined) { |
| 120 | + throw new Error(`Safe wallet ${walletId}: child keychain is missing derivedFromParentWithSeed (derivation index)`); |
| 121 | + } |
| 122 | + |
| 123 | + const derived = deriveSafeChildHardenedFromXprv(rootPrv, childKeychain.derivedFromParentWithSeed); |
| 124 | + |
| 125 | + if (!childKeychain.pub) { |
| 126 | + throw new Error(`Safe wallet ${walletId}: child keychain is missing pub for pre-sign verification`); |
| 127 | + } |
| 128 | + if (derived.pub !== childKeychain.pub) { |
| 129 | + throw new SafeDerivedPublicKeyMismatchError(walletId, childKeychain.pub, derived.pub); |
| 130 | + } |
| 131 | + |
| 132 | + return derived.prv; |
| 133 | +} |
0 commit comments