PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-core/src/bitgo/utils

Просмотр файла: opengpgUtils.ts

/* eslint-disable @typescript-eslint/ban-ts-comment */

import * as pgp from 'openpgp';
import {
  createMessage,
  decrypt,
  encrypt,
  Key,
  readKey,
  readMessage,
  readPrivateKey,
  readSignature,
  sign,
  verify,
} from 'openpgp';
import * as _ from 'lodash';
import { ecc as secp256k1 } from '@bitgo/secp256k1';
import { BitGoBase } from '../bitgoBase';
import crypto from 'crypto';

const sodium = require('libsodium-wrappers-sumo');

export type KeyValidityDict = {
  keyID: pgp.KeyID;
  valid: boolean | null;
}[];

export type AuthEncMessage = {
  encryptedMessage: string;
  signature: string;
};

/**
 * Fetches BitGo's public gpg key used in MPC flows
 * @param {BitGoBase} bitgo BitGo object
 * @return {Key} public gpg key
 */
export async function getBitgoGpgPubKey(bitgo: BitGoBase): Promise<{ mpcV1: Key; mpcV2: Key | undefined }> {
  const constants = await bitgo.fetchConstants();
  if (!constants.mpc || !constants.mpc.bitgoPublicKey) {
    throw new Error('Unable to create MPC keys - bitgoPublicKey is missing from constants');
  }

  const bitgoPublicKeyStr = constants.mpc.bitgoPublicKey as string;
  const bitgoMPCv2PublicKeyStr = constants.mpc.bitgoMPCv2PublicKey
    ? await readKey({ armoredKey: constants.mpc.bitgoMPCv2PublicKey as string })
    : undefined;
  return { mpcV1: await readKey({ armoredKey: bitgoPublicKeyStr }), mpcV2: bitgoMPCv2PublicKeyStr };
}

/**
 * Verifies the primary user on a GPG key using a reference key representing the user to be checked.
 * Allows a verification without a date check by wrapping verifyPrimaryUser of openpgp.
 * @param {Key} pubKey gpg key to check the primary user of.
 * @param {Key} primaryUser gpg key of the user to check.
 * @param {boolean} checkDates If false, disable date checks in the openpgp call to check the primary user.
 * @return {KeyValidityDict} list of users checked and whether each passed as a primary user in pubKey or not.
 */
export async function verifyPrimaryUserWrapper(
  pubKey: Key,
  primaryUser: Key,
  checkDates: boolean
): Promise<KeyValidityDict> {
  if (checkDates) {
    return await pubKey.verifyPrimaryUser([primaryUser]);
  } else {
    return await pubKey.verifyPrimaryUser([primaryUser], null as unknown as undefined);
  }
}

/**
 * Verify an Eddsa or Ecdsa KeyShare Proof.
 *
 * @param senderPubKey public key of the sender of the privateShareProof
 * @param privateShareProof u value proof
 * @param uValue u value from an Eddsa keyshare
 * @param algo
 * @return {boolean} whether uValue proof actually was signed by sender as part of their subkeys
 */
export async function verifyShareProof(
  senderPubKey: string,
  privateShareProof: string,
  uValue: string,
  algo: 'eddsa' | 'ecdsa'
): Promise<boolean> {
  const decodedProof = await pgp.readKey({ armoredKey: privateShareProof });
  const senderGpgKey = await pgp.readKey({ armoredKey: senderPubKey });
  if (!(await verifyPrimaryUserWrapper(decodedProof, senderGpgKey, true))[0].valid) {
    return false;
  }
  const proofSubkeys = decodedProof.getSubkeys()[1];
  if (algo === 'eddsa') {
    const decodedUValueProof = Buffer.from(proofSubkeys.keyPacket.publicParams['Q'].slice(1)).toString('hex');
    const rawUValueProof = Buffer.from(
      sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(uValue, 'hex'))
    ).toString('hex');
    return decodedUValueProof === rawUValueProof;
  } else if (algo === 'ecdsa') {
    const decodedUValueProof = Buffer.from(proofSubkeys.keyPacket.publicParams['Q']).toString('hex');
    const rawUValueProof = secp256k1.pointFromScalar(Buffer.from(uValue, 'hex'), false);
    return rawUValueProof !== null && decodedUValueProof === Buffer.from(rawUValueProof).toString('hex');
  } else {
    throw new Error('Invalid algorithm provided');
  }
}

/**
 * Verify a shared data proof.
 *
 * @param senderPubKeyArm public key of the signer of the key with proof data
 * @param keyWithNotation signed reciever key with notation data
 * @param dataToVerify data to be checked against notation data in the signed key
 * @return {boolean} whether proof is valid
 */
export async function verifySharedDataProof(
  senderPubKeyArm: string,
  keyWithNotation: string,
  dataToVerify: { name: string; value: string }[]
): Promise<boolean> {
  const senderPubKey = await pgp.readKey({ armoredKey: senderPubKeyArm });
  const signedKey = await pgp.readKey({ armoredKey: keyWithNotation });
  if (
    !(await verifyPrimaryUserWrapper(signedKey, senderPubKey, false).then((values) =>
      _.some(values, (value) => value.valid)
    ))
  ) {
    return false;
  }
  const primaryUser = await signedKey.getPrimaryUser(null as unknown as undefined);
  const anyInvalidProof = _.some(
    // @ts-ignore
    primaryUser.user.otherCertifications[0].rawNotations,
    (notation) => dataToVerify.find((i) => i.name === notation.name)?.value !== Buffer.from(notation.value).toString()
  );
  return !anyInvalidProof;
}

/**
 * Creates a proof through adding notation data to a GPG ceritifying signature.
 *
 * @param privateKeyArmored gpg private key in armor format of the sender
 * @param publicKeyToCertArmored gpg public key in armor fomrat of the reciever
 * @param notations data to be proofed
 * @return {string} keyshare proof
 */
export async function createSharedDataProof(
  privateKeyArmored: string,
  publicKeyToCertArmored: string,
  notations: { name: string; value: string }[]
): Promise<string> {
  const certifyingKey = await pgp.readKey({ armoredKey: privateKeyArmored });
  const publicKeyToCert = await pgp.readKey({ armoredKey: publicKeyToCertArmored });
  const dateTime = new Date();
  // UserId Packet.
  const userIdPkt = new pgp.UserIDPacket();
  const primaryUser = await publicKeyToCert.getPrimaryUser(null as unknown as undefined);
  // @ts-ignore
  userIdPkt.userID = primaryUser.user.userID.userID;
  // Signature packet.
  const signaturePacket = new pgp.SignaturePacket();
  signaturePacket.signatureType = pgp.enums.signature.certPositive;
  signaturePacket.publicKeyAlgorithm = pgp.enums.publicKey.ecdsa;
  signaturePacket.hashAlgorithm = pgp.enums.hash.sha256;
  // @ts-ignore
  signaturePacket.issuerFingerprint = await primaryUser.user.mainKey.keyPacket.getFingerprintBytes();
  // @ts-ignore
  signaturePacket.issuerKeyID = primaryUser.user.mainKey.keyPacket.keyID;
  // @ts-ignore
  signaturePacket.signingKeyID = primaryUser.user.mainKey.keyPacket.keyID;
  // @ts-ignore
  signaturePacket.signersUserID = primaryUser.user.userID.userID;
  // @ts-ignore
  signaturePacket.features = [1];
  notations.forEach(({ name, value }) => {
    signaturePacket.rawNotations.push({
      name: name,
      value: new Uint8Array(Buffer.from(value)),
      humanReadable: true,
      critical: false,
    });
  });

  // Prepare signing data.
  const keydataToSign = {};
  // @ts-ignore
  keydataToSign.key = publicKeyToCert.keyPacket;
  // @ts-ignore
  keydataToSign.userID = userIdPkt;

  // Sign the data (create certification).
  // @ts-ignore
  await signaturePacket.sign(certifyingKey.keyPacket, keydataToSign, dateTime);

  // Assemble packets together.
  const publicKeyToCertPkts = publicKeyToCert.toPacketList();
  const newKeyPktList = new pgp.PacketList();
  newKeyPktList.push(...publicKeyToCertPkts.slice(0, 3), signaturePacket, ...publicKeyToCertPkts.slice(3));
  // @ts-ignore
  const newPubKey = new pgp.PublicKey(newKeyPktList);
  return newPubKey.armor().replace(/\r\n/g, '\n');
}

/**
 * Creates a KeyShare Proof based on given algo.
 *
 * Creates an EdDSA KeyShare Proof by appending an ed25519 subkey (auth) to an armored gpg private key.
 * Creates an ECDSA KeyShare Proof by Append a secp256k1 subkey (auth) to a PGP keychain.
 *
 * @param privateArmor gpg private key in armor format
 * @param uValue u value from an Eddsa keyshare
 * @param algo algo to use, eddsa or ecdsa
 * @return {string} keyshare proof
 */
export async function createShareProof(privateArmor: string, uValue: string, algo: string): Promise<string> {
  const privateKey = await readKey({ armoredKey: privateArmor });
  const dateTime = new Date();
  // @ts-ignore - type inconsistency, this ctor supports a date param: https://docs.openpgpjs.org/SecretSubkeyPacket.html
  const secretSubkeyPacket = new pgp.SecretSubkeyPacket(dateTime);
  secretSubkeyPacket.algorithm = pgp.enums.publicKey[algo];
  // @ts-ignore - same as above
  secretSubkeyPacket.isEncrypted = false;
  let oid;
  let Q;
  if (algo === 'eddsa') {
    await sodium.ready;
    const subKeyVal = Buffer.from(
      sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(uValue, 'hex'), 'uint8array')
    );
    // Sub-key (encryption key) packet.
    oid = [0x2b, 0x06, 0x01, 0x04, 0x01, 0xda, 0x47, 0x0f, 0x01];
    // @ts-ignore
    oid.write = () => new Uint8Array(Buffer.from('092b06010401da470f01', 'hex'));
    Q = new Uint8Array([0x40, ...subKeyVal]);
  } else if (algo === 'ecdsa') {
    oid = [0x2b, 0x81, 0x04, 0x00, 0x0a];
    // @ts-ignore - same as above
    oid.write = () => new Uint8Array(Buffer.from('052b8104000a', 'hex'));
    Q = secp256k1.pointFromScalar(new Uint8Array(Buffer.from(uValue, 'hex')), false);
  }
  secretSubkeyPacket.publicParams = {
    oid,
    Q,
  };
  // @ts-ignore - same as above
  await secretSubkeyPacket.computeFingerprintAndKeyID();

  // Sub-key signature packet.
  const subKeydataToSign = {
    key: privateKey.keyPacket,
    bind: secretSubkeyPacket,
  };
  const subkeySignaturePacket = new pgp.SignaturePacket();
  subkeySignaturePacket.signatureType = pgp.enums.signature.subkeyBinding;
  subkeySignaturePacket.publicKeyAlgorithm = pgp.enums.publicKey.ecdsa;
  subkeySignaturePacket.hashAlgorithm = pgp.enums.hash.sha256;
  subkeySignaturePacket.keyFlags = new Uint8Array([pgp.enums.keyFlags.authentication]);

  // Sign the subkey
  // @ts-ignore - sign supports arbitrary data for 2nd param: https://docs.openpgpjs.org/SignaturePacket.html
  await subkeySignaturePacket.sign(privateKey.keyPacket, subKeydataToSign, dateTime);

  // Assemble packets together.
  const newKeyPktList = new pgp.PacketList();
  const privateKeyPkts = privateKey.toPacketList();
  privateKeyPkts.forEach((packet) => newKeyPktList.push(packet));
  newKeyPktList.push(secretSubkeyPacket, subkeySignaturePacket);
  // @ts-ignore - supports packet list as ctor param: https://docs.openpgpjs.org/PrivateKey.html
  const newPubKey = new pgp.PrivateKey(newKeyPktList).toPublic();

  if (!(await verifyPrimaryUserWrapper(newPubKey, privateKey, true))[0].valid) {
    throw new Error('Incorrect signature');
  }

  return newPubKey.armor().replace(/\r\n/g, '\n');
}

/**
 * Encrypts string using gpg key
 * @DEPRECATED - should use encryptAndSignText instead for added security
 *
 * @param text string to encrypt
 * @param key encryption key
 * @return {string} encrypted string
 *
 * TODO(BG-47170): Delete once gpg signatures are fully supported
 */
export async function encryptText(text: string, key: Key): Promise<string> {
  const messageToEncrypt = await createMessage({
    text,
  });
  return await encrypt({
    message: messageToEncrypt,
    encryptionKeys: [key],
    format: 'armored',
    config: {
      rejectCurves: new Set(),
      showVersion: false,
      showComment: false,
    },
  });
}

/**
 * Encrypts and signs a string
 * @param text string to encrypt and sign
 * @param publicArmor public key to encrypt with
 * @param privateArmor private key to sign with
 */
export async function encryptAndSignText(text: string, publicArmor: string, privateArmor: string): Promise<string> {
  const publicKey = await readKey({ armoredKey: publicArmor });
  const privateKey = await readPrivateKey({ armoredKey: privateArmor });

  const message = await createMessage({ text });

  const signedMessage = await encrypt({
    message,
    encryptionKeys: publicKey,
    signingKeys: privateKey,
    format: 'armored',
    config: {
      rejectCurves: new Set(),
      showVersion: false,
      showComment: false,
    },
  });

  return signedMessage;
}

/**
 * Reads a signed and encrypted message
 *
 * @param signed signed and encrypted message
 * @param publicArmor public key to verify signature
 * @param privateArmor private key to decrypt message
 */
export async function readSignedMessage(signed: string, publicArmor: string, privateArmor: string): Promise<string> {
  const publicKey = await readKey({ armoredKey: publicArmor });
  const privateKey = await readPrivateKey({ armoredKey: privateArmor });

  const message = await readMessage({ armoredMessage: signed });
  const decrypted = await decrypt({
    message,
    verificationKeys: publicKey,
    decryptionKeys: privateKey,
    expectSigned: true,
    config: { rejectCurves: new Set() },
  });

  return decrypted.data;
}

/**
 * Generates a signature
 *
 * @param text string to generate a signature for
 * @param privateArmor private key as armored string
 * @return {string} armored signature string
 */
export async function signText(text: string, privateArmor: string): Promise<string> {
  const privateKey = await readPrivateKey({ armoredKey: privateArmor });
  const message = await createMessage({ text });
  const signature = await sign({
    message,
    signingKeys: privateKey,
    format: 'armored',
    detached: true,
  });

  return signature;
}

/**
 * Verifies signature was generated by the public key and matches the expected text
 *
 * @param text text that the signature was for
 * @param armoredSignature signed message as an armored string
 * @param publicArmor public key that generated the signature
 */
export async function verifySignature(text: string, armoredSignature: string, publicArmor: string): Promise<boolean> {
  const publicKey = await readKey({ armoredKey: publicArmor });
  const signature = await readSignature({ armoredSignature });
  const message = await createMessage({ text });
  const verificationResult = await verify({
    message,
    signature,
    verificationKeys: publicKey,
  });

  if (verificationResult.signatures.length !== 1) {
    throw new Error('Invalid number of signatures');
  }

  try {
    await verificationResult.signatures[0].verified;
    return text === verificationResult.data;
  } catch {
    return false;
  }
}

/**
 * Generate a GPG key pair
 *
 * @param: keyCurve the curve to create a key with
 * @param: username name of the user (optional)
 * @param: email email of the user (optional)
 */
export async function generateGPGKeyPair(
  keyCurve: pgp.EllipticCurveName,
  username?: string | undefined,
  email?: string | undefined
): Promise<pgp.SerializedKeyPair<string>> {
  const randomHexString = crypto.randomBytes(12).toString('hex');
  username = username ?? randomHexString;
  email = email ?? `user-${randomHexString}@${randomHexString}.com`;

  // Allow generating secp256k1 key pairs
  pgp.config.rejectCurves = new Set();
  const gpgKey = await pgp.generateKey({
    userIDs: [
      {
        name: username,
        email,
      },
    ],
    curve: keyCurve,
  });

  return gpgKey;
}

Выполнить команду


Для локальной разработки. Не используйте в интернете!