diff --git a/modules/bitgo/test/v2/unit/internal/opengpgUtils.ts b/modules/bitgo/test/v2/unit/internal/opengpgUtils.ts index 8ceea1c9eb..0036359927 100644 --- a/modules/bitgo/test/v2/unit/internal/opengpgUtils.ts +++ b/modules/bitgo/test/v2/unit/internal/opengpgUtils.ts @@ -215,6 +215,70 @@ describe('OpenGPG Utils Tests', function () { .should.be.rejectedWith('Error decrypting message: Session key decryption failed.'); }); + it('should succeed on encryption with detached signature and decryption with verification', async function () { + const text = 'original message'; + + const signedMessage = await openpgpUtils.encryptAndDetachSignText( + text, + recipientKey.publicKey, + senderKey.privateKey + ); + ( + await openpgpUtils.decryptAndVerifySignedText(signedMessage, senderKey.publicKey, recipientKey.privateKey) + ).should.equal(text); + }); + + it('should fail on encryption with detached signature and decryption with wrong private key', async function () { + const text = 'original message'; + + const signedMessage = await openpgpUtils.encryptAndDetachSignText( + text, + recipientKey.publicKey, + senderKey.privateKey + ); + await openpgpUtils + .decryptAndVerifySignedText(signedMessage, senderKey.publicKey, otherKey.privateKey) + .should.be.rejectedWith('Error decrypting message: Session key decryption failed.'); + }); + + it('should fail on encryption with detached signature and decryption verification with wrong sender public key', async function () { + const text = 'original message'; + + const signedMessage = await openpgpUtils.encryptAndDetachSignText( + text, + recipientKey.publicKey, + senderKey.privateKey + ); + await openpgpUtils + .decryptAndVerifySignedText(signedMessage, otherKey.publicKey, recipientKey.privateKey) + .should.be.rejectedWith( + `Error decrypting message: Could not find signing key with key ID ${( + await openpgp.readKey({ armoredKey: senderKey.publicKey }) + ) + .getKeyID() + .toHex()}` + ); + }); + + it('should fail on encryption with detached signature by unintended sender and decryption verification', async function () { + const text = 'original message'; + + const signedMessage = await openpgpUtils.encryptAndDetachSignText( + text, + recipientKey.publicKey, + otherKey.privateKey + ); + await openpgpUtils + .decryptAndVerifySignedText(signedMessage, senderKey.publicKey, recipientKey.privateKey) + .should.be.rejectedWith( + `Error decrypting message: Could not find signing key with key ID ${( + await openpgp.readKey({ armoredKey: otherKey.publicKey }) + ) + .getKeyID() + .toHex()}` + ); + }); + it('should encrypt, sign, and decrypt without previously clearing rejectedCurves', async function () { openpgp.config.rejectCurves = new Set([openpgp.enums.curve.secp256k1]); diff --git a/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts b/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts index 94c508e310..f4b5e4162f 100644 --- a/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/opengpgUtils.ts @@ -25,6 +25,11 @@ export type KeyValidityDict = { 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 @@ -304,6 +309,75 @@ export async function encryptText(text: string, key: Key): Promise { }); } +/** + * Encrypts and detach 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 encryptAndDetachSignText( + text: string, + publicArmor: string, + privateArmor: string +): Promise { + const publicKey = await readKey({ armoredKey: publicArmor }); + const privateKey = await readPrivateKey({ armoredKey: privateArmor }); + const message = await createMessage({ text }); + const encryptedMessage = await encrypt({ + message, + encryptionKeys: publicKey, + format: 'armored', + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + }); + const signature = await sign({ + message, + signingKeys: privateKey, + format: 'armored', + detached: true, + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + }); + return { + encryptedMessage: encryptedMessage, + signature: signature, + }; +} + +/** + * Encrypts and detach signs a string + * @param text string to encrypt and sign + * @param publicArmor public key to verify signature with + * @param privateArmor private key to decrypt with + */ +export async function decryptAndVerifySignedText( + encryptedAndSignedMessage: AuthEncMessage, + publicArmor: string, + privateArmor: string +): Promise { + const publicKey = await readKey({ armoredKey: publicArmor }); + const privateKey = await readPrivateKey({ armoredKey: privateArmor }); + const decryptedMessage = await decrypt({ + message: await readMessage({ armoredMessage: encryptedAndSignedMessage.encryptedMessage }), + decryptionKeys: privateKey, + signature: await readSignature({ armoredSignature: encryptedAndSignedMessage.signature }), + verificationKeys: publicKey, + expectSigned: true, + config: { + rejectCurves: new Set(), + showVersion: false, + showComment: false, + }, + }); + return decryptedMessage.data; +} + /** * Encrypts and signs a string * @param text string to encrypt and sign