separate out crypto utils

This commit is contained in:
Adhiraj Singh
2021-09-23 22:37:33 +05:30
parent f267f27ada
commit f1527a9f6b
13 changed files with 131 additions and 131 deletions

View File

@@ -78,9 +78,10 @@ If the connection is successful, you will see a QR code printed on your terminal
## Notable Differences Between Baileys Web & MD
1. Baileys no longer maintains an internal state of chats/contacts/messages. You must take this on your own, simply because your state in MD is its own source of truth & there is no one-size-fits-all way to handle the storage for this.
2. A baileys "socket" is meant to be a temporary & disposable object -- this is done to maintain simplicity & prevent bugs. I felt the entire Baileys object became too bloated as it supported too many configurations.
3. Moreover, Baileys does not offer an inbuilt reconnect mechanism anymore (though it's super easy to set one up on your own with your own rules, check the example script)
1. Baileys has been written from the ground up to have a more "functional" structure. This is done primarily for simplicity & more testability
2. Baileys no longer maintains an internal state of chats/contacts/messages. You must take this on your own, simply because your state in MD is its own source of truth & there is no one-size-fits-all way to handle the storage for this.
3. A baileys "socket" is meant to be a temporary & disposable object -- this is done to maintain simplicity & prevent bugs. I felt the entire Baileys object became too bloated as it supported too many configurations. You're encouraged to write your own implementation to handle missing functionality.
4. Moreover, Baileys does not offer an inbuilt reconnect mechanism anymore (though it's super easy to set one up on your own with your own rules, check the example script)
## Configuring the Connection

View File

@@ -4,7 +4,7 @@ import { BinaryNode, getBinaryNodeChild, getBinaryNodeChildren, jidNormalizedUse
import { makeSocket } from "./socket";
import { proto } from '../../WAProto'
import { toNumber } from "../Utils/generics";
import { compressImage, generateProfilePicture } from "..";
import { generateProfilePicture } from "../Utils";
export const makeChatsSocket = (config: SocketConfig) => {
const { logger } = config

View File

@@ -1,8 +1,8 @@
import { makeGroupsSocket } from "./groups"
import { SocketConfig, WAMessageStubType, ParticipantAction, Chat, GroupMetadata } from "../Types"
import { decodeMessageStanza, encodeBigEndian, toNumber, whatsappID } from "../Utils"
import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, S_WHATSAPP_NET, areJidsSameUser, getBinaryNodeChildren, getBinaryNodeChild } from '../WABinary'
import { decodeMessageStanza, encodeBigEndian, toNumber } from "../Utils"
import { BinaryNode, jidDecode, jidEncode, isJidStatusBroadcast, areJidsSameUser, getBinaryNodeChildren } from '../WABinary'
import { downloadIfHistory } from '../Utils/history'
import { proto } from "../../WAProto"
import { generateSignalPubKey, xmppPreKey, xmppSignedPreKey } from "../Utils/signal"
@@ -139,7 +139,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
switch (message.messageStubType) {
case WAMessageStubType.GROUP_PARTICIPANT_LEAVE:
case WAMessageStubType.GROUP_PARTICIPANT_REMOVE:
participants = message.messageStubParameters.map(whatsappID)
participants = message.messageStubParameters
emitParticipantsUpdate('remove')
// mark the chat read only if you left the group
if (participants.includes(meJid)) {
@@ -149,7 +149,7 @@ export const makeMessagesRecvSocket = (config: SocketConfig) => {
case WAMessageStubType.GROUP_PARTICIPANT_ADD:
case WAMessageStubType.GROUP_PARTICIPANT_INVITE:
case WAMessageStubType.GROUP_PARTICIPANT_ADD_REQUEST_JOIN:
participants = message.messageStubParameters.map(whatsappID)
participants = message.messageStubParameters
if (participants.includes(meJid)) {
chatUpdate.readOnly = false
}

View File

@@ -5,7 +5,7 @@ import WebSocket from "ws"
import { randomBytes } from 'crypto'
import { proto } from '../../WAProto'
import { DisconnectReason, SocketConfig, BaileysEventEmitter } from "../Types"
import { generateCurveKeyPair, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout } from "../Utils"
import { Curve, initAuthState, generateRegistrationNode, configureSuccessfulPairing, generateLoginNode, encodeBigEndian, promiseTimeout } from "../Utils"
import { DEFAULT_ORIGIN, DEF_TAG_PREFIX, DEF_CALLBACK_PREFIX, KEY_BUNDLE_TYPE } from "../Defaults"
import { assertNodeErrorFree, BinaryNode, encodeBinaryNode, S_WHATSAPP_NET } from '../WABinary'
import noiseHandler from '../Utils/noise-handler'
@@ -42,7 +42,7 @@ export const makeSocket = ({
})
ws.setMaxListeners(0)
/** ephemeral key pair used to encrypt/decrypt communication. Unique for each connection */
const ephemeralKeyPair = generateCurveKeyPair()
const ephemeralKeyPair = Curve.generateKeyPair()
/** WA noise protocol wrapper */
const noise = noiseHandler(ephemeralKeyPair)
const authState = initialAuthState || initAuthState()

View File

@@ -1,5 +1,5 @@
import { Boom } from '@hapi/boom'
import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./generics"
import { aesDecrypt, hmacSign, aesEncrypt, hkdf } from "./crypto"
import { AuthenticationState, ChatModification } from "../Types"
import { proto } from '../../WAProto'
import { LT_HASH_ANTI_TAMPERING } from '../WABinary/LTHash'

100
src/Utils/crypto.ts Normal file
View File

@@ -0,0 +1,100 @@
import CurveCrypto from 'libsignal/src/curve25519_wrapper'
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
import { KeyPair } from '../Types'
export const Curve = {
generateKeyPair: (): KeyPair => {
const { pubKey, privKey } = CurveCrypto.keyPair(randomBytes(32))
return {
private: Buffer.from(privKey),
public: Buffer.from(pubKey)
}
},
sharedKey: (privateKey: Uint8Array, publicKey: Uint8Array) => {
const shared = CurveCrypto.sharedSecret(publicKey, privateKey)
return Buffer.from(shared)
},
sign: (privateKey: Uint8Array, buf: Uint8Array) => (
Buffer.from(CurveCrypto.sign(privateKey, buf))
),
verify: (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => {
try {
CurveCrypto.verify(pubKey, message, signature)
return true
} catch(error) {
if(error.message.includes('Invalid')) {
return false
}
throw error
}
}
}
export const signedKeyPair = (keyPair: KeyPair, keyId: number) => {
const signKeys = Curve.generateKeyPair()
const pubKey = new Uint8Array(33)
pubKey.set([5], 0)
pubKey.set(signKeys.public, 1)
const signature = Curve.sign(keyPair.private, pubKey)
return { keyPair: signKeys, signature, keyId }
}
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
export function aesDecrypt(buffer: Buffer, key: Buffer) {
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
}
/** decrypt AES 256 CBC */
export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
const aes = createDecipheriv('aes-256-cbc', key, IV)
return Buffer.concat([aes.update(buffer), aes.final()])
}
// encrypt AES 256 CBC; where a random IV is prefixed to the buffer
export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) {
const IV = randomBytes(16)
const aes = createCipheriv('aes-256-cbc', key, IV)
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
}
// encrypt AES 256 CBC with a given IV
export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
const aes = createCipheriv('aes-256-cbc', key, IV)
return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer
}
// sign HMAC using SHA 256
export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') {
return createHmac(variant, key).update(buffer).digest()
}
export function sha256(buffer: Buffer) {
return createHash('sha256').update(buffer).digest()
}
// HKDF key expansion
// from: https://github.com/benadida/node-hkdf
export function hkdf(buffer: Buffer, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) {
const hashAlg = 'sha256'
const hashLength = 32
salt = salt || Buffer.alloc(hashLength)
// now we compute the PRK
const prk = createHmac(hashAlg, salt).update(buffer).digest()
let prev = Buffer.from([])
const buffers = []
const num_blocks = Math.ceil(expandedLength / hashLength)
const infoBuff = Buffer.from(info || [])
for (var i=0; i<num_blocks; i++) {
const hmac = createHmac(hashAlg, prk)
// XXX is there a more optimal way to build up buffers?
const input = Buffer.concat([
prev,
infoBuff,
Buffer.from(String.fromCharCode(i + 1))
]);
hmac.update(input)
prev = hmac.digest()
buffers.push(prev)
}
return Buffer.concat(buffers, expandedLength)
}

View File

@@ -1,8 +1,6 @@
import { Boom } from '@hapi/boom'
import CurveCrypto from 'libsignal/src/curve25519_wrapper'
import { createCipheriv, createDecipheriv, createHash, createHmac, randomBytes } from 'crypto'
import { randomBytes } from 'crypto'
import { platform, release } from 'os'
import { KeyPair } from '../Types'
import { proto } from '../../WAProto'
import { Binary } from '../WABinary'
@@ -71,46 +69,6 @@ export const encodeWAMessage = (message: proto.IMessage) => (
)
)
export const generateCurveKeyPair = (): KeyPair => {
const { pubKey, privKey } = CurveCrypto.keyPair(randomBytes(32))
return {
private: Buffer.from(privKey),
public: Buffer.from(pubKey)
}
}
export const generateSharedKey = (privateKey: Uint8Array, publicKey: Uint8Array) => {
const shared = CurveCrypto.sharedSecret(publicKey, privateKey)
return Buffer.from(shared)
}
export const curveSign = (privateKey: Uint8Array, buf: Uint8Array) => (
Buffer.from(CurveCrypto.sign(privateKey, buf))
)
export const curveVerify = (pubKey: Uint8Array, message: Uint8Array, signature: Uint8Array) => {
try {
CurveCrypto.verify(pubKey, message, signature)
return true
} catch(error) {
if(error.message.includes('Invalid')) {
return false
}
throw error
}
}
export const signedKeyPair = (keyPair: KeyPair, keyId: number) => {
const signKeys = generateCurveKeyPair()
const pubKey = new Uint8Array(33)
pubKey.set([5], 0)
pubKey.set(signKeys.public, 1)
const signature = curveSign(keyPair.private, pubKey)
return { keyPair: signKeys, signature, keyId }
}
export const generateRegistrationId = () => (
Uint16Array.from(randomBytes(2))[0] & 0x3fff
)
@@ -134,9 +92,6 @@ export const encodeBigEndian = (e: number, t=4) => {
export const toNumber = (t: Long | number) => (typeof t?.['low'] !== 'undefined' ? t['low'] : t) as number
export const whatsappID = (jid: string) => jid?.replace ('@c.us', '@s.whatsapp.net')
export const isGroupID = (jid: string) => jid?.endsWith ('@g.us')
export function shallowChanges <T> (old: T, current: T, {lookForDeletedKeys}: {lookForDeletedKeys: boolean}): Partial<T> {
let changes: Partial<T> = {}
for (let key in current) {
@@ -153,64 +108,6 @@ export function shallowChanges <T> (old: T, current: T, {lookForDeletedKeys}: {l
}
return changes
}
/** decrypt AES 256 CBC; where the IV is prefixed to the buffer */
export function aesDecrypt(buffer: Buffer, key: Buffer) {
return aesDecryptWithIV(buffer.slice(16, buffer.length), key, buffer.slice(0, 16))
}
/** decrypt AES 256 CBC */
export function aesDecryptWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
const aes = createDecipheriv('aes-256-cbc', key, IV)
return Buffer.concat([aes.update(buffer), aes.final()])
}
// encrypt AES 256 CBC; where a random IV is prefixed to the buffer
export function aesEncrypt(buffer: Buffer | Uint8Array, key: Buffer) {
const IV = randomBytes(16)
const aes = createCipheriv('aes-256-cbc', key, IV)
return Buffer.concat([IV, aes.update(buffer), aes.final()]) // prefix IV to the buffer
}
// encrypt AES 256 CBC with a given IV
export function aesEncrypWithIV(buffer: Buffer, key: Buffer, IV: Buffer) {
const aes = createCipheriv('aes-256-cbc', key, IV)
return Buffer.concat([aes.update(buffer), aes.final()]) // prefix IV to the buffer
}
// sign HMAC using SHA 256
export function hmacSign(buffer: Buffer | Uint8Array, key: Buffer | Uint8Array, variant: 'sha256' | 'sha512' = 'sha256') {
return createHmac(variant, key).update(buffer).digest()
}
export function sha256(buffer: Buffer) {
return createHash('sha256').update(buffer).digest()
}
// HKDF key expansion
// from: https://github.com/benadida/node-hkdf
export function hkdf(buffer: Buffer, expandedLength: number, { info, salt }: { salt?: Buffer, info?: string }) {
const hashAlg = 'sha256'
const hashLength = 32
salt = salt || Buffer.alloc(hashLength)
// now we compute the PRK
const prk = createHmac(hashAlg, salt).update(buffer).digest()
let prev = Buffer.from([])
const buffers = []
const num_blocks = Math.ceil(expandedLength / hashLength)
const infoBuff = Buffer.from(info || [])
for (var i=0; i<num_blocks; i++) {
const hmac = createHmac(hashAlg, prk)
// XXX is there a more optimal way to build up buffers?
const input = Buffer.concat([
prev,
infoBuff,
Buffer.from(String.fromCharCode(i + 1))
]);
hmac.update(input)
prev = hmac.digest()
buffers.push(prev)
}
return Buffer.concat(buffers, expandedLength)
}
/** unix timestamp of a date in seconds */
export const unixTimestampSeconds = (date: Date = new Date()) => Math.floor(date.getTime()/1000)

View File

@@ -2,4 +2,5 @@ export * from './decode-wa-message'
export * from './generics'
export * from './messages'
export * from './messages-media'
export * from './validate-connection'
export * from './validate-connection'
export * from './crypto'

View File

@@ -11,7 +11,8 @@ import { join } from 'path'
import { once } from 'events'
import got, { Options, Response } from 'got'
import { MessageType, WAMessageContent, WAProto, WAGenericMediaMessage, WAMediaUpload, MediaType } from '../Types'
import { generateMessageID, hkdf } from './generics'
import { generateMessageID } from './generics'
import { hkdf } from './crypto'
import { DEFAULT_ORIGIN } from '../Defaults'
export const hkdfInfoKey = (type: MediaType) => {

View File

@@ -18,7 +18,7 @@ import {
MediaType,
WAMessageStatus
} from "../Types"
import { generateMessageID, unixTimestampSeconds, whatsappID } from "./generics"
import { generateMessageID, unixTimestampSeconds } from "./generics"
import { encryptedStream, generateThumbnail, getAudioDuration } from "./messages-media"
type MediaUploadData = {
@@ -268,8 +268,6 @@ export const generateWAMessageFromContent = (
options: MessageGenerationOptionsFromContent
) => {
if (!options.timestamp) options.timestamp = new Date() // set timestamp to now
// prevent an annoying bug (WA doesn't accept sending messages with '@c.us')
jid = whatsappID(jid)
const key = Object.keys(message)[0]
const timestamp = unixTimestampSeconds(options.timestamp)

View File

@@ -1,4 +1,4 @@
import { sha256, generateSharedKey, hkdf } from "./generics";
import { sha256, Curve, hkdf } from "./crypto";
import { Binary } from "../WABinary";
import { createCipheriv, createDecipheriv } from "crypto";
import { NOISE_MODE, NOISE_WA_HEADER } from "../Defaults";
@@ -100,10 +100,10 @@ export default ({ public: publicKey, private: privateKey }: KeyPair) => {
finishInit,
processHandshake: ({ serverHello }: proto.HandshakeMessage, noiseKey: KeyPair) => {
authenticate(serverHello!.ephemeral!)
mixIntoKey(generateSharedKey(privateKey, serverHello.ephemeral!))
mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral!))
const decStaticContent = decrypt(serverHello!.static!)
mixIntoKey(generateSharedKey(privateKey, decStaticContent))
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
const certDecoded = decrypt(serverHello!.payload!)
const { details: certDetails, signature: certSignature } = proto.NoiseCertificate.decode(certDecoded)
@@ -115,7 +115,7 @@ export default ({ public: publicKey, private: privateKey }: KeyPair) => {
}
const keyEnc = encrypt(noiseKey.public)
mixIntoKey(generateSharedKey(noiseKey.private, serverHello!.ephemeral!))
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello!.ephemeral!))
return keyEnc
},

View File

@@ -1,5 +1,6 @@
import * as libsignal from 'libsignal'
import { encodeBigEndian, generateCurveKeyPair } from "./generics"
import { encodeBigEndian } from "./generics"
import { Curve } from "./crypto"
import { SenderKeyDistributionMessage, GroupSessionBuilder, SenderKeyRecord, SenderKeyName, GroupCipher } from '../../WASignalGroup'
import { SignalIdentity, SignalKeyStore, SignedKeyPair, KeyPair, AuthenticationState } from "../Types/Auth"
import { assertNodeErrorFree, BinaryNode, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildUInt, jidDecode } from "../WABinary"
@@ -48,7 +49,7 @@ export const generateOrGetPreKeys = ({ creds }: AuthenticationState, range: numb
const newPreKeys: { [id: number]: KeyPair } = { }
if(remaining > 0) {
for(let i = creds.nextPreKeyId;i <= lastPreKeyId;i++) {
newPreKeys[i] = generateCurveKeyPair()
newPreKeys[i] = Curve.generateKeyPair()
}
}
return {

View File

@@ -2,7 +2,8 @@ import { Boom } from '@hapi/boom'
import { randomBytes } from 'crypto'
import { proto } from '../../WAProto'
import type { AuthenticationState, SocketConfig, SignalKeyStore, AuthenticationCreds, KeyPair } from "../Types"
import { curveSign, hmacSign, curveVerify, encodeInt, generateCurveKeyPair, generateRegistrationId, signedKeyPair } from './generics'
import { Curve, hmacSign, signedKeyPair } from './crypto'
import { encodeInt, generateRegistrationId } from './generics'
import { BinaryNode, S_WHATSAPP_NET, jidDecode, Binary } from '../WABinary'
import { createSignalIdentity } from './signal'
@@ -116,10 +117,10 @@ export const initInMemoryKeyStore = (
}
export const initAuthState = (): AuthenticationState => {
const identityKey = generateCurveKeyPair()
const identityKey = Curve.generateKeyPair()
return {
creds: {
noiseKey: generateCurveKeyPair(),
noiseKey: Curve.generateKeyPair(),
signedIdentityKey: identityKey,
signedPreKey: signedKeyPair(identityKey, 1),
registrationId: generateRegistrationId(),
@@ -158,12 +159,12 @@ export const configureSuccessfulPairing = (
const { accountSignatureKey, accountSignature } = account
const accountMsg = Binary.build(new Uint8Array([6, 0]), account.details, signedIdentityKey.public).readByteArray()
if (!curveVerify(accountSignatureKey, accountMsg, accountSignature)) {
if (!Curve.verify(accountSignatureKey, accountMsg, accountSignature)) {
throw new Boom('Failed to verify account signature')
}
const deviceMsg = Binary.build(new Uint8Array([6, 1]), account.details, signedIdentityKey.public, account.accountSignatureKey).readByteArray()
account.deviceSignature = curveSign(signedIdentityKey.private, deviceMsg)
account.deviceSignature = Curve.sign(signedIdentityKey.private, deviceMsg)
const identity = createSignalIdentity(jid, accountSignatureKey)