PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-core/src/bitgo/utils/tss/ecdsa
Просмотр файла: ecdsaMPCv2.ts
import { bigIntToBufferBE, DklsComms, DklsDkg, DklsDsg, DklsTypes, DklsUtils } from '@bitgo/sdk-lib-mpc';
import * as sjcl from '@bitgo/sjcl';
import assert from 'assert';
import { Buffer } from 'buffer';
import { Hash } from 'crypto';
import { NonEmptyString } from 'io-ts-types';
import createKeccakHash from 'keccak';
import * as pgp from 'openpgp';
import { KeychainsTriplet } from '../../../baseCoin';
import {
MPCv2BroadcastMessage,
MPCv2KeyGenRound1Response,
MPCv2KeyGenRound2Response,
MPCv2KeyGenRound3Response,
MPCv2KeyGenStateEnum,
MPCv2P2PMessage,
MPCv2PartyFromStringOrNumber,
MPCv2SignatureShareRound1Output,
MPCv2SignatureShareRound2Output,
} from '@bitgo/public-types';
import { Ecdsa } from '../../../../account-lib';
import { AddKeychainOptions, Keychain, KeyType } from '../../../keychain';
import { DecryptedRetrofitPayload } from '../../../keychain/iKeychains';
import { ECDSAMethodTypes, getTxRequest } from '../../../tss';
import { sendSignatureShareV2, sendTxRequest } from '../../../tss/common';
import { MPCv2PartiesEnum } from './typesMPCv2';
import {
getSignatureShareRoundOne,
getSignatureShareRoundThree,
getSignatureShareRoundTwo,
verifyBitGoMessagesAndSignaturesRoundOne,
verifyBitGoMessagesAndSignaturesRoundTwo,
} from '../../../tss/ecdsa/ecdsaMPCv2';
import { KeyCombined } from '../../../tss/ecdsa/types';
import { generateGPGKeyPair } from '../../opengpgUtils';
import {
CustomMPCv2SigningRound1GeneratingFunction,
CustomMPCv2SigningRound2GeneratingFunction,
CustomMPCv2SigningRound3GeneratingFunction,
RequestType,
SignatureShareRecord,
TSSParams,
TSSParamsForMessage,
TSSParamsForMessageWithPrv,
TSSParamsWithPrv,
TxRequest,
} from '../baseTypes';
import { BaseEcdsaUtils } from './base';
import { EcdsaMPCv2KeyGenSendFn, KeyGenSenderForEnterprise } from './ecdsaMPCv2KeyGenSender';
import { envRequiresBitgoPubGpgKeyConfig, isBitgoMpcPubKey } from '../../../tss/bitgoPubKeys';
export class EcdsaMPCv2Utils extends BaseEcdsaUtils {
/** @inheritdoc */
async createKeychains(params: {
passphrase: string;
enterprise: string;
originalPasscodeEncryptionCode?: string;
retrofit?: DecryptedRetrofitPayload;
}): Promise<KeychainsTriplet> {
const { userSession, backupSession } = this.getUserAndBackupSession(2, 3, params.retrofit);
const userGpgKey = await generateGPGKeyPair('secp256k1');
const backupGpgKey = await generateGPGKeyPair('secp256k1');
// Get the BitGo public key based on user/enterprise feature flags
// If it doesn't work, use the default public key from the constants
const bitgoPublicGpgKey = (
(await this.getBitgoGpgPubkeyBasedOnFeatureFlags(params.enterprise, true)) ?? this.bitgoMPCv2PublicGpgKey
).armor();
if (envRequiresBitgoPubGpgKeyConfig(this.bitgo.getEnv())) {
// Ensure the public key is one of the expected BitGo public keys when in test or prod.
assert(isBitgoMpcPubKey(bitgoPublicGpgKey, 'mpcv2'), 'Invalid BitGo GPG public key');
}
const userGpgPrvKey: DklsTypes.PartyGpgKey = {
partyId: MPCv2PartiesEnum.USER,
gpgKey: userGpgKey.privateKey,
};
const backupGpgPrvKey: DklsTypes.PartyGpgKey = {
partyId: MPCv2PartiesEnum.BACKUP,
gpgKey: backupGpgKey.privateKey,
};
const bitgoGpgPubKey: DklsTypes.PartyGpgKey = {
partyId: MPCv2PartiesEnum.BITGO,
gpgKey: bitgoPublicGpgKey,
};
// #region round 1
const userRound1BroadcastMsg = await userSession.initDkg();
const backupRound1BroadcastMsg = await backupSession.initDkg();
const round1SerializedMessages = DklsTypes.serializeMessages({
broadcastMessages: [userRound1BroadcastMsg, backupRound1BroadcastMsg],
p2pMessages: [],
});
const round1Messages = await DklsComms.encryptAndAuthOutgoingMessages(
round1SerializedMessages,
[bitgoGpgPubKey],
[userGpgPrvKey, backupGpgPrvKey]
);
const { sessionId, bitgoMsg1, bitgoToBackupMsg2, bitgoToUserMsg2 } = await this.sendKeyGenerationRound1(
params.enterprise,
userGpgKey.publicKey,
backupGpgKey.publicKey,
params.retrofit?.walletId
? {
...round1Messages,
walletId: params.retrofit.walletId,
}
: round1Messages
);
// #endregion
// #region round 2
const bitgoRound1BroadcastMessages = await DklsComms.decryptAndVerifyIncomingMessages(
{ p2pMessages: [], broadcastMessages: [this.formatBitgoBroadcastMessage(bitgoMsg1)] },
[bitgoGpgPubKey],
[userGpgPrvKey, backupGpgPrvKey]
);
const bitgoRound1BroadcastMsg = bitgoRound1BroadcastMessages.broadcastMessages.find(
(m) => m.from === MPCv2PartiesEnum.BITGO
);
assert(bitgoRound1BroadcastMsg, 'BitGo message 1 not found in broadcast messages');
const userRound2P2PMessages = userSession.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: [DklsTypes.deserializeBroadcastMessage(bitgoRound1BroadcastMsg), backupRound1BroadcastMsg],
});
const userToBitgoMsg2 = userRound2P2PMessages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BITGO
);
assert(userToBitgoMsg2, 'User message 2 not found in P2P messages');
const serializedUserToBitgoMsg2 = DklsTypes.serializeP2PMessage(userToBitgoMsg2);
const backupRound2P2PMessages = backupSession.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: [userRound1BroadcastMsg, DklsTypes.deserializeBroadcastMessage(bitgoRound1BroadcastMsg)],
});
const serializedBackupToBitgoMsg2 = DklsTypes.serializeMessages(backupRound2P2PMessages).p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.BITGO
);
assert(serializedBackupToBitgoMsg2, 'Backup message 2 not found in P2P messages');
const round2Messages = await DklsComms.encryptAndAuthOutgoingMessages(
{ p2pMessages: [serializedUserToBitgoMsg2, serializedBackupToBitgoMsg2], broadcastMessages: [] },
[bitgoGpgPubKey],
[userGpgPrvKey, backupGpgPrvKey]
);
const {
sessionId: sessionIdRound2,
bitgoCommitment2,
bitgoToUserMsg3,
bitgoToBackupMsg3,
} = await this.sendKeyGenerationRound2(params.enterprise, sessionId, round2Messages);
// #endregion
// #region round 3
assert.equal(sessionId, sessionIdRound2, 'Round 1 and 2 Session IDs do not match');
const decryptedBitgoToUserRound2Msgs = await DklsComms.decryptAndVerifyIncomingMessages(
{ p2pMessages: [this.formatP2PMessage(bitgoToUserMsg2)], broadcastMessages: [] },
[bitgoGpgPubKey],
[userGpgPrvKey]
);
const serializedBitgoToUserRound2Msg = decryptedBitgoToUserRound2Msgs.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.USER
);
assert(serializedBitgoToUserRound2Msg, 'BitGo to User message 2 not found in P2P messages');
const bitgoToUserRound2Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToUserRound2Msg);
const decryptedBitgoToBackupRound2Msg = await DklsComms.decryptAndVerifyIncomingMessages(
{ p2pMessages: [this.formatP2PMessage(bitgoToBackupMsg2)], broadcastMessages: [] },
[bitgoGpgPubKey],
[backupGpgPrvKey]
);
const serializedBitgoToBackupRound2Msg = decryptedBitgoToBackupRound2Msg.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.BACKUP
);
assert(serializedBitgoToBackupRound2Msg, 'BitGo to Backup message 2 not found in P2P messages');
const bitgoToBackupRound2Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToBackupRound2Msg);
const userToBackupMsg2 = userRound2P2PMessages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BACKUP
);
assert(userToBackupMsg2, 'User to Backup message 2 not found in P2P messages');
const backupToUserMsg2 = backupRound2P2PMessages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.USER
);
assert(backupToUserMsg2, 'Backup to User message 2 not found in P2P messages');
const userRound3Messages = userSession.handleIncomingMessages({
broadcastMessages: [],
p2pMessages: [bitgoToUserRound2Msg, backupToUserMsg2],
});
const userToBackupMsg3 = userRound3Messages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BACKUP
);
assert(userToBackupMsg3, 'User to Backup message 3 not found in P2P messages');
const userToBitgoMsg3 = userRound3Messages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BITGO
);
assert(userToBitgoMsg3, 'User to Bitgo message 3 not found in P2P messages');
const serializedUserToBitgoMsg3 = DklsTypes.serializeP2PMessage(userToBitgoMsg3);
const backupRound3Messages = backupSession.handleIncomingMessages({
broadcastMessages: [],
p2pMessages: [bitgoToBackupRound2Msg, userToBackupMsg2],
});
const backupToUserMsg3 = backupRound3Messages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.USER
);
assert(backupToUserMsg3, 'Backup to User message 3 not found in P2P messages');
const backupToBitgoMsg3 = backupRound3Messages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.BITGO
);
assert(backupToBitgoMsg3, 'Backup to Bitgo message 3 not found in P2P messages');
const serializedBackupToBitgoMsg3 = DklsTypes.serializeP2PMessage(backupToBitgoMsg3);
const decryptedBitgoToUserRound3Messages = await DklsComms.decryptAndVerifyIncomingMessages(
{ broadcastMessages: [], p2pMessages: [this.formatP2PMessage(bitgoToUserMsg3, bitgoCommitment2)] },
[bitgoGpgPubKey],
[userGpgPrvKey]
);
const serializedBitgoToUserRound3Msg = decryptedBitgoToUserRound3Messages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.USER
);
assert(serializedBitgoToUserRound3Msg, 'BitGo to User message 3 not found in P2P messages');
const bitgoToUserRound3Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToUserRound3Msg);
const decryptedBitgoToBackupRound3Messages = await DklsComms.decryptAndVerifyIncomingMessages(
{ broadcastMessages: [], p2pMessages: [this.formatP2PMessage(bitgoToBackupMsg3, bitgoCommitment2)] },
[bitgoGpgPubKey],
[backupGpgPrvKey]
);
const serializedBitgoToBackupRound3Msg = decryptedBitgoToBackupRound3Messages.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BITGO && m.to === MPCv2PartiesEnum.BACKUP
);
assert(serializedBitgoToBackupRound3Msg, 'BitGo to Backup message 3 not found in P2P messages');
const bitgoToBackupRound3Msg = DklsTypes.deserializeP2PMessage(serializedBitgoToBackupRound3Msg);
const userRound4Messages = userSession.handleIncomingMessages({
p2pMessages: [backupToUserMsg3, bitgoToUserRound3Msg],
broadcastMessages: [],
});
const userRound4BroadcastMsg = userRound4Messages.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.USER);
assert(userRound4BroadcastMsg, 'User message 4 not found in broadcast messages');
const serializedUserRound4BroadcastMsg = DklsTypes.serializeBroadcastMessage(userRound4BroadcastMsg);
const backupRound4Messages = backupSession.handleIncomingMessages({
p2pMessages: [userToBackupMsg3, bitgoToBackupRound3Msg],
broadcastMessages: [],
});
const backupRound4BroadcastMsg = backupRound4Messages.broadcastMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP
);
assert(backupRound4BroadcastMsg, 'Backup message 4 not found in broadcast messages');
const serializedBackupRound4BroadcastMsg = DklsTypes.serializeBroadcastMessage(backupRound4BroadcastMsg);
const round3Messages = await DklsComms.encryptAndAuthOutgoingMessages(
{
p2pMessages: [serializedUserToBitgoMsg3, serializedBackupToBitgoMsg3],
broadcastMessages: [serializedUserRound4BroadcastMsg, serializedBackupRound4BroadcastMsg],
},
[bitgoGpgPubKey],
[userGpgPrvKey, backupGpgPrvKey]
);
const {
sessionId: sessionIdRound3,
bitgoMsg4,
commonKeychain: bitgoCommonKeychain,
} = await this.sendKeyGenerationRound3(params.enterprise, sessionId, round3Messages);
// #endregion
// #region keychain creation
assert.equal(sessionId, sessionIdRound3, 'Round 1 and 3 Session IDs do not match');
const bitgoRound4BroadcastMessages = DklsTypes.deserializeMessages(
await DklsComms.decryptAndVerifyIncomingMessages(
{ p2pMessages: [], broadcastMessages: [this.formatBitgoBroadcastMessage(bitgoMsg4)] },
[bitgoGpgPubKey],
[]
)
).broadcastMessages;
const bitgoRound4BroadcastMsg = bitgoRound4BroadcastMessages.find((m) => m.from === MPCv2PartiesEnum.BITGO);
assert(bitgoRound4BroadcastMsg, 'BitGo message 4 not found in broadcast messages');
userSession.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: [bitgoRound4BroadcastMsg, backupRound4BroadcastMsg],
});
backupSession.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: [bitgoRound4BroadcastMsg, userRound4BroadcastMsg],
});
const userPrivateMaterial = userSession.getKeyShare();
const backupPrivateMaterial = backupSession.getKeyShare();
const userReducedPrivateMaterial = userSession.getReducedKeyShare();
const backupReducedPrivateMaterial = backupSession.getReducedKeyShare();
const userCommonKeychain = DklsTypes.getCommonKeychain(userPrivateMaterial);
const backupCommonKeychain = DklsTypes.getCommonKeychain(backupPrivateMaterial);
assert.equal(bitgoCommonKeychain, userCommonKeychain, 'User and Bitgo Common keychains do not match');
assert.equal(bitgoCommonKeychain, backupCommonKeychain, 'Backup and Bitgo Common keychains do not match');
const userKeychainPromise = this.addUserKeychain(
bitgoCommonKeychain,
userPrivateMaterial,
userReducedPrivateMaterial,
params.passphrase,
params.originalPasscodeEncryptionCode
);
const backupKeychainPromise = this.addBackupKeychain(
bitgoCommonKeychain,
userPrivateMaterial,
backupReducedPrivateMaterial,
params.passphrase,
params.originalPasscodeEncryptionCode
);
const bitgoKeychainPromise = this.addBitgoKeychain(bitgoCommonKeychain);
const [userKeychain, backupKeychain, bitgoKeychain] = await Promise.all([
userKeychainPromise,
backupKeychainPromise,
bitgoKeychainPromise,
]);
// #endregion
return {
userKeychain,
backupKeychain,
bitgoKeychain,
};
}
// #region keychain utils
async createParticipantKeychain(
participantIndex: MPCv2PartyFromStringOrNumber,
commonKeychain: string,
privateMaterial?: Buffer,
reducedPrivateMaterial?: Buffer,
passphrase?: string,
originalPasscodeEncryptionCode?: string
): Promise<Keychain> {
let source: string;
let encryptedPrv: string | undefined = undefined;
let reducedEncryptedPrv: string | undefined = undefined;
switch (participantIndex) {
case MPCv2PartiesEnum.USER:
case MPCv2PartiesEnum.BACKUP:
source = participantIndex === MPCv2PartiesEnum.USER ? 'user' : 'backup';
assert(privateMaterial, `Private material is required for ${source} keychain`);
assert(reducedPrivateMaterial, `Reduced private material is required for ${source} keychain`);
assert(passphrase, `Passphrase is required for ${source} keychain`);
encryptedPrv = this.bitgo.encrypt({
input: privateMaterial.toString('base64'),
password: passphrase,
});
reducedEncryptedPrv = this.bitgo.encrypt({
// Buffer.toString('base64') can not be used here as it does not work on the browser.
// The browser deals with a Buffer as Uint8Array, therefore in the browser .toString('base64') just creates a comma seperated string of the array values.
input: btoa(String.fromCharCode.apply(null, Array.from(new Uint8Array(reducedPrivateMaterial)))),
password: passphrase,
});
break;
case MPCv2PartiesEnum.BITGO:
source = 'bitgo';
break;
default:
throw new Error('Invalid participant index');
}
const recipientKeychainParams: AddKeychainOptions = {
source,
keyType: 'tss' as KeyType,
commonKeychain,
encryptedPrv,
originalPasscodeEncryptionCode,
isMPCv2: true,
};
const keychains = this.baseCoin.keychains();
return { ...(await keychains.add(recipientKeychainParams)), reducedEncryptedPrv: reducedEncryptedPrv };
}
/**
* Converts a User or Backup MPCv1 SigningMaterial to RetrofitData needed by MPCv2 DKG.
*
* @param decryptedKeyshare - MPCv1 decrypted signing material for user or backup as a json.stringify string and bitgo's Big Si.
* @param partyId - The party ID of the MPCv1 keyshare.
* @returns The retrofit data needed to start an MPCv2 DKG session.
* @deprecated
*/
static getKeyDataForRetrofit(
decryptedKeyshare: string,
partyId: MPCv2PartiesEnum.BACKUP | MPCv2PartiesEnum.USER
): DklsTypes.RetrofitData {
const mpc = new Ecdsa();
const xiList = [
Array.from(bigIntToBufferBE(BigInt(1), 32)),
Array.from(bigIntToBufferBE(BigInt(2), 32)),
Array.from(bigIntToBufferBE(BigInt(3), 32)),
];
return EcdsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key({
mpcv1PartyKeyShare: decryptedKeyshare,
mpcv1PartyIndex: partyId === MPCv2PartiesEnum.USER ? 1 : 2,
xiList,
mpc,
});
}
/**
* Converts user and backup MPCv1 SigningMaterial to RetrofitData needed by MPCv2 DKG.
*
* @param {Object} params - MPCv1 decrypted signing material for user and backup as a json.stringify string and bitgo's Big Si.
* @returns {{ mpcv2UserKeyShare: DklsTypes.RetrofitData; mpcv2BakcupKeyShare: DklsTypes.RetrofitData }} - the retrofit data needed to start an MPCv2 DKG session.
*/
getMpcV2RetrofitDataFromMpcV1Keys(params: { mpcv1UserKeyShare: string; mpcv1BackupKeyShare: string }): {
mpcv2UserKeyShare: DklsTypes.RetrofitData;
mpcv2BackupKeyShare: DklsTypes.RetrofitData;
} {
const mpc = new Ecdsa();
const xiList = [
Array.from(bigIntToBufferBE(BigInt(1), 32)),
Array.from(bigIntToBufferBE(BigInt(2), 32)),
Array.from(bigIntToBufferBE(BigInt(3), 32)),
];
return {
mpcv2UserKeyShare: EcdsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key({
mpcv1PartyKeyShare: params.mpcv1UserKeyShare,
mpcv1PartyIndex: 1,
xiList,
mpc,
}),
mpcv2BackupKeyShare: EcdsaMPCv2Utils.getMpcV2RetrofitDataFromMpcV1Key({
mpcv1PartyKeyShare: params.mpcv1BackupKeyShare,
mpcv1PartyIndex: 2,
xiList,
mpc,
}),
};
}
/**
* Get retrofit data from MPCv1 key share.
* @param mpcv1PartyKeyShare
* @param mpcv1PartyIndex
* @param xiList
* @param mpc
* @deprecated
*/
static getMpcV2RetrofitDataFromMpcV1Key({
mpcv1PartyKeyShare,
mpcv1PartyIndex,
xiList,
mpc,
}: {
mpcv1PartyKeyShare: string;
mpcv1PartyIndex: number;
xiList: number[][];
mpc: Ecdsa;
}): DklsTypes.RetrofitData {
const signingMaterial: ECDSAMethodTypes.SigningMaterial = JSON.parse(mpcv1PartyKeyShare);
let keyCombined: KeyCombined | undefined = undefined;
switch (mpcv1PartyIndex) {
case 1:
assert(signingMaterial.backupNShare, 'User MPCv1 key material should have backup NShare.');
assert(signingMaterial.bitgoNShare, 'BitGo MPCv1 key material should have user NShare.');
keyCombined = mpc.keyCombine(signingMaterial.pShare, [
signingMaterial.backupNShare,
signingMaterial.bitgoNShare,
]);
break;
case 2:
assert(signingMaterial.userNShare, 'User MPCv1 key material should have backup NShare.');
assert(signingMaterial.bitgoNShare, 'BitGo MPCv1 key material should have user NShare.');
keyCombined = mpc.keyCombine(signingMaterial.pShare, [signingMaterial.userNShare, signingMaterial.bitgoNShare]);
break;
case 3:
assert(signingMaterial.userNShare, 'User MPCv1 key material should have backup NShare.');
assert(signingMaterial.backupNShare, 'Backup MPCv1 key material should have user NShare.');
keyCombined = mpc.keyCombine(signingMaterial.pShare, [
signingMaterial.userNShare,
signingMaterial.backupNShare,
]);
break;
default:
throw new Error('Invalid participant index');
}
return {
xShare: keyCombined.xShare,
xiList: xiList,
};
}
private async addUserKeychain(
commonKeychain: string,
privateMaterial: Buffer,
reducedPrivateMaterial: Buffer,
passphrase: string,
originalPasscodeEncryptionCode?: string
): Promise<Keychain> {
return this.createParticipantKeychain(
MPCv2PartiesEnum.USER,
commonKeychain,
privateMaterial,
reducedPrivateMaterial,
passphrase,
originalPasscodeEncryptionCode
);
}
private async addBackupKeychain(
commonKeychain: string,
privateMaterial: Buffer,
reducedPrivateMaterial: Buffer,
passphrase: string,
originalPasscodeEncryptionCode?: string
): Promise<Keychain> {
return this.createParticipantKeychain(
MPCv2PartiesEnum.BACKUP,
commonKeychain,
privateMaterial,
reducedPrivateMaterial,
passphrase,
originalPasscodeEncryptionCode
);
}
private getUserAndBackupSession(m: number, n: number, retrofit?: DecryptedRetrofitPayload) {
if (retrofit) {
const retrofitData = this.getMpcV2RetrofitDataFromMpcV1Keys({
mpcv1UserKeyShare: retrofit.decryptedUserKey,
mpcv1BackupKeyShare: retrofit.decryptedBackupKey,
});
const userSession = new DklsDkg.Dkg(n, m, MPCv2PartiesEnum.USER, undefined, retrofitData.mpcv2UserKeyShare);
const backupSession = new DklsDkg.Dkg(n, m, MPCv2PartiesEnum.BACKUP, undefined, retrofitData.mpcv2BackupKeyShare);
return { userSession, backupSession };
}
const userSession = new DklsDkg.Dkg(n, m, MPCv2PartiesEnum.USER);
const backupSession = new DklsDkg.Dkg(n, m, MPCv2PartiesEnum.BACKUP);
return { userSession, backupSession };
}
private async addBitgoKeychain(commonKeychain: string): Promise<Keychain> {
return this.createParticipantKeychain(MPCv2PartiesEnum.BITGO, commonKeychain);
}
// #endregion
async sendKeyGenerationRound1(
enterprise: string,
userGpgPublicKey: string,
backupGpgPublicKey: string,
payload: DklsTypes.AuthEncMessages & { walletId?: string }
): Promise<MPCv2KeyGenRound1Response> {
return this.sendKeyGenerationRound1BySender(
KeyGenSenderForEnterprise(this.bitgo, enterprise),
userGpgPublicKey,
backupGpgPublicKey,
payload
);
}
async sendKeyGenerationRound2(
enterprise: string,
sessionId: string,
payload: DklsTypes.AuthEncMessages
): Promise<MPCv2KeyGenRound2Response> {
return this.sendKeyGenerationRound2BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise), sessionId, payload);
}
async sendKeyGenerationRound3(
enterprise: string,
sessionId: string,
payload: DklsTypes.AuthEncMessages
): Promise<MPCv2KeyGenRound3Response> {
return this.sendKeyGenerationRound3BySender(KeyGenSenderForEnterprise(this.bitgo, enterprise), sessionId, payload);
}
async sendKeyGenerationRound1BySender(
senderFn: EcdsaMPCv2KeyGenSendFn<MPCv2KeyGenRound1Response>,
userGpgPublicKey: string,
backupGpgPublicKey: string,
payload: DklsTypes.AuthEncMessages & { walletId?: string }
): Promise<MPCv2KeyGenRound1Response> {
assert(NonEmptyString.is(userGpgPublicKey), 'User GPG public key is required');
assert(NonEmptyString.is(backupGpgPublicKey), 'Backup GPG public key is required');
const userMsg1 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.USER)?.payload;
assert(userMsg1, 'User message 1 not found in broadcast messages');
const backupMsg1 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.BACKUP)?.payload;
assert(backupMsg1, 'Backup message 1 not found in broadcast messages');
return senderFn(MPCv2KeyGenStateEnum['MPCv2-R1'], {
userGpgPublicKey,
backupGpgPublicKey,
userMsg1: { from: 0, ...userMsg1 },
backupMsg1: { from: 1, ...backupMsg1 },
walletId: payload.walletId,
});
}
async sendKeyGenerationRound2BySender(
senderFn: EcdsaMPCv2KeyGenSendFn<MPCv2KeyGenRound2Response>,
sessionId: string,
payload: DklsTypes.AuthEncMessages
): Promise<MPCv2KeyGenRound2Response> {
assert(NonEmptyString.is(sessionId), 'Session ID is required');
const userMsg2 = payload.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BITGO
);
assert(userMsg2, 'User to Bitgo message 2 not found in P2P messages');
assert(userMsg2.commitment, 'User to Bitgo commitment not found in P2P messages');
assert(NonEmptyString.is(userMsg2.commitment), 'User to Bitgo commitment is required');
const backupMsg2 = payload.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.BITGO
);
assert(backupMsg2, 'Backup to Bitgo message 2 not found in P2P messages');
assert(backupMsg2.commitment, 'Backup to Bitgo commitment not found in P2P messages');
assert(NonEmptyString.is(backupMsg2.commitment), 'Backup to Bitgo commitment is required');
return senderFn(MPCv2KeyGenStateEnum['MPCv2-R2'], {
sessionId,
userMsg2: {
from: MPCv2PartiesEnum.USER,
to: MPCv2PartiesEnum.BITGO,
signature: userMsg2.payload.signature,
encryptedMessage: userMsg2.payload.encryptedMessage,
},
userCommitment2: userMsg2.commitment,
backupMsg2: {
from: MPCv2PartiesEnum.BACKUP,
to: MPCv2PartiesEnum.BITGO,
signature: backupMsg2.payload.signature,
encryptedMessage: backupMsg2.payload.encryptedMessage,
},
backupCommitment2: backupMsg2.commitment,
});
}
async sendKeyGenerationRound3BySender(
senderFn: EcdsaMPCv2KeyGenSendFn<MPCv2KeyGenRound3Response>,
sessionId: string,
payload: DklsTypes.AuthEncMessages
): Promise<MPCv2KeyGenRound3Response> {
assert(NonEmptyString.is(sessionId), 'Session ID is required');
const userMsg3 = payload.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.USER && m.to === MPCv2PartiesEnum.BITGO
)?.payload;
assert(userMsg3, 'User to Bitgo message 3 not found in P2P messages');
const backupMsg3 = payload.p2pMessages.find(
(m) => m.from === MPCv2PartiesEnum.BACKUP && m.to === MPCv2PartiesEnum.BITGO
)?.payload;
assert(backupMsg3, 'Backup to Bitgo message 3 not found in P2P messages');
const userMsg4 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.USER)?.payload;
assert(userMsg4, 'User message 1 not found in broadcast messages');
const backupMsg4 = payload.broadcastMessages.find((m) => m.from === MPCv2PartiesEnum.BACKUP)?.payload;
assert(backupMsg4, 'Backup message 1 not found in broadcast messages');
return senderFn(MPCv2KeyGenStateEnum['MPCv2-R3'], {
sessionId,
userMsg3: { from: 0, to: 2, ...userMsg3 },
backupMsg3: { from: 1, to: 2, ...backupMsg3 },
userMsg4: { from: 0, ...userMsg4 },
backupMsg4: { from: 1, ...backupMsg4 },
});
}
// #endregion
// #region sign tx request
/**
* Signs the transaction associated to the transaction request.
* @param {string | TxRequest} params.txRequest - transaction request object or id
* @param {string} params.prv - decrypted private key
* @param {string} params.reqId - request id
* @param {string} params.mpcv2PartyId - party id for the signer involved in this mpcv2 request (either 0 for user or 1 for backup)
* @returns {Promise<TxRequest>} fully signed TxRequest object
*/
async signTxRequest(params: TSSParamsWithPrv): Promise<TxRequest> {
this.bitgo.setRequestTracer(params.reqId);
return this.signRequestBase(params, RequestType.tx);
}
/**
* Signs the message associated to the transaction request.
* @param {string | TxRequest} params.txRequest - transaction request object or id
* @param {string} params.prv - decrypted private key
* @param {string} params.reqId - request id
* @returns {Promise<TxRequest>} fully signed TxRequest object
*/
async signTxRequestForMessage(params: TSSParamsForMessageWithPrv): Promise<TxRequest> {
this.bitgo.setRequestTracer(params.reqId);
return this.signRequestBase(params, RequestType.message);
}
private async signRequestBase(
params: TSSParamsWithPrv | TSSParamsForMessageWithPrv,
requestType: RequestType
): Promise<TxRequest> {
const userKeyShare = Buffer.from(params.prv, 'base64');
const txRequest: TxRequest =
typeof params.txRequest === 'string'
? await getTxRequest(this.bitgo, this.wallet.id(), params.txRequest, params.reqId)
: params.txRequest;
let txOrMessageToSign;
let derivationPath;
let bufferContent;
const userGpgKey = await generateGPGKeyPair('secp256k1');
const bitgoGpgPubKey = await this.pickBitgoPubGpgKeyForSigning(true, params.reqId, txRequest.enterpriseId);
if (!bitgoGpgPubKey) {
throw new Error('Missing BitGo GPG key for MPCv2');
}
if (requestType === RequestType.tx) {
assert(txRequest.transactions || txRequest.unsignedTxs, 'Unable to find transactions in txRequest');
const unsignedTx =
txRequest.apiVersion === 'full' ? txRequest.transactions![0].unsignedTx : txRequest.unsignedTxs[0];
// For ICP transactions, the HSM signs the serializedTxHex, while the user signs the signableHex separately.
// Verification cannot be performed directly on the signableHex alone. However, we can parse the serializedTxHex
// to regenerate the signableHex and compare it against the provided value for verification.
// In contrast, for other coin families, verification is typically done using just the signableHex.
if (this.baseCoin.getConfig().family === 'icp') {
await this.baseCoin.verifyTransaction({
txPrebuild: { txHex: unsignedTx.serializedTxHex, txInfo: unsignedTx.signableHex },
txParams: params.txParams || { recipients: [] },
wallet: this.wallet,
walletType: this.wallet.multisigType(),
});
} else {
await this.baseCoin.verifyTransaction({
txPrebuild: { txHex: unsignedTx.signableHex },
txParams: params.txParams || { recipients: [] },
wallet: this.wallet,
walletType: this.wallet.multisigType(),
});
}
txOrMessageToSign = unsignedTx.signableHex;
derivationPath = unsignedTx.derivationPath;
bufferContent = Buffer.from(txOrMessageToSign, 'hex');
} else if (requestType === RequestType.message) {
txOrMessageToSign = txRequest.messages![0].messageEncoded;
derivationPath = txRequest.messages![0].derivationPath || 'm/0';
bufferContent = Buffer.from(txOrMessageToSign, 'hex');
} else {
throw new Error('Invalid request type');
}
let hash: Hash;
try {
hash = this.baseCoin.getHashFunction();
} catch (err) {
hash = createKeccakHash('keccak256') as Hash;
}
// check what the encoding is supposed to be for message
const hashBuffer = hash.update(bufferContent).digest();
const otherSigner = new DklsDsg.Dsg(
userKeyShare,
params.mpcv2PartyId ? params.mpcv2PartyId : 0,
derivationPath,
hashBuffer
);
const userSignerBroadcastMsg1 = await otherSigner.init();
const signatureShareRound1 = await getSignatureShareRoundOne(
userSignerBroadcastMsg1,
userGpgKey,
params.mpcv2PartyId
);
let latestTxRequest = await sendSignatureShareV2(
this.bitgo,
txRequest.walletId,
txRequest.txRequestId,
[signatureShareRound1],
requestType,
this.baseCoin.getMPCAlgorithm(),
userGpgKey.publicKey,
undefined,
this.wallet.multisigTypeVersion(),
params.reqId
);
assert(latestTxRequest.transactions || latestTxRequest.messages, 'Invalid txRequest Object');
let bitgoToUserMessages1And2: any;
if (requestType === RequestType.tx) {
bitgoToUserMessages1And2 = latestTxRequest.transactions![0].signatureShares;
} else {
bitgoToUserMessages1And2 = latestTxRequest.messages![0].signatureShares;
}
// TODO: Use codec for parsing
const parsedBitGoToUserSigShareRoundOne = JSON.parse(
bitgoToUserMessages1And2[bitgoToUserMessages1And2.length - 1].share
) as MPCv2SignatureShareRound1Output;
if (parsedBitGoToUserSigShareRoundOne.type !== 'round1Output') {
throw new Error('Unexpected signature share response. Unable to parse data.');
}
const serializedBitGoToUserMessagesRound1And2 = await verifyBitGoMessagesAndSignaturesRoundOne(
parsedBitGoToUserSigShareRoundOne,
userGpgKey,
bitgoGpgPubKey,
params.mpcv2PartyId
);
/** Round 2 **/
const deserializedMessages = DklsTypes.deserializeMessages(serializedBitGoToUserMessagesRound1And2);
const userToBitGoMessagesRound2 = otherSigner.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: deserializedMessages.broadcastMessages,
});
const userToBitGoMessagesRound3 = otherSigner.handleIncomingMessages({
p2pMessages: deserializedMessages.p2pMessages,
broadcastMessages: [],
});
const signatureShareRoundTwo = await getSignatureShareRoundTwo(
userToBitGoMessagesRound2,
userToBitGoMessagesRound3,
userGpgKey,
bitgoGpgPubKey,
params.mpcv2PartyId
);
latestTxRequest = await sendSignatureShareV2(
this.bitgo,
txRequest.walletId,
txRequest.txRequestId,
[signatureShareRoundTwo],
requestType,
this.baseCoin.getMPCAlgorithm(),
userGpgKey.publicKey,
undefined,
this.wallet.multisigTypeVersion(),
params.reqId
);
assert(latestTxRequest.transactions || latestTxRequest.messages, 'Invalid txRequest Object');
const txRequestSignatureShares =
requestType === RequestType.tx
? latestTxRequest.transactions![0].signatureShares
: latestTxRequest.messages![0].signatureShares;
// TODO: Use codec for parsing
const parsedBitGoToUserSigShareRoundTwo = JSON.parse(
txRequestSignatureShares[txRequestSignatureShares.length - 1].share
) as MPCv2SignatureShareRound2Output;
if (parsedBitGoToUserSigShareRoundTwo.type !== 'round2Output') {
throw new Error('Unexpected signature share response. Unable to parse data.');
}
const serializedBitGoToUserMessagesRound3 = await verifyBitGoMessagesAndSignaturesRoundTwo(
parsedBitGoToUserSigShareRoundTwo,
userGpgKey,
bitgoGpgPubKey,
params.mpcv2PartyId
);
/** Round 3 **/
const deserializedBitGoToUserMessagesRound3 = DklsTypes.deserializeMessages({
p2pMessages: serializedBitGoToUserMessagesRound3.p2pMessages,
broadcastMessages: [],
});
const userToBitGoMessagesRound4 = otherSigner.handleIncomingMessages({
p2pMessages: deserializedBitGoToUserMessagesRound3.p2pMessages,
broadcastMessages: [],
});
const signatureShareRoundThree = await getSignatureShareRoundThree(
userToBitGoMessagesRound4,
userGpgKey,
bitgoGpgPubKey,
params.mpcv2PartyId
);
// Submit for final signature share combine
await sendSignatureShareV2(
this.bitgo,
txRequest.walletId,
txRequest.txRequestId,
[signatureShareRoundThree],
requestType,
this.baseCoin.getMPCAlgorithm(),
userGpgKey.publicKey,
undefined,
this.wallet.multisigTypeVersion(),
params.reqId
);
return sendTxRequest(this.bitgo, txRequest.walletId, txRequest.txRequestId, requestType, params.reqId);
}
// #endregion
// #region formatting utils
formatBitgoBroadcastMessage(broadcastMessage: MPCv2BroadcastMessage) {
return {
from: broadcastMessage.from,
payload: { message: broadcastMessage.message, signature: broadcastMessage.signature },
};
}
formatP2PMessage(p2pMessage: MPCv2P2PMessage, commitment?: string) {
return {
payload: { encryptedMessage: p2pMessage.encryptedMessage, signature: p2pMessage.signature },
from: p2pMessage.from,
to: p2pMessage.to,
commitment,
};
}
// #endregion
// #region private utils
/**
* Get the hash string and derivation path from the transaction request.
* @param {TxRequest} txRequest - the transaction request object
* @param {RequestType} requestType - the request type
* @returns {{ hashBuffer: Buffer; derivationPath: string }} - the hash string and derivation path
*/
private getHashStringAndDerivationPath(
txRequest: TxRequest,
requestType: RequestType = RequestType.tx
): { hashBuffer: Buffer; derivationPath: string } {
let txToSign: string;
let derivationPath: string;
if (requestType === RequestType.tx) {
assert(txRequest.transactions && txRequest.transactions.length === 1, 'Unable to find transactions in txRequest');
txToSign = txRequest.transactions[0].unsignedTx.signableHex;
derivationPath = txRequest.transactions[0].unsignedTx.derivationPath;
} else if (requestType === RequestType.message) {
// TODO(WP-2176): Add support for message signing
throw new Error('MPCv2 message signing not supported yet.');
} else {
throw new Error('Invalid request type, got: ' + requestType);
}
let hash: Hash;
try {
hash = this.baseCoin.getHashFunction();
} catch (err) {
hash = createKeccakHash('keccak256') as Hash;
}
const hashBuffer = hash.update(Buffer.from(txToSign, 'hex')).digest();
return { hashBuffer, derivationPath };
}
/**
* Gets the BitGo and user GPG keys from the BitGo public GPG key and the encrypted user GPG private key.
* @param {string} bitgoPublicGpgKey - the BitGo public GPG key
* @param {string} encryptedUserGpgPrvKey - the encrypted user GPG private key
* @param {string} walletPassphrase - the wallet passphrase
* @returns {Promise<{ bitgoGpgKey: pgp.Key; userGpgKey: pgp.SerializedKeyPair<string> }>} - the BitGo and user GPG keys
*/
private async getBitgoAndUserGpgKeys(
bitgoPublicGpgKey: string,
encryptedUserGpgPrvKey: string,
walletPassphrase: string
): Promise<{
bitgoGpgKey: pgp.Key;
userGpgKey: pgp.SerializedKeyPair<string>;
}> {
const bitgoGpgKey = await pgp.readKey({ armoredKey: bitgoPublicGpgKey });
const userDecryptedKey = await pgp.readKey({
armoredKey: this.bitgo.decrypt({ input: encryptedUserGpgPrvKey, password: walletPassphrase }),
});
const userGpgKey: pgp.SerializedKeyPair<string> = {
privateKey: userDecryptedKey.armor(),
publicKey: userDecryptedKey.toPublic().armor(),
};
return {
bitgoGpgKey,
userGpgKey,
};
}
/**
* Validates the adata and cyphertext.
* @param adata string
* @param cyphertext string
* @returns void
* @throws {Error} if the adata or cyphertext is invalid
*/
private validateAdata(adata: string, cyphertext: string): void {
let cypherJson;
try {
cypherJson = JSON.parse(cyphertext);
} catch (e) {
throw new Error('Failed to parse cyphertext to JSON, got: ' + cyphertext);
}
// using decodeURIComponent to handle special characters
if (decodeURIComponent(cypherJson.adata) !== decodeURIComponent(adata)) {
throw new Error('Adata does not match cyphertext adata');
}
}
// #endregion
// #region external signer
/** @inheritdoc */
async signEcdsaMPCv2TssUsingExternalSigner(
params: TSSParams | TSSParamsForMessage,
externalSignerMPCv2SigningRound1Generator: CustomMPCv2SigningRound1GeneratingFunction,
externalSignerMPCv2SigningRound2Generator: CustomMPCv2SigningRound2GeneratingFunction,
externalSignerMPCv2SigningRound3Generator: CustomMPCv2SigningRound3GeneratingFunction,
requestType: RequestType = RequestType.tx
): Promise<TxRequest> {
const { txRequest, reqId } = params;
let txRequestResolved: TxRequest;
// TODO(WP-2176): Add support for message signing
assert(
requestType === RequestType.tx,
'Only transaction signing is supported for external signer, got: ' + requestType
);
if (typeof txRequest === 'string') {
txRequestResolved = await getTxRequest(this.bitgo, this.wallet.id(), txRequest, reqId);
} else {
txRequestResolved = txRequest;
}
const bitgoPublicGpgKey = await this.pickBitgoPubGpgKeyForSigning(
true,
params.reqId,
txRequestResolved.enterpriseId
);
if (!bitgoPublicGpgKey) {
throw new Error('Missing BitGo GPG key for MPCv2');
}
// round 1
const { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey } =
await externalSignerMPCv2SigningRound1Generator({ txRequest: txRequestResolved });
const round1TxRequest = await sendSignatureShareV2(
this.bitgo,
txRequestResolved.walletId,
txRequestResolved.txRequestId,
[signatureShareRound1],
requestType,
this.baseCoin.getMPCAlgorithm(),
userGpgPubKey,
undefined,
this.wallet.multisigTypeVersion(),
reqId
);
// round 2
const { signatureShareRound2, encryptedRound2Session } = await externalSignerMPCv2SigningRound2Generator({
txRequest: round1TxRequest,
encryptedRound1Session,
encryptedUserGpgPrvKey,
bitgoPublicGpgKey: bitgoPublicGpgKey.armor(),
});
const round2TxRequest = await sendSignatureShareV2(
this.bitgo,
txRequestResolved.walletId,
txRequestResolved.txRequestId,
[signatureShareRound2],
requestType,
this.baseCoin.getMPCAlgorithm(),
userGpgPubKey,
undefined,
this.wallet.multisigTypeVersion(),
reqId
);
assert(
round2TxRequest.transactions && round2TxRequest.transactions[0].signatureShares,
'Missing signature shares in round 2 txRequest'
);
// round 3
const { signatureShareRound3 } = await externalSignerMPCv2SigningRound3Generator({
txRequest: round2TxRequest,
encryptedRound2Session,
encryptedUserGpgPrvKey,
bitgoPublicGpgKey: bitgoPublicGpgKey.armor(),
});
await sendSignatureShareV2(
this.bitgo,
txRequestResolved.walletId,
txRequestResolved.txRequestId,
[signatureShareRound3],
requestType,
this.baseCoin.getMPCAlgorithm(),
userGpgPubKey,
undefined,
this.wallet.multisigTypeVersion(),
reqId
);
return sendTxRequest(this.bitgo, txRequestResolved.walletId, txRequestResolved.txRequestId, requestType, reqId);
}
async createOfflineRound1Share(params: { txRequest: TxRequest; prv: string; walletPassphrase: string }): Promise<{
signatureShareRound1: SignatureShareRecord;
userGpgPubKey: string;
encryptedRound1Session: string;
encryptedUserGpgPrvKey: string;
}> {
const { prv, walletPassphrase, txRequest } = params;
const { hashBuffer, derivationPath } = this.getHashStringAndDerivationPath(txRequest);
const adata = `${hashBuffer.toString('hex')}:${derivationPath}`;
const userKeyShare = Buffer.from(prv, 'base64');
const userGpgKey = await generateGPGKeyPair('secp256k1');
const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer);
const userSignerBroadcastMsg1 = await userSigner.init();
const signatureShareRound1 = await getSignatureShareRoundOne(userSignerBroadcastMsg1, userGpgKey);
const session = userSigner.getSession();
const encryptedRound1Session = this.bitgo.encrypt({ input: session, password: walletPassphrase, adata });
const userGpgPubKey = userGpgKey.publicKey;
const encryptedUserGpgPrvKey = this.bitgo.encrypt({
input: userGpgKey.privateKey,
password: walletPassphrase,
adata,
});
return { signatureShareRound1, userGpgPubKey, encryptedRound1Session, encryptedUserGpgPrvKey };
}
async createOfflineRound2Share(params: {
txRequest: TxRequest;
prv: string;
walletPassphrase: string;
bitgoPublicGpgKey: string;
encryptedUserGpgPrvKey: string;
encryptedRound1Session: string;
}): Promise<{
signatureShareRound2: SignatureShareRecord;
encryptedRound2Session: string;
}> {
const { prv, walletPassphrase, encryptedUserGpgPrvKey, encryptedRound1Session, bitgoPublicGpgKey, txRequest } =
params;
const { hashBuffer, derivationPath } = this.getHashStringAndDerivationPath(txRequest);
const adata = `${hashBuffer.toString('hex')}:${derivationPath}`;
const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys(
bitgoPublicGpgKey,
encryptedUserGpgPrvKey,
walletPassphrase
);
const signatureShares = txRequest.transactions?.[0].signatureShares;
assert(signatureShares, 'Missing signature shares in round 1 txRequest');
const parsedBitGoToUserSigShareRoundOne = JSON.parse(
signatureShares[signatureShares.length - 1].share
) as MPCv2SignatureShareRound1Output;
if (parsedBitGoToUserSigShareRoundOne.type !== 'round1Output') {
throw new Error('Unexpected signature share response. Unable to parse data.');
}
const serializedBitGoToUserMessagesRound1 = await verifyBitGoMessagesAndSignaturesRoundOne(
parsedBitGoToUserSigShareRoundOne,
userGpgKey,
bitgoGpgKey
);
const round1Session = this.bitgo.decrypt({ input: encryptedRound1Session, password: walletPassphrase });
this.validateAdata(adata, encryptedRound1Session);
const userKeyShare = Buffer.from(prv, 'base64');
const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer);
await userSigner.setSession(round1Session);
const deserializedMessages = DklsTypes.deserializeMessages(serializedBitGoToUserMessagesRound1);
const userToBitGoMessagesRound2 = userSigner.handleIncomingMessages({
p2pMessages: [],
broadcastMessages: deserializedMessages.broadcastMessages,
});
const userToBitGoMessagesRound3 = userSigner.handleIncomingMessages({
p2pMessages: deserializedMessages.p2pMessages,
broadcastMessages: [],
});
const signatureShareRound2 = await getSignatureShareRoundTwo(
userToBitGoMessagesRound2,
userToBitGoMessagesRound3,
userGpgKey,
bitgoGpgKey
);
const session = userSigner.getSession();
const encryptedRound2Session = this.bitgo.encrypt({ input: session, password: walletPassphrase, adata });
return {
signatureShareRound2,
encryptedRound2Session,
};
}
async createOfflineRound3Share(params: {
txRequest: TxRequest;
prv: string;
walletPassphrase: string;
bitgoPublicGpgKey: string;
encryptedUserGpgPrvKey: string;
encryptedRound2Session: string;
}): Promise<{
signatureShareRound3: SignatureShareRecord;
}> {
const { prv, walletPassphrase, encryptedUserGpgPrvKey, encryptedRound2Session, bitgoPublicGpgKey, txRequest } =
params;
assert(txRequest.transactions && txRequest.transactions.length === 1, 'Unable to find transactions in txRequest');
const { hashBuffer, derivationPath } = this.getHashStringAndDerivationPath(txRequest);
const adata = `${hashBuffer.toString('hex')}:${derivationPath}`;
const { bitgoGpgKey, userGpgKey } = await this.getBitgoAndUserGpgKeys(
bitgoPublicGpgKey,
encryptedUserGpgPrvKey,
walletPassphrase
);
const signatureShares = txRequest.transactions?.[0].signatureShares;
assert(signatureShares, 'Missing signature shares in round 2 txRequest');
const parsedBitGoToUserSigShareRoundTwo = JSON.parse(
signatureShares[signatureShares.length - 1].share
) as MPCv2SignatureShareRound2Output;
if (parsedBitGoToUserSigShareRoundTwo.type !== 'round2Output') {
throw new Error('Unexpected signature share response. Unable to parse data.');
}
const serializedBitGoToUserMessagesRound3 = await verifyBitGoMessagesAndSignaturesRoundTwo(
parsedBitGoToUserSigShareRoundTwo,
userGpgKey,
bitgoGpgKey
);
const deserializedBitGoToUserMessagesRound3 = DklsTypes.deserializeMessages({
p2pMessages: serializedBitGoToUserMessagesRound3.p2pMessages,
broadcastMessages: [],
});
const round2Session = this.bitgo.decrypt({ input: encryptedRound2Session, password: walletPassphrase });
this.validateAdata(adata, encryptedRound2Session);
const userKeyShare = Buffer.from(prv, 'base64');
const userSigner = new DklsDsg.Dsg(userKeyShare, 0, derivationPath, hashBuffer);
await userSigner.setSession(round2Session);
const userToBitGoMessagesRound4 = userSigner.handleIncomingMessages({
p2pMessages: deserializedBitGoToUserMessagesRound3.p2pMessages,
broadcastMessages: [],
});
const signatureShareRound3 = await getSignatureShareRoundThree(userToBitGoMessagesRound4, userGpgKey, bitgoGpgKey);
return { signatureShareRound3 };
}
// #endregion
}
/**
* Checks if the given key share, when decrypted, contains valid GG18 signing material.
*
* @param {string} keyShare - The encrypted key share string.
* @param {string|undefined} walletPassphrase - The passphrase used to decrypt the key share
* @returns {boolean} - Returns `true` if the decrypted data contains valid signing material, otherwise `false`.
*/
export function isGG18SigningMaterial(keyShare: string, walletPassphrase: string | undefined): boolean {
const prv = sjcl.decrypt(walletPassphrase, keyShare);
try {
const signingMaterial = JSON.parse(prv);
return (
signingMaterial.pShare &&
signingMaterial.bitgoNShare &&
(signingMaterial.userNShare || signingMaterial.backupNShare)
);
} catch (error) {
return false;
}
}
/**
* Get the MPC v2 recovery key shares from the provided user and backup key shares.
* @param encryptedUserKey encrypted gg18 or MPCv2 user key
* @param encryptedBackupKey encrypted gg18 or MPCv2 backup key
* @param walletPassphrase password for user and backup key
* @returns MPC v2 recovery key shares
*/
export async function getMpcV2RecoveryKeyShares(
encryptedUserKey: string,
encryptedBackupKey: string,
walletPassphrase?: string
): Promise<{
userKeyShare: Buffer;
backupKeyShare: Buffer;
commonKeyChain: string;
}> {
if (isGG18SigningMaterial(encryptedUserKey, walletPassphrase)) {
return getMpcV2RecoveryKeySharesFromGG18(encryptedUserKey, encryptedBackupKey, walletPassphrase);
}
return getMpcV2RecoveryKeySharesFromReducedKey(encryptedUserKey, encryptedBackupKey, walletPassphrase);
}
/**
* Signs a message hash using MPC v2 recovery key shares.
*
* @param {Buffer} messageHash
* @param {Buffer} userKeyShare
* @param {Buffer} backupKeyShare
* @param {string} commonKeyChain
* @returns {Promise<{ recid: number, r: string, s: string, y: string }>}
*
* @async
*/
export async function signRecoveryMpcV2(
messageHash: Buffer,
userKeyShare: Buffer,
backupKeyShare: Buffer,
commonKeyChain: string
): Promise<{
recid: number;
r: string;
s: string;
y: string;
}> {
const userDsg = new DklsDsg.Dsg(userKeyShare, 0, 'm/0', messageHash);
const backupDsg = new DklsDsg.Dsg(backupKeyShare, 1, 'm/0', messageHash);
const signatureString = DklsUtils.verifyAndConvertDklsSignature(
messageHash,
(await DklsUtils.executeTillRound(5, userDsg, backupDsg)) as DklsTypes.DeserializedDklsSignature,
commonKeyChain,
'm/0',
undefined,
false
);
const sigParts = signatureString.split(':');
return {
recid: parseInt(sigParts[0], 10),
r: sigParts[1],
s: sigParts[2],
y: sigParts[3],
};
}
// #region private utils
/**
* Get the MPC v2 recovery key shares from the provided user and backup key shares.
* @param encryptedGG18UserKey encrypted gg18 user key
* @param encryptedGG18BackupKey encrypted gg18 backup key
* @param walletPassphrase password for user and backup key
* @returns MPC v2 recovery key shares
*/
async function getMpcV2RecoveryKeySharesFromGG18(
encryptedGG18UserKey: string,
encryptedGG18BackupKey: string,
walletPassphrase?: string
): Promise<{
userKeyShare: Buffer;
backupKeyShare: Buffer;
commonKeyChain: string;
}> {
const [userKeyCombined, backupKeyCombined] = getKeyCombinedFromTssKeyShares(
encryptedGG18UserKey,
encryptedGG18BackupKey,
walletPassphrase
);
const retrofitDataA: DklsTypes.RetrofitData = {
xShare: userKeyCombined.xShare,
};
const retrofitDataB: DklsTypes.RetrofitData = {
xShare: backupKeyCombined.xShare,
};
const [user, backup] = await DklsUtils.generate2of2KeyShares(retrofitDataA, retrofitDataB);
const userKeyShare = user.getKeyShare();
const backupKeyShare = backup.getKeyShare();
return {
userKeyShare,
backupKeyShare,
commonKeyChain: DklsTypes.getCommonKeychain(backupKeyShare),
};
}
/**
* Retrieves the MPC v2 recovery key shares from the provided user and backup key shares.
*
* @param {string} encryptedMPCv2UserKey
* @param {string} encryptedMPCv2BackupKey
* @param {string} [walletPassphrase] - The passphrase used to decrypt the key shares
* @returns {Promise<{ userKeyShare: KeyShare, backupKeyShare: KeyShare, commonKeyChain: string }>}
*
* @async
*/
async function getMpcV2RecoveryKeySharesFromReducedKey(
encryptedMPCv2UserKey: string,
encryptedMPCv2BackupKey: string,
walletPassphrase?: string
): Promise<{
userKeyShare: Buffer;
backupKeyShare: Buffer;
commonKeyChain: string;
}> {
const userCompressedPrv = Buffer.from(sjcl.decrypt(walletPassphrase, encryptedMPCv2UserKey), 'base64');
const bakcupCompressedPrv = Buffer.from(sjcl.decrypt(walletPassphrase, encryptedMPCv2BackupKey), 'base64');
const userPrvJSON: DklsTypes.ReducedKeyShare = DklsTypes.getDecodedReducedKeyShare(userCompressedPrv);
const backupPrvJSON: DklsTypes.ReducedKeyShare = DklsTypes.getDecodedReducedKeyShare(bakcupCompressedPrv);
const userKeyRetrofit: DklsTypes.RetrofitData = {
xShare: {
x: Buffer.from(userPrvJSON.prv).toString('hex'),
y: Buffer.from(userPrvJSON.pub).toString('hex'),
chaincode: Buffer.from(userPrvJSON.rootChainCode).toString('hex'),
},
xiList: userPrvJSON.xList.slice(0, 2),
};
const backupKeyRetrofit: DklsTypes.RetrofitData = {
xShare: {
x: Buffer.from(backupPrvJSON.prv).toString('hex'),
y: Buffer.from(backupPrvJSON.pub).toString('hex'),
chaincode: Buffer.from(backupPrvJSON.rootChainCode).toString('hex'),
},
xiList: backupPrvJSON.xList.slice(0, 2),
};
const [user, backup] = await DklsUtils.generate2of2KeyShares(userKeyRetrofit, backupKeyRetrofit);
const userKeyShare = user.getKeyShare();
const backupKeyShare = backup.getKeyShare();
const commonKeyChain = DklsTypes.getCommonKeychain(userKeyShare);
return { userKeyShare, backupKeyShare, commonKeyChain };
}
/**
* Gets the combined key for GG18
* @param encryptedGG18UserKey encrypted GG18 user key
* @param encryptedGG18BackupKey encrypted GG18 backup key
* @param walletPassphrase wallet passphrase
* @returns key shares
*/
function getKeyCombinedFromTssKeyShares(
encryptedGG18UserKey: string,
encryptedGG18BackupKey: string,
walletPassphrase?: string
): [ECDSAMethodTypes.KeyCombined, ECDSAMethodTypes.KeyCombined] {
let backupPrv;
let userPrv;
try {
backupPrv = sjcl.decrypt(walletPassphrase, encryptedGG18BackupKey);
userPrv = sjcl.decrypt(walletPassphrase, encryptedGG18UserKey);
} catch (e) {
throw new Error(`Error decrypting backup keychain: ${e.message}`);
}
const userSigningMaterial = JSON.parse(userPrv) as ECDSAMethodTypes.SigningMaterial;
const backupSigningMaterial = JSON.parse(backupPrv) as ECDSAMethodTypes.SigningMaterial;
if (!userSigningMaterial.backupNShare) {
throw new Error('Invalid user key - missing backupNShare');
}
if (!backupSigningMaterial.userNShare) {
throw new Error('Invalid backup key - missing userNShare');
}
const MPC = new Ecdsa();
const userKeyCombined = MPC.keyCombine(userSigningMaterial.pShare, [
userSigningMaterial.bitgoNShare,
userSigningMaterial.backupNShare,
]);
const backupKeyCombined = MPC.keyCombine(backupSigningMaterial.pShare, [
backupSigningMaterial.userNShare,
backupSigningMaterial.bitgoNShare,
]);
if (
userKeyCombined.xShare.y !== backupKeyCombined.xShare.y ||
userKeyCombined.xShare.chaincode !== backupKeyCombined.xShare.chaincode
) {
throw new Error('Common keychains do not match');
}
return [userKeyCombined, backupKeyCombined];
}
// #endregion
Выполнить команду
Для локальной разработки. Не используйте в интернете!