diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts index 1dc0fd5580..464cc45fab 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/job/job.service.ts @@ -347,7 +347,8 @@ export class JobService { this.pgpConfigService.passphrase, ); - manifest = JSON.parse(await encryption.decrypt(manifestEncrypted)); + const decryptedData = await encryption.decrypt(manifestEncrypted); + manifest = JSON.parse(Buffer.from(decryptedData).toString()); } catch { throw new Error(ErrorJob.ManifestDecryptionFailed); } diff --git a/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts index fd629d8588..3a39e6afb5 100644 --- a/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts +++ b/packages/apps/fortune/exchange-oracle/server/src/modules/storage/storage.service.ts @@ -57,7 +57,8 @@ export class StorageService { this.pgpConfigService.passphrase, ); - return JSON.parse(await encryption.decrypt(fileContent)) as ISolution[]; + const decryptedData = await encryption.decrypt(fileContent); + return JSON.parse(Buffer.from(decryptedData).toString()) as ISolution[]; } return typeof fileContent == 'string' diff --git a/packages/apps/fortune/recording-oracle/src/modules/storage/storage.service.ts b/packages/apps/fortune/recording-oracle/src/modules/storage/storage.service.ts index 35e60d7579..6c7bcdfa5c 100644 --- a/packages/apps/fortune/recording-oracle/src/modules/storage/storage.service.ts +++ b/packages/apps/fortune/recording-oracle/src/modules/storage/storage.service.ts @@ -53,7 +53,8 @@ export class StorageService { this.pgpConfigService.passphrase, ); - return JSON.parse(await encryption.decrypt(fileContent)); + const decryptedData = await encryption.decrypt(fileContent); + return JSON.parse(Buffer.from(decryptedData).toString()); } catch { throw new Error('Unable to decrypt manifest'); } diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts index c8a0e31c28..4de044dd1f 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.spec.ts @@ -2650,9 +2650,11 @@ describe('JobService', () => { expect(storageService.uploadFile).toHaveBeenCalled(); expect( JSON.parse( - await encryption.decrypt( - (storageService.uploadFile as any).mock.calls[0][0], - ), + Buffer.from( + await encryption.decrypt( + (storageService.uploadFile as any).mock.calls[0][0], + ), + ).toString(), ), ).toEqual(fortuneManifestParams); }); @@ -2675,9 +2677,11 @@ describe('JobService', () => { expect(storageService.uploadFile).toHaveBeenCalled(); expect( JSON.parse( - await encryption.decrypt( - (storageService.uploadFile as any).mock.calls[0][0], - ), + Buffer.from( + await encryption.decrypt( + (storageService.uploadFile as any).mock.calls[0][0], + ), + ).toString(), ), ).toEqual(fortuneManifestParams); }); @@ -2699,9 +2703,11 @@ describe('JobService', () => { expect(storageService.uploadFile).toHaveBeenCalled(); expect( JSON.parse( - await encryption.decrypt( - (storageService.uploadFile as any).mock.calls[0][0], - ), + Buffer.from( + await encryption.decrypt( + (storageService.uploadFile as any).mock.calls[0][0], + ), + ).toString(), ), ).toEqual(fortuneManifestParams); }); @@ -2768,9 +2774,11 @@ describe('JobService', () => { expect(storageService.uploadFile).toHaveBeenCalled(); expect( JSON.parse( - await encryption.decrypt( - (storageService.uploadFile as any).mock.calls[0][0], - ), + Buffer.from( + await encryption.decrypt( + (storageService.uploadFile as any).mock.calls[0][0], + ), + ).toString(), ), ).toEqual(manifest); }); @@ -2793,9 +2801,11 @@ describe('JobService', () => { expect(storageService.uploadFile).toHaveBeenCalled(); expect( JSON.parse( - await encryption.decrypt( - (storageService.uploadFile as any).mock.calls[0][0], - ), + Buffer.from( + await encryption.decrypt( + (storageService.uploadFile as any).mock.calls[0][0], + ), + ).toString(), ), ).toEqual(manifest); }); @@ -2816,9 +2826,11 @@ describe('JobService', () => { expect(storageService.uploadFile).toHaveBeenCalled(); expect( JSON.parse( - await encryption.decrypt( - (storageService.uploadFile as any).mock.calls[0][0], - ), + Buffer.from( + await encryption.decrypt( + (storageService.uploadFile as any).mock.calls[0][0], + ), + ).toString(), ), ).toEqual(manifest); }); diff --git a/packages/apps/job-launcher/server/src/modules/job/job.service.ts b/packages/apps/job-launcher/server/src/modules/job/job.service.ts index 2b753ac04d..7a14e8d8cd 100644 --- a/packages/apps/job-launcher/server/src/modules/job/job.service.ts +++ b/packages/apps/job-launcher/server/src/modules/job/job.service.ts @@ -1013,7 +1013,9 @@ export class JobService { let manifest = await this.storageService.download(jobEntity.manifestUrl); if (typeof manifest === 'string' && isPGPMessage(manifest)) { - manifest = await this.encryption.decrypt(manifest as any); + manifest = Buffer.from( + await this.encryption.decrypt(manifest), + ).toString(); } if (isValidJSON(manifest)) { @@ -1496,7 +1498,9 @@ export class JobService { let manifest; if (typeof manifestData === 'string' && isPGPMessage(manifestData)) { - manifestData = await this.encryption.decrypt(manifestData as any); + manifestData = Buffer.from( + await this.encryption.decrypt(manifestData), + ).toString(); } if (isValidJSON(manifestData)) { diff --git a/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts b/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts index e4a85a1f63..a191b5a77c 100644 --- a/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts +++ b/packages/apps/job-launcher/server/src/modules/storage/storage.service.ts @@ -41,7 +41,8 @@ export class StorageService { typeof fileContent === 'string' && EncryptionUtils.isEncrypted(fileContent) ) { - return await this.encryption.decrypt(fileContent); + const decryptedData = await this.encryption.decrypt(fileContent); + return Buffer.from(decryptedData).toString(); } else { return fileContent; } diff --git a/packages/apps/reputation-oracle/server/src/modules/storage/storage.service.ts b/packages/apps/reputation-oracle/server/src/modules/storage/storage.service.ts index 0fc19395b5..a2fe4c6e69 100644 --- a/packages/apps/reputation-oracle/server/src/modules/storage/storage.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/storage/storage.service.ts @@ -83,10 +83,12 @@ export class StorageService { this.pgpConfigService.privateKey!, this.pgpConfigService.passphrase, ); + const decryptedData = await encryption.decrypt(fileContent); + const content = Buffer.from(decryptedData).toString(); try { - return JSON.parse(await encryption.decrypt(fileContent)); + return JSON.parse(content); } catch { - return await encryption.decrypt(fileContent); + return content; } } else { return fileContent; diff --git a/packages/sdk/typescript/human-protocol-sdk/example/encrypt.ts b/packages/sdk/typescript/human-protocol-sdk/example/encrypt.ts new file mode 100644 index 0000000000..5815c11ffd --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/example/encrypt.ts @@ -0,0 +1,59 @@ +import { Encryption } from '../src/encryption'; + +const ExampleKeys = { + private: `-----BEGIN PGP PRIVATE KEY BLOCK----- + +xVgEZyOGRhYJKwYBBAHaRw8BAQdA6soD3CBjRnPfsYIxOGObykL8X8y+dZlf +IzAI7mk40E4AAQCPRLKy/1n/QxTRk/Ql+Dt2VYADqDmczBxhWELCbz9Wvw/J +zRxleGFtcGxlIDxzaW1wbGVAZXhhbXBsZS5jb20+wowEEBYKAD4FgmcjhkYE +CwkHCAmQZ4KBmVeekE0DFQgKBBYAAgECGQECmwMCHgEWIQQkzuPtNIUkzz4M +CFhngoGZV56QTQAA89oBAKm5Tai1Ynx4BCU6PSLp0QMEE2ImV/LzwHkLMz52 +mzeJAP9NyyNyIMNH1SpSezy309UhEdJVapGoXQwO1eQR4B+LCMddBGcjhkYS +CisGAQQBl1UBBQEBB0BykPL7zxjKQpZMYuEnIDBz7vshngm4zJMNOsE6pDSH +NgMBCAcAAP9b+n3/5QKVd0UP/ow3uyH0X44gc2U4fKV8IhZBJLiSEA9ZwngE +GBYKACoFgmcjhkYJkGeCgZlXnpBNApsMFiEEJM7j7TSFJM8+DAhYZ4KBmVee +kE0AAE0WAP9FE0I9dHToxLAkMKiM9tTzL43GVl6K8Lvn9nrLJcfLgQEAtFXL +39GhAUKNbHMpdeEmxukrdF+rXzUfvOsYGPLIgwI= +=GfVY +-----END PGP PRIVATE KEY BLOCK-----`, + public: `-----BEGIN PGP PUBLIC KEY BLOCK----- + +xjMEZyOGRhYJKwYBBAHaRw8BAQdA6soD3CBjRnPfsYIxOGObykL8X8y+dZlf +IzAI7mk40E7NHGV4YW1wbGUgPHNpbXBsZUBleGFtcGxlLmNvbT7CjAQQFgoA +PgWCZyOGRgQLCQcICZBngoGZV56QTQMVCAoEFgACAQIZAQKbAwIeARYhBCTO +4+00hSTPPgwIWGeCgZlXnpBNAADz2gEAqblNqLVifHgEJTo9IunRAwQTYiZX +8vPAeQszPnabN4kA/03LI3Igw0fVKlJ7PLfT1SER0lVqkahdDA7V5BHgH4sI +zjgEZyOGRhIKKwYBBAGXVQEFAQEHQHKQ8vvPGMpClkxi4ScgMHPu+yGeCbjM +kw06wTqkNIc2AwEIB8J4BBgWCgAqBYJnI4ZGCZBngoGZV56QTQKbDBYhBCTO +4+00hSTPPgwIWGeCgZlXnpBNAABNFgD/RRNCPXR06MSwJDCojPbU8y+NxlZe +ivC75/Z6yyXHy4EBALRVy9/RoQFCjWxzKXXhJsbpK3Rfq181H7zrGBjyyIMC +=wH1v +-----END PGP PUBLIC KEY BLOCK-----`, +}; + +const exampleRound = async () => { + const encryption = await Encryption.build(ExampleKeys.private); + + const message = Buffer.from( + JSON.stringify( + { + date: new Date().toISOString(), + random: Math.random(), + text: 'Hello from example!', + }, + null, + 2 + ) + ); + + const encrypted = await encryption.signAndEncrypt(message, [ + ExampleKeys.public, + ]); + + const decrypted = await encryption.decrypt(encrypted, ExampleKeys.public); + console.log('Decrypted', JSON.parse(Buffer.from(decrypted).toString())); +}; + +(async () => { + await exampleRound(); +})(); diff --git a/packages/sdk/typescript/human-protocol-sdk/src/encryption.ts b/packages/sdk/typescript/human-protocol-sdk/src/encryption.ts index c2f13803cd..072b4b2dad 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/encryption.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/encryption.ts @@ -1,6 +1,16 @@ import * as openpgp from 'openpgp'; import { IKeyPair } from './interfaces'; +type MessageDataType = string | Uint8Array; + +function makeMessageDataBinary(message: MessageDataType): Uint8Array { + if (typeof message === 'string') { + return Buffer.from(message); + } + + return message; +} + /** * ## Introduction * @@ -126,20 +136,21 @@ export class Encryption { * ``` */ public async signAndEncrypt( - message: string, + message: MessageDataType, publicKeys: string[] ): Promise { - const plaintext = message; - const pgpPublicKeys = await Promise.all( publicKeys.map((armoredKey) => openpgp.readKey({ armoredKey })) ); - const pgpMessage = await openpgp.createMessage({ text: plaintext }); + const pgpMessage = await openpgp.createMessage({ + binary: makeMessageDataBinary(message), + }); const encrypted = await openpgp.encrypt({ message: pgpMessage, encryptionKeys: pgpPublicKeys, signingKeys: this.privateKey, + format: 'armored', }); return encrypted as string; @@ -176,23 +187,43 @@ export class Encryption { * const resultMessage = await encription.decrypt('message'); * ``` */ - public async decrypt(message: string, publicKey?: string): Promise { - const pgpMessage = await openpgp.readMessage({ armoredMessage: message }); + public async decrypt( + message: string, + publicKey?: string + ): Promise { + const pgpMessage = await openpgp.readMessage({ + armoredMessage: message, + }); const decryptionOptions: openpgp.DecryptOptions = { message: pgpMessage, decryptionKeys: this.privateKey, - expectSigned: !!publicKey, + format: 'binary', }; - if (publicKey) { + const shouldVerifySignature = !!publicKey; + if (shouldVerifySignature) { const pgpPublicKey = await openpgp.readKey({ armoredKey: publicKey }); decryptionOptions.verificationKeys = pgpPublicKey; } - const { data: decrypted } = await openpgp.decrypt(decryptionOptions); + const { data: decrypted, signatures } = + await openpgp.decrypt(decryptionOptions); + + /** + * There is an option to automatically verify signatures - `expectSigned`, + * but atm it has a bug - https://github.com/openpgpjs/openpgpjs/issues/1803, + * so we have to verify it manually till it's fixed. + */ + try { + if (shouldVerifySignature) { + await signatures[0].verified; + } + } catch { + throw new Error('Signature could not be verified'); + } - return decrypted as string; + return decrypted as Uint8Array; } /** @@ -419,19 +450,20 @@ export class EncryptionUtils { * ``` */ public static async encrypt( - message: string, + message: MessageDataType, publicKeys: string[] ): Promise { - const plaintext = message; - const pgpPublicKeys = await Promise.all( publicKeys.map((armoredKey) => openpgp.readKey({ armoredKey })) ); - const pgpMessage = await openpgp.createMessage({ text: plaintext }); + const pgpMessage = await openpgp.createMessage({ + binary: makeMessageDataBinary(message), + }); const encrypted = await openpgp.encrypt({ message: pgpMessage, encryptionKeys: pgpPublicKeys, + format: 'armored', }); return encrypted as string; diff --git a/packages/sdk/typescript/human-protocol-sdk/test/encryption.test.ts b/packages/sdk/typescript/human-protocol-sdk/test/encryption.test.ts index 849fe7eab6..dac00cc13e 100644 --- a/packages/sdk/typescript/human-protocol-sdk/test/encryption.test.ts +++ b/packages/sdk/typescript/human-protocol-sdk/test/encryption.test.ts @@ -2,11 +2,13 @@ import { describe, test, expect, beforeAll } from 'vitest'; import { Encryption, EncryptionUtils } from '../src/encryption'; import { + BINARY_MESSAGE_CONTENT, ENCRYPTEDMESSAGE, KEYPAIR1, KEYPAIR2, KEYPAIR3, MESSAGE, + SIGNEDENCRYPTEDBINARYMESSAGE, SIGNEDENCRYPTEDLOCKEDMESSAGE, SIGNEDENCRYPTEDMESSAGE, SIGNEDMESSAGE, @@ -249,6 +251,20 @@ describe('Encryption', async () => { expect(encryptedMessage.includes(expectedMessageFooter)).toBeTruthy(); }); + test('should sign and ecnrypt a binary message', async () => { + const encryption = await Encryption.build(KEYPAIR1.privateKey); + const encryptedMessage = await encryption.signAndEncrypt( + Buffer.from(JSON.stringify(BINARY_MESSAGE_CONTENT)), + [KEYPAIR2.publicKey, KEYPAIR3.publicKey] + ); + console.log('en', encryptedMessage); + const expectedMessageHeader = '-----BEGIN PGP MESSAGE-----\n'; + const expectedMessageFooter = '-----END PGP MESSAGE-----\n'; + + expect(encryptedMessage.includes(expectedMessageHeader)).toBeTruthy(); + expect(encryptedMessage.includes(expectedMessageFooter)).toBeTruthy(); + }); + test('should throw an error when an invalid public key is provided', async () => { const encryption = await Encryption.build( KEYPAIR1.privateKey, @@ -285,8 +301,7 @@ describe('Encryption', async () => { SIGNEDENCRYPTEDMESSAGE, KEYPAIR1.publicKey ); - - expect(decryptedMessage).toBe(MESSAGE); + expect(Buffer.from(decryptedMessage).toString()).toBe(MESSAGE); const encryption3 = await Encryption.build( KEYPAIR3.privateKey, @@ -296,7 +311,18 @@ describe('Encryption', async () => { SIGNEDENCRYPTEDMESSAGE, KEYPAIR1.publicKey ); - expect(decryptedMessage2).toBe(MESSAGE); + expect(Buffer.from(decryptedMessage2).toString()).toBe(MESSAGE); + }); + + test('should decrypt and verify binary message', async () => { + const encryption2 = await Encryption.build(KEYPAIR2.privateKey); + const decryptedMessage = await encryption2.decrypt( + SIGNEDENCRYPTEDBINARYMESSAGE, + KEYPAIR1.publicKey + ); + expect(JSON.parse(Buffer.from(decryptedMessage).toString())).toEqual( + BINARY_MESSAGE_CONTENT + ); }); test('should decrypt and verify a message encrypted with a locked private key', async () => { @@ -306,7 +332,7 @@ describe('Encryption', async () => { KEYPAIR3.publicKey ); - expect(decryptedMessage).toBe(MESSAGE); + expect(Buffer.from(decryptedMessage).toString()).toBe(MESSAGE); }); test('should throw an error when no encrypted message is provided', async () => { @@ -332,7 +358,7 @@ describe('Encryption', async () => { ); const decryptedMessage = await encryption2.decrypt(ENCRYPTEDMESSAGE); - expect(decryptedMessage).toBe(MESSAGE); + expect(Buffer.from(decryptedMessage).toString()).toBe(MESSAGE); }); test('should fail when decrypting and verifying an unsigned message', async () => { @@ -343,7 +369,7 @@ describe('Encryption', async () => { await expect( encryption2.decrypt(ENCRYPTEDMESSAGE, KEYPAIR1.publicKey) - ).rejects.toThrowError('Error decrypting message: Message is not signed'); + ).rejects.toThrowError('Signature could not be verified'); }); }); diff --git a/packages/sdk/typescript/human-protocol-sdk/test/utils/constants.ts b/packages/sdk/typescript/human-protocol-sdk/test/utils/constants.ts index 0a90a171b7..d55c1815aa 100644 --- a/packages/sdk/typescript/human-protocol-sdk/test/utils/constants.ts +++ b/packages/sdk/typescript/human-protocol-sdk/test/utils/constants.ts @@ -140,6 +140,12 @@ WXs/I9nhKcx2pAAA5VwA/R6fW20uAR7ItubxVhQa+PLEVSiUKgYOU9jp75av }; export const MESSAGE = 'Human Protocol'; +export const BINARY_MESSAGE_CONTENT = { + number: 1, + boolean: true, + null: null, + string: MESSAGE, +} as const; export const SIGNEDMESSAGE = `-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA512 @@ -167,6 +173,21 @@ cii1NVsBkAg4xxsZHrjXKqXzHnnhseNGEW5eNYUZWZJZTZLSMGWV031Kwjm1 =pawu -----END PGP MESSAGE-----`; +export const SIGNEDENCRYPTEDBINARYMESSAGE = `-----BEGIN PGP MESSAGE----- + +wV4D62ta7QqCH7ASAQdA1Fu+j1ROzZfOhWFonXU5nqN8SS7lZ9PcOH5R0EsJ +WwMwVqgc90AQPe6QqHXADnmWRcmlMa60UqZXp6RM117IA61U3eMWZMvZpm8w +WchAe+/YwV4DM57hZqoRiU8SAQdAjBxr7VwdHExxR16YJRfl6jmS/+jVf7Ed +a1M134lkWFQwzGk4dbVx/PVlH3QbwzSuwh4EvdTku/5eA6TTAG8SsPnjeYwe ++bOn6blgHitbCyRg0sA4ARd/h3Cv3SPLv8TLLY1+oZKCxj8JixvRJ7pdQFtX +uQj4/3ebFN3Wz4Bc22og61V0bRwbItkvAkdPytAAohMSdvtmB/afqN5KleH7 +Q+hftw7iUpLxGEhykh4lHy9dt/ylzPFYP1MVAm9aTdNMo0YEeCji8EPbbxl4 +hdPtck+uT1OpewbrhZNxg5Mtzc+t5tf/TfNYllAO6u/XTTWsOBKGcLhsituV +j7PegOO0npgohXmOxKFMmjJZhJOEbDOwCxTb72lZHwOXP5f95xMuSWoUGFpO +lk9n7jB3p5HYtUDSriV2YSI+G3NWmqGHG4/XTdodK2DQb8/Hp88= +=qQI9 +-----END PGP MESSAGE-----`; + export const SIGNEDENCRYPTEDLOCKEDMESSAGE = `-----BEGIN PGP MESSAGE----- wV4D62ta7QqCH7ASAQdAlbHVdZeqqWEy2toJlJWa/2N+L6zdYJKzyiItdWZz