From 36d6cfd5fbd9c6e429be20ed820c726f05e37e4f Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Thu, 2 Mar 2023 17:41:43 +0530 Subject: [PATCH] refactor: poll decryption --- package.json | 1 - src/Utils/index.ts | 1 - src/Utils/messages-poll.ts | 139 ----------------------------------- src/Utils/process-message.ts | 50 ++++++++++++- yarn.lock | 65 +--------------- 5 files changed, 52 insertions(+), 204 deletions(-) delete mode 100644 src/Utils/messages-poll.ts diff --git a/package.json b/package.json index 9ba9e52..88ade39 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,6 @@ }, "peerDependencies": { "@adiwajshing/keyed-db": "^0.2.4", - "@peculiar/webcrypto": "^1.4.1", "jimp": "^0.16.1", "link-preview-js": "^3.0.0", "qrcode-terminal": "^0.12.0", diff --git a/src/Utils/index.ts b/src/Utils/index.ts index 23329e1..156abe0 100644 --- a/src/Utils/index.ts +++ b/src/Utils/index.ts @@ -15,4 +15,3 @@ export * from './use-multi-file-auth-state' export * from './link-preview' export * from './event-buffer' export * from './process-message' -export * from './messages-poll' diff --git a/src/Utils/messages-poll.ts b/src/Utils/messages-poll.ts deleted file mode 100644 index 1ba1e16..0000000 --- a/src/Utils/messages-poll.ts +++ /dev/null @@ -1,139 +0,0 @@ -// original code: https://gist.github.com/PurpShell/44433d21631ff0aefbea57f7b5e31139 - -/** - * Create crypto instance. - * @description If your nodejs crypto module doesn't have WebCrypto, you must install `@peculiar/webcrypto` first - * @return {Crypto} - */ -export const getCrypto = (): Crypto => { - const c = require('crypto') - - return 'subtle' in (c?.webcrypto || {}) ? c.webcrypto : new (require('@peculiar/webcrypto').Crypto)() -} - -/** - * Compare the SHA-256 hashes of the poll options from the update to find the original choices - * @param options Options from the poll creation message - * @param pollOptionHashes hash from `decryptPollMessageRaw()` - * @return {Promise} the original option, can be empty when none are currently selected - */ -export const comparePollMessage = async(options: string[], pollOptionHashes: string[]): Promise => { - const selectedOptions: string[] = [] - const crypto = getCrypto() - for(const option of options) { - const hash = Buffer - .from( - await crypto.subtle.digest( - 'SHA-256', - (new TextEncoder).encode(option) - ) - ) - .toString('hex').toUpperCase() - - if(pollOptionHashes.findIndex(h => h === hash) > -1) { - selectedOptions.push(option) - } - } - - ; - return selectedOptions -} - -/** - * Raw method to decrypt the message after gathering all information - * @description Use `decryptPollMessage()` instead, only use this if you know what you are doing - * @param encPayload Encryption payload/contents want to decrypt, you can get it from `pollUpdateMessage.vote.encPayload` - * @param encIv Encryption iv (used to decrypt the payload), you can get it from `pollUpdateMessage.vote.encIv` - * @param additionalData poll Additional data to decrypt poll message - * @param decryptionKey Generated decryption key to decrypt the poll message - * @return {Promise} - */ -const decryptPollMessageInternal = async( - encPayload: Uint8Array, - encIv: Uint8Array, - additionalData: Uint8Array, - decryptionKey: Uint8Array, -): Promise => { - const crypto = getCrypto() - - const tagSize_multiplier = 16 - const encoded = encPayload - const key = await crypto.subtle.importKey('raw', decryptionKey, 'AES-GCM', false, ['encrypt', 'decrypt']) - const decrypted = await crypto.subtle.decrypt({ name: 'AES-GCM', iv: encIv, additionalData: additionalData, tagLength: 8 * tagSize_multiplier }, key, encoded) - return new Uint8Array(decrypted).slice(2) // remove 2 bytes (OA20)(space+newline) -} - -/** - * Decode the message from `decryptPollMessageInternal()` - * @param decryptedMessage the message from `decrpytPollMessageInternal()` - * @return {string} - */ -export const decodePollMessage = (decryptedMessage: Uint8Array): string => { - const n = [48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65, 66, 67, 68, 69, 70] - const outarr: number[] = [] - - for(let i = 0; i < decryptedMessage.length; i++) { - const val = decryptedMessage[i] - outarr.push(n[val >> 4], n[15 & val]) - } - - return String.fromCharCode(...outarr) -} - -/** - * raw function to decrypt a poll message update - * @param encPayload Encryption payload/contents want to decrypt, you can get it from `pollUpdateMessage.vote.encPayload` - * @param encIv Encryption iv (used to decrypt the payload), you can get it from `pollUpdateMessage.vote.encIv` - * @param encKey Encryption key (used to decrypt the payload), you need to store/save the encKey. If you want get the encKey, you could get it from `Message.messageContextInfo.messageSecret`, only available on poll creation message. - * @param pollMsgSender sender The sender's jid of poll message, you can use `pollUpdateMessage.pollCreationMessageKey.participant` (Note: you need to normalize the jid first) - * @param pollMsgId The ID of poll message, you can use `pollUpdateMessage.pollMessageCreationKey.id` - * @param voteMsgSender The poll voter's jid, you can use `Message.key.remoteJid`, `Message.key.participant`, or `Message.participant`. (Note: you need to normalize the jid first) - * @return {Promise} The option or empty array if something went wrong OR everything was unticked - */ -export const decryptPollMessageRaw = async( - encKey: Uint8Array, - encPayload: Uint8Array, - encIv: Uint8Array, - pollMsgSender: string, - pollMsgId: string, - voteMsgSender: string -): Promise => { - const enc = new TextEncoder() - const crypto = getCrypto() - - const stanzaId = enc.encode(pollMsgId) - const parentMsgOriginalSender = enc.encode(pollMsgSender) - const modificationSender = enc.encode(voteMsgSender) - const modificationType = enc.encode('Poll Vote') - const pad = new Uint8Array([1]) - - const signMe = new Uint8Array([...stanzaId, ...parentMsgOriginalSender, ...modificationSender, ...modificationType, pad] as any) - - const createSignKey = async(n: Uint8Array = new Uint8Array(32)) => { - return (await crypto.subtle.importKey('raw', n, - { 'name': 'HMAC', 'hash': 'SHA-256' }, false, ['sign'] - )) - } - - const sign = async(n: Uint8Array, key: CryptoKey) => { - return (await crypto.subtle.sign({ 'name': 'HMAC', 'hash': 'SHA-256' }, key, n)) - } - - let key = await createSignKey() - - const temp = await sign(encKey, key) - - key = await createSignKey(new Uint8Array(temp)) - - const decryptionKey = new Uint8Array(await sign(signMe, key)) - - const additionalData = enc.encode(`${pollMsgId}\u0000${voteMsgSender}`) - - const decryptedMessage = await decryptPollMessageInternal(encPayload, encIv, additionalData, decryptionKey) - - const pollOptionHash = decodePollMessage(decryptedMessage) - - // '0A20' in hex represents unicode " " and "\n" thus declaring the end of one option - // we want multiple hashes to make it easier to iterate and understand for your use cases - return pollOptionHash.split('0A20') || [] -} diff --git a/src/Utils/process-message.ts b/src/Utils/process-message.ts index 87a88ee..ad4bea8 100644 --- a/src/Utils/process-message.ts +++ b/src/Utils/process-message.ts @@ -2,8 +2,11 @@ import { AxiosRequestConfig } from 'axios' import type { Logger } from 'pino' import { proto } from '../../WAProto' import { AuthenticationCreds, BaileysEventEmitter, Chat, GroupMetadata, ParticipantAction, SignalKeyStoreWithTransaction, WAMessageStubType } from '../Types' -import { downloadAndProcessHistorySyncNotification, getContentType, normalizeMessageContent, toNumber } from '../Utils' +import { getContentType, normalizeMessageContent } from '../Utils/messages' import { areJidsSameUser, isJidBroadcast, isJidStatusBroadcast, jidNormalizedUser } from '../WABinary' +import { aesDecryptGCM, hmacSign } from './crypto' +import { getKeyAuthor, toNumber } from './generics' +import { downloadAndProcessHistorySyncNotification } from './history' type ProcessMessageContext = { shouldProcessHistoryMsg: boolean @@ -88,6 +91,51 @@ export const getChatId = ({ remoteJid, participant, fromMe }: proto.IMessageKey) return remoteJid! } +type PollContext = { + /** normalised jid of the person that created the poll */ + pollCreatorJid: string + /** ID of the poll creation message */ + pollMsgId: string + /** poll creation message enc key */ + pollEncKey: Uint8Array + /** jid of the person that voted */ + voterJid: string +} + +/** + * Decrypt a poll vote + * @param vote encrypted vote + * @param ctx additional info about the poll required for decryption + * @returns list of SHA256 options + */ +export function decryptPollVote( + { encPayload, encIv }: proto.Message.IPollEncValue, + { + pollCreatorJid, + pollMsgId, + pollEncKey, + voterJid, + }: PollContext +) { + const sign = Buffer.concat( + [ + toBinary(pollMsgId), + toBinary(pollCreatorJid), + toBinary(voterJid), + toBinary('Poll Vote'), + new Uint8Array([1]) + ] + ) + + const key0 = hmacSign(pollEncKey, new Uint8Array(32), 'sha256') + const decKey = hmacSign(sign, key0, 'sha256') + const aad = toBinary(`${pollMsgId}\u0000${voterJid}`) + + const decrypted = aesDecryptGCM(encPayload!, decKey, encIv!, aad) + return proto.Message.PollVoteMessage.decode(decrypted) + + function toBinary(txt: string) { + return Buffer.from(txt) const processMessage = async( message: proto.IWebMessageInfo, { diff --git a/yarn.lock b/yarn.lock index 2395efb..43a983d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -897,33 +897,6 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@peculiar/asn1-schema@^2.1.6", "@peculiar/asn1-schema@^2.3.0": - version "2.3.3" - resolved "https://registry.yarnpkg.com/@peculiar/asn1-schema/-/asn1-schema-2.3.3.tgz#21418e1f3819e0b353ceff0c2dad8ccb61acd777" - integrity sha512-6GptMYDMyWBHTUKndHaDsRZUO/XMSgIns2krxcm2L7SEExRHwawFvSwNBhqNPR9HJwv3MruAiF1bhN0we6j6GQ== - dependencies: - asn1js "^3.0.5" - pvtsutils "^1.3.2" - tslib "^2.4.0" - -"@peculiar/json-schema@^1.1.12": - version "1.1.12" - resolved "https://registry.yarnpkg.com/@peculiar/json-schema/-/json-schema-1.1.12.tgz#fe61e85259e3b5ba5ad566cb62ca75b3d3cd5339" - integrity sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w== - dependencies: - tslib "^2.0.0" - -"@peculiar/webcrypto@^1.4.1": - version "1.4.1" - resolved "https://registry.yarnpkg.com/@peculiar/webcrypto/-/webcrypto-1.4.1.tgz#821493bd5ad0f05939bd5f53b28536f68158360a" - integrity sha512-eK4C6WTNYxoI7JOabMoZICiyqRRtJB220bh0Mbj5RwRycleZf9BPyZoxsTvpP0FpmVS2aS13NKOuh5/tN3sIRw== - dependencies: - "@peculiar/asn1-schema" "^2.3.0" - "@peculiar/json-schema" "^1.1.12" - pvtsutils "^1.3.2" - tslib "^2.4.1" - webcrypto-core "^1.7.4" - "@protobufjs/aspromise@^1.1.1", "@protobufjs/aspromise@^1.1.2": version "1.1.2" resolved "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz" @@ -1424,15 +1397,6 @@ array.prototype.flatmap@^1.3.0: es-abstract "^1.19.2" es-shim-unscopables "^1.0.0" -asn1js@^3.0.1, asn1js@^3.0.5: - version "3.0.5" - resolved "https://registry.yarnpkg.com/asn1js/-/asn1js-3.0.5.tgz#5ea36820443dbefb51cc7f88a2ebb5b462114f38" - integrity sha512-FVnvrKJwpt9LP2lAMl8qZswRNm3T4q9CON+bxldk2iwk3FFpuwhx2FfinyitizWHsVYyaY+y5JzDR0rCMV5yTQ== - dependencies: - pvtsutils "^1.3.2" - pvutils "^1.1.3" - tslib "^2.4.0" - asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -3488,7 +3452,7 @@ json-stable-stringify-without-jsonify@^1.0.1: json5@2.x, json5@^2.2.1: version "2.2.3" - resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + resolved "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz" integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== jsonc-parser@^3.0.0: @@ -3532,7 +3496,7 @@ levn@~0.3.0: "libsignal@git+https://github.com/adiwajshing/libsignal-node": version "2.0.1" - resolved "git+https://github.com/adiwajshing/libsignal-node.git#11dbd962ea108187c79a7c46fe4d6f790e23da97" + resolved "git+ssh://git@github.com/adiwajshing/libsignal-node.git#11dbd962ea108187c79a7c46fe4d6f790e23da97" dependencies: curve25519-js "^0.0.4" protobufjs "6.8.8" @@ -4259,18 +4223,6 @@ punycode@^2.1.0, punycode@^2.1.1: resolved "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz" integrity sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A== -pvtsutils@^1.3.2: - version "1.3.2" - resolved "https://registry.yarnpkg.com/pvtsutils/-/pvtsutils-1.3.2.tgz#9f8570d132cdd3c27ab7d51a2799239bf8d8d5de" - integrity sha512-+Ipe2iNUyrZz+8K/2IOo+kKikdtfhRKzNpQbruF2URmqPtoqAs8g3xS7TJvFF2GcPXjh7DkqMnpVveRFq4PgEQ== - dependencies: - tslib "^2.4.0" - -pvutils@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/pvutils/-/pvutils-1.1.3.tgz#f35fc1d27e7cd3dfbd39c0826d173e806a03f5a3" - integrity sha512-pMpnA0qRdFp32b1sJl1wOJNxZLQ2cbQx+k6tjNtZ8CpvVhNqEPRgivZ2WOUev2YMajecdH7ctUPDvEe87nariQ== - qrcode-terminal@^0.12.0: version "0.12.0" resolved "https://registry.npmjs.org/qrcode-terminal/-/qrcode-terminal-0.12.0.tgz" @@ -4906,7 +4858,7 @@ tslib@^1.8.1: resolved "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.0, tslib@^2.4.0, tslib@^2.4.1: +tslib@^2.4.0: version "2.4.1" resolved "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz" integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== @@ -5069,17 +5021,6 @@ walker@^1.0.7: dependencies: makeerror "1.0.12" -webcrypto-core@^1.7.4: - version "1.7.5" - resolved "https://registry.yarnpkg.com/webcrypto-core/-/webcrypto-core-1.7.5.tgz#c02104c953ca7107557f9c165d194c6316587ca4" - integrity sha512-gaExY2/3EHQlRNNNVSrbG2Cg94Rutl7fAaKILS1w8ZDhGxdFOaw6EbCfHIxPy9vt/xwp5o0VQAx9aySPF6hU1A== - dependencies: - "@peculiar/asn1-schema" "^2.1.6" - "@peculiar/json-schema" "^1.1.12" - asn1js "^3.0.1" - pvtsutils "^1.3.2" - tslib "^2.4.0" - webidl-conversions@^3.0.0: version "3.0.1" resolved "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"