PHP WebShell
Текущая директория: /opt/BitGoJS/modules/sdk-core/src/bitgo/keychain
Просмотр файла: keychains.ts
import { TssSettings } from '@bitgo/public-types';
import assert from 'assert';
import * as _ from 'lodash';
import * as common from '../../common';
import { IBaseCoin, KeychainsTriplet, KeyPair } from '../baseCoin';
import { BitGoBase } from '../bitgoBase';
import { decodeOrElse, ECDSAUtils, EDDSAUtils, generateRandomPassword, RequestTracer } from '../utils';
import {
AddKeychainOptions,
ApiKeyShare,
ChangedKeychains,
CreateBackupOptions,
CreateBitGoOptions,
CreateMpcOptions,
GetKeychainOptions,
GetKeysForSigningOptions,
IKeychains,
Keychain,
ListKeychainOptions,
ListKeychainsResult,
RecreateMpcOptions,
UpdatePasswordOptions,
UpdateSingleKeychainPasswordOptions,
} from './iKeychains';
import { BitGoKeyFromOvcShares, BitGoToOvcJSON, OvcToBitGoJSON } from './ovcJsonCodec';
export class Keychains implements IKeychains {
private readonly bitgo: BitGoBase;
private readonly baseCoin: IBaseCoin;
constructor(bitgo: BitGoBase, baseCoin: IBaseCoin) {
this.bitgo = bitgo;
this.baseCoin = baseCoin;
}
/**
* Get a keychain by ID
* @param params
* @param params.id
* @param params.xpub (optional)
* @param params.ethAddress (optional)
* @param params.reqId (optional)
*/
async get(params: GetKeychainOptions): Promise<Keychain> {
common.validateParams(params, [], ['xpub', 'ethAddress']);
if (_.isUndefined(params.id)) {
throw new Error('id must be defined');
}
const id = params.id;
if (params.reqId) {
this.bitgo.setRequestTracer(params.reqId);
}
return await this.bitgo.get(this.baseCoin.url('/key/' + encodeURIComponent(id))).result();
}
/**
* list the users keychains
* @param params
* @param params.limit - Max number of results in a single call.
* @param params.prevId - Continue iterating (provided by nextBatchPrevId in the previous list)
* @returns {*}
*/
async list(params: ListKeychainOptions = {}): Promise<ListKeychainsResult> {
const queryObject: any = {};
if (!_.isUndefined(params.limit)) {
if (!_.isNumber(params.limit)) {
throw new Error('invalid limit argument, expecting number');
}
queryObject.limit = params.limit;
}
if (!_.isUndefined(params.prevId)) {
if (!_.isString(params.prevId)) {
throw new Error('invalid prevId argument, expecting string');
}
queryObject.prevId = params.prevId;
}
return this.bitgo.get(this.baseCoin.url('/key')).query(queryObject).result();
}
/**
* Change the decryption password for all possible keychains associated with a user.
*
* This function iterates through all keys associated with the user, decrypts
* them with the old password and re-encrypts them with the new password.
*
* This should be called when a user changes their login password, and are expecting
* that their wallet passwords are changed to match the new login password.
*
* @param params
* @param params.oldPassword - The old password used for encrypting the key
* @param params.newPassword - The new password to be used for encrypting the key
* @returns changedKeys Object - e.g.:
* {
* xpub1: encryptedPrv,
* ...
* }
*/
async updatePassword(params: UpdatePasswordOptions): Promise<ChangedKeychains> {
common.validateParams(params, ['oldPassword', 'newPassword'], []);
const changedKeys: ChangedKeychains = {};
let prevId;
let keysLeft = true;
while (keysLeft) {
const result: ListKeychainsResult = await this.list({ limit: 500, prevId });
for (const key of result.keys) {
const oldEncryptedPrv = key.encryptedPrv;
if (_.isUndefined(oldEncryptedPrv)) {
continue;
}
try {
const updatedKeychain = this.updateSingleKeychainPassword({
keychain: key,
oldPassword: params.oldPassword,
newPassword: params.newPassword,
});
if (updatedKeychain.encryptedPrv) {
const changedKeyIdentifier = updatedKeychain.type === 'tss' ? updatedKeychain.id : updatedKeychain.pub;
if (changedKeyIdentifier) {
changedKeys[changedKeyIdentifier] = updatedKeychain.encryptedPrv;
}
}
} catch (e) {
// if the password was incorrect, silence the error, throw otherwise
if (!e.message.includes('private key is incorrect')) {
throw e;
}
}
}
if (result.nextBatchPrevId) {
prevId = result.nextBatchPrevId;
} else {
keysLeft = false;
}
}
return changedKeys;
}
/**
* Update the password used to decrypt a single keychain
* @param params
* @param params.keychain - The keychain whose password should be updated
* @param params.oldPassword - The old password used for encrypting the key
* @param params.newPassword - The new password to be used for encrypting the key
* @returns {object}
*/
updateSingleKeychainPassword(params: UpdateSingleKeychainPasswordOptions = {}): Keychain {
if (!_.isString(params.oldPassword)) {
throw new Error('expected old password to be a string');
}
if (!_.isString(params.newPassword)) {
throw new Error('expected new password to be a string');
}
if (!_.isObject(params.keychain) || !_.isString(params.keychain.encryptedPrv)) {
throw new Error('expected keychain to be an object with an encryptedPrv property');
}
const oldEncryptedPrv = params.keychain.encryptedPrv;
try {
const decryptedPrv = this.bitgo.decrypt({ input: oldEncryptedPrv, password: params.oldPassword });
const newEncryptedPrv = this.bitgo.encrypt({ input: decryptedPrv, password: params.newPassword });
return _.assign({}, params.keychain, { encryptedPrv: newEncryptedPrv });
} catch (e) {
// catching an error here means that the password was incorrect or, less likely, the input to decrypt is corrupted
throw new Error('password used to decrypt keychain private key is incorrect');
}
}
/**
* Create a public/private key pair
* @param params - optional params
* @param params.seed optional - seed to use for keypair generation
* @param params.isRootKey optional - whether the resulting keypair should be a root key
* @returns {KeyPair} - the generated keypair
*/
create(params: { seed?: Buffer; isRootKey?: boolean } = {}): KeyPair {
if (params?.isRootKey) {
return this.baseCoin.generateRootKeyPair(params.seed);
}
return this.baseCoin.generateKeyPair(params.seed);
}
/**
* Add a keychain to BitGo's records
* @param params
*/
async add(params: AddKeychainOptions = {}): Promise<Keychain> {
params = params || {};
common.validateParams(
params,
[],
[
'pub',
'encryptedPrv',
'keyType',
'type',
'source',
'originalPasscodeEncryptionCode',
'enterprise',
'derivedFromParentWithSeed',
]
);
if (!_.isUndefined(params.disableKRSEmail)) {
if (!_.isBoolean(params.disableKRSEmail)) {
throw new Error('invalid disableKRSEmail argument, expecting boolean');
}
}
if (params.reqId) {
this.bitgo.setRequestTracer(params.reqId);
}
return await this.bitgo
.post(this.baseCoin.url('/key'))
.send({
pub: params.pub,
commonPub: params.commonPub,
commonKeychain: params.commonKeychain,
encryptedPrv: params.encryptedPrv,
type: params.type,
keyType: params.keyType,
source: params.source,
provider: params.provider,
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
enterprise: params.enterprise,
derivedFromParentWithSeed: params.derivedFromParentWithSeed,
disableKRSEmail: params.disableKRSEmail,
krsSpecific: params.krsSpecific,
keyShares: params.keyShares,
userGPGPublicKey: params.userGPGPublicKey,
backupGPGPublicKey: params.backupGPGPublicKey,
algoUsed: params.algoUsed,
isDistributedCustody: params.isDistributedCustody,
isMPCv2: params.isMPCv2,
coinSpecific: params.coinSpecific,
})
.result();
}
/**
* Create a BitGo key
* @param params (empty)
*/
async createBitGo(params: CreateBitGoOptions = {}): Promise<Keychain> {
params.source = 'bitgo';
this.baseCoin.preCreateBitGo(params as any);
return await this.add(params);
}
/**
* Create a backup key
* @param params
* @param params.provider (optional)
*/
async createBackup(params: CreateBackupOptions = {}): Promise<Keychain> {
params.source = 'backup';
const isTssBackupKey = params.prv && (params.commonKeychain || params.commonPub);
if (_.isUndefined(params.provider) && !isTssBackupKey) {
// if the provider is undefined, we generate a local key and add the source details
const key = this.create();
_.extend(params, key);
if (params.passphrase !== undefined) {
_.extend(params, { encryptedPrv: this.bitgo.encrypt({ input: key.prv, password: params.passphrase }) });
}
}
const serverResponse = await this.add(params);
return _.extend({}, serverResponse, _.pick(params, ['prv', 'encryptedPrv', 'provider', 'source']));
}
/**
* Gets keys for signing from a wallet
* @param params
* @returns {Promise<Keychain[]>}
*/
async getKeysForSigning(params: GetKeysForSigningOptions = {}): Promise<Keychain[]> {
if (!_.isObject(params.wallet)) {
throw new Error('missing required param wallet');
}
const wallet = params.wallet;
const reqId = params.reqId || new RequestTracer();
const ids = wallet.baseCoin.keyIdsForSigning();
const keychainQueriesBluebirds = ids.map((id) => this.get({ id: wallet.keyIds()[id], reqId }));
return Promise.all(keychainQueriesBluebirds);
}
/**
* Convenience function to create and store MPC keychains with BitGo.
* @param params passphrase used to encrypt secret materials
* @return {Promise<KeychainsTriplet>} newly created User, Backup, and BitGo keys
*/
async createMpc(params: CreateMpcOptions): Promise<KeychainsTriplet> {
let MpcUtils;
let multisigTypeVersion: 'MPCv2' | undefined = undefined;
if (params.multisigType === 'tss' && this.baseCoin.getMPCAlgorithm() === 'ecdsa') {
const tssSettings: TssSettings = await this.bitgo
.get(this.bitgo.microservicesUrl('/api/v2/tss/settings'))
.result();
multisigTypeVersion =
tssSettings.coinSettings[this.baseCoin.getFamily()]?.walletCreationSettings?.multiSigTypeVersion;
}
switch (params.multisigType) {
case 'tss':
MpcUtils =
this.baseCoin.getMPCAlgorithm() === 'eddsa'
? EDDSAUtils.default
: multisigTypeVersion === 'MPCv2'
? ECDSAUtils.EcdsaMPCv2Utils
: ECDSAUtils.EcdsaUtils;
break;
default:
throw new Error('Unsupported multi-sig type');
}
const mpcUtils = new MpcUtils(this.bitgo, this.baseCoin);
return await mpcUtils.createKeychains({
passphrase: params.passphrase,
enterprise: params.enterprise,
originalPasscodeEncryptionCode: params.originalPasscodeEncryptionCode,
retrofit: params.retrofit,
});
}
async recreateMpc(params: RecreateMpcOptions): Promise<KeychainsTriplet> {
assert(params.coin, new Error('missing required param coin'));
assert(params.walletId, new Error('missing required param walletId'));
assert(params.otp, new Error('missing required param otp'));
assert(params.passphrase, new Error('missing required param passphrase'));
assert(
params.encryptedMaterial.encryptedWalletPassphrase,
new Error('missing required param encryptedWalletPassphrase')
);
assert(params.encryptedMaterial.encryptedUserKey, new Error('missing required param encryptedUserKey'));
assert(params.encryptedMaterial.encryptedBackupKey, new Error('missing required param encryptedBackupKey'));
await this.bitgo.post(this.bitgo.microservicesUrl('/api/v1/user/unlock')).send({ otp: params.otp }).result();
const { recoveryInfo } = await this.bitgo
.post(this.bitgo.microservicesUrl(`/api/v2/${params.coin}/wallet/${params.walletId}/passcoderecovery`))
.result();
if (!recoveryInfo || !('passcodeEncryptionCode' in recoveryInfo)) {
throw new Error('failed to get recovery info');
}
const decryptedWalletPassphrase = this.bitgo.decrypt({
input: params.encryptedMaterial.encryptedWalletPassphrase,
password: recoveryInfo.passcodeEncryptionCode,
});
const decryptedUserKey = this.bitgo.decrypt({
input: params.encryptedMaterial.encryptedUserKey,
password: decryptedWalletPassphrase,
});
const decryptedBackupKey = this.bitgo.decrypt({
input: params.encryptedMaterial.encryptedBackupKey,
password: decryptedWalletPassphrase,
});
return this.createMpc({
...params,
multisigType: 'tss',
retrofit: {
decryptedUserKey,
decryptedBackupKey,
walletId: params.walletId,
},
});
}
/**
* It parses the JSON downloaded from the OVC for platform (BitGo),
* and creates a corresponding TSS BitGo key. It also returns the JSON that needs
* to be uploaded back to the OVCs containing the BitGo -> OVC shares.
* @param ovcOutputJson JSON format of the file downloaded from the OVC for platform
* @returns {BitGoKeyFromOvcShares}
*/
async createTssBitGoKeyFromOvcShares(ovcOutputJson: unknown): Promise<BitGoKeyFromOvcShares> {
const decodedOvcOutput = decodeOrElse(OvcToBitGoJSON.name, OvcToBitGoJSON, ovcOutputJson, (errors) => {
throw new Error(`Error(s) parsing OVC JSON: ${errors}`);
});
if (decodedOvcOutput.state !== 1) {
throw new Error('State expected to be "1". Please complete the first two OVC operations');
}
// OVC-1 is responsible for the User key
const ovc1 = decodedOvcOutput.ovc[1];
// OVC-2 is responsible for the Backup key
const ovc2 = decodedOvcOutput.ovc[2];
const keyShares: ApiKeyShare[] = [
{
from: 'user',
to: 'bitgo',
publicShare: ovc1.ovcToBitgoShare.publicShare,
privateShare: ovc1.ovcToBitgoShare.privateShare,
privateShareProof: ovc1.ovcToBitgoShare.uSig.toString() ?? '',
vssProof: ovc1.ovcToBitgoShare.vssProof ?? '',
},
{
from: 'backup',
to: 'bitgo',
publicShare: ovc2.ovcToBitgoShare.publicShare,
privateShare: ovc2.ovcToBitgoShare.privateShare,
privateShareProof: ovc2.ovcToBitgoShare.uSig.toString() ?? '',
vssProof: ovc2.ovcToBitgoShare.vssProof ?? '',
},
];
const key = await this.baseCoin.keychains().add({
source: 'bitgo',
keyShares,
keyType: 'tss',
userGPGPublicKey: ovc1.gpgPubKey,
backupGPGPublicKey: ovc2.gpgPubKey,
});
assert(key.keyShares);
assert(key.commonKeychain);
assert(key.walletHSMGPGPublicKeySigs);
const bitgoToUserShare = key.keyShares.find(
(value: { from: string; to: string }) => value.from === 'bitgo' && value.to === 'user'
);
assert(bitgoToUserShare);
assert(bitgoToUserShare.vssProof);
assert(bitgoToUserShare.paillierPublicKey);
const bitgoToBackupShare = key.keyShares.find(
(value: { from: string; to: string }) => value.from === 'bitgo' && value.to === 'backup'
);
assert(bitgoToBackupShare);
assert(bitgoToBackupShare.vssProof);
assert(bitgoToBackupShare.paillierPublicKey);
// Create JSON data with platform shares for OVC-1 and OVC-2
const bitgoToOvcOutput: BitGoToOvcJSON = {
wallet: {
...decodedOvcOutput,
platform: {
commonKeychain: key.commonKeychain,
walletGpgPubKeySigs: key.walletHSMGPGPublicKeySigs,
ovc: {
// BitGo to User (OVC-1)
1: {
bitgoToOvcShare: {
i: 1,
j: 3,
publicShare: bitgoToUserShare.publicShare,
privateShare: bitgoToUserShare.privateShare,
paillierPublicKey: bitgoToUserShare.paillierPublicKey,
vssProof: bitgoToUserShare.vssProof,
},
},
// BitGo to Backup (OVC-2)
2: {
bitgoToOvcShare: {
i: 2,
j: 3,
publicShare: bitgoToBackupShare.publicShare,
privateShare: bitgoToBackupShare.privateShare,
paillierPublicKey: bitgoToBackupShare.paillierPublicKey,
vssProof: bitgoToBackupShare.vssProof,
},
},
},
},
},
};
// Mark it ready for next operation, should be 2
bitgoToOvcOutput.wallet.state += 1;
const output: BitGoKeyFromOvcShares = {
bitGoKeyId: key.id,
bitGoOutputJsonForOvc: bitgoToOvcOutput,
};
return decodeOrElse(BitGoKeyFromOvcShares.name, BitGoKeyFromOvcShares, output, (errors) => {
throw new Error(`Error producing the output: ${errors}`);
});
}
/**
* Create user keychain, encrypt the private key with the wallet passphrase and store it in BitGo.
* @param walletPassphrase
* @returns Keychain including the decrypted private key
*/
async createUserKeychain(walletPassphrase: string): Promise<Keychain> {
const keychains = this.baseCoin.keychains();
const newKeychain = keychains.create();
const originalPasscodeEncryptionCode = generateRandomPassword(5);
const encryptedPrv = this.bitgo.encrypt({
password: walletPassphrase,
input: newKeychain.prv,
});
return {
...(await keychains.add({
encryptedPrv,
originalPasscodeEncryptionCode,
pub: newKeychain.pub,
source: 'user',
})),
prv: newKeychain.prv,
};
}
}
Выполнить команду
Для локальной разработки. Не используйте в интернете!