@zervonex/baileys 0.0.0-stage → 3.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +3203 -2
- package/WAProto/WAProtoCompile.js +291 -0
- package/WAProto/fetcher.js +69 -0
- package/WAProto/index.js +88 -0
- package/WAProto/parser.js +313 -0
- package/engine-requirements.js +10 -0
- package/lib/Builders/AIRich.js +590 -0
- package/lib/Builders/base.js +130 -0
- package/lib/Builders/builders.js +448 -0
- package/lib/Builders/index.js +3 -0
- package/lib/Defaults/index.js +225 -0
- package/lib/Defaults/phonenumber-mcc.json +223 -0
- package/lib/Signal/index.js +2 -0
- package/lib/Signal/libsignal.js +523 -0
- package/lib/Signal/lid-mapping.js +283 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.js +84 -0
- package/lib/Socket/business.js +152 -0
- package/lib/Socket/chats.js +855 -0
- package/lib/Socket/communities.js +147 -0
- package/lib/Socket/groups.js +294 -0
- package/lib/Socket/index.js +15 -0
- package/lib/Socket/messages-recv.js +904 -0
- package/lib/Socket/messages-send.js +693 -0
- package/lib/Socket/mex.js +34 -0
- package/lib/Socket/newsletter.js +752 -0
- package/lib/Socket/nexus-handler.js +55 -0
- package/lib/Socket/registration.js +155 -0
- package/lib/Socket/socket.js +798 -0
- package/lib/Socket/spam-report.js +65 -0
- package/lib/Socket/username.js +82 -0
- package/lib/Store/index.js +6 -0
- package/lib/Store/make-cache-manager-store.js +81 -0
- package/lib/Store/make-in-memory-store.js +425 -0
- package/lib/Store/make-ordered-dictionary.js +82 -0
- package/lib/Store/object-repository.js +31 -0
- package/lib/Store/reporting-info-store.js +38 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +15 -0
- package/lib/Types/Newsletter.js +100 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +14 -0
- package/lib/Types/State.js +26 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +30 -0
- package/lib/Utils/auth-utils.js +142 -0
- package/lib/Utils/baileys-event-stream.js +33 -0
- package/lib/Utils/bot-capabilities.js +40 -0
- package/lib/Utils/bot-planning-replay.js +44 -0
- package/lib/Utils/browser-utils.js +26 -0
- package/lib/Utils/business.js +75 -0
- package/lib/Utils/chat-utils.js +316 -0
- package/lib/Utils/companion-reg-client-utils.js +20 -0
- package/lib/Utils/consumer-application.js +38 -0
- package/lib/Utils/crypto.js +52 -0
- package/lib/Utils/decode-wa-message.js +197 -0
- package/lib/Utils/event-buffer.js +273 -0
- package/lib/Utils/extras.js +186 -0
- package/lib/Utils/generics.js +195 -0
- package/lib/Utils/history.js +51 -0
- package/lib/Utils/identity-change-handler.js +19 -0
- package/lib/Utils/index.js +39 -0
- package/lib/Utils/key-store.js +3 -0
- package/lib/Utils/link-preview.js +51 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.js +3 -0
- package/lib/Utils/make-mutex.js +16 -0
- package/lib/Utils/message-composer.js +59 -0
- package/lib/Utils/message-retry-manager.js +48 -0
- package/lib/Utils/messages-media.js +520 -0
- package/lib/Utils/messages.js +742 -0
- package/lib/Utils/meta-ai-msmsg.js +122 -0
- package/lib/Utils/meta-compositing.js +75 -0
- package/lib/Utils/modern-messages.js +235 -0
- package/lib/Utils/native-flow.js +55 -0
- package/lib/Utils/noise-handler.js +161 -0
- package/lib/Utils/pre-key-manager.js +76 -0
- package/lib/Utils/process-message.js +399 -0
- package/lib/Utils/reporting-utils.js +171 -0
- package/lib/Utils/signal.js +167 -0
- package/lib/Utils/spam-report-utils.js +142 -0
- package/lib/Utils/sync-action-utils.js +33 -0
- package/lib/Utils/tc-token-utils.js +102 -0
- package/lib/Utils/use-multi-file-auth-state.js +307 -0
- package/lib/Utils/validate-connection.js +181 -0
- package/lib/WABinary/constants.js +1321 -0
- package/lib/WABinary/decode.js +152 -0
- package/lib/WABinary/encode.js +140 -0
- package/lib/WABinary/generic-utils.js +179 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +235 -0
- package/lib/WABinary/types.js +4 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncBotProfileProtocol.js +30 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +48 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +23 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +28 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +15 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +24 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +9 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +21 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +19 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +22 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +16 -0
- package/lib/WAUSync/Protocols/index.js +12 -0
- package/lib/WAUSync/USyncQuery.js +100 -0
- package/lib/WAUSync/USyncUser.js +20 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/index.js +68 -0
- package/package.json +216 -5
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { hkdf, aesDecryptGCM } from './crypto.js'
|
|
2
|
+
import { unpadRandomMax16 } from './generics.js'
|
|
3
|
+
import { jidNormalizedUser } from '../WABinary/index.js'
|
|
4
|
+
import { proto } from '../../WAProto/index.js'
|
|
5
|
+
|
|
6
|
+
const BOT_MESSAGE_INFO = 'Bot Message'
|
|
7
|
+
const KEY_LENGTH = 32
|
|
8
|
+
const AUTH_TAG_LENGTH = 16
|
|
9
|
+
const MSG_ID_HEX_RE = /^[0-9A-Fa-f]{32}$/
|
|
10
|
+
|
|
11
|
+
const toBuffer = value => {
|
|
12
|
+
if (Buffer.isBuffer(value)) return value
|
|
13
|
+
if (value instanceof Uint8Array) return Buffer.from(value.buffer, value.byteOffset, value.byteLength)
|
|
14
|
+
return Buffer.from(value)
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const buildMessageIdRepresentations = messageId => {
|
|
18
|
+
const ascii = Buffer.from(messageId)
|
|
19
|
+
const binary = MSG_ID_HEX_RE.test(messageId) ? Buffer.from(messageId, 'hex') : ascii
|
|
20
|
+
return [{ label: 'msgIdAscii', value: ascii }, ...(binary.equals(ascii) ? [] : [{ label: 'msgIdBinary', value: binary }])]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const pushUnique = (items, seen, item) => {
|
|
24
|
+
const key = JSON.stringify([item.messageId, item.idSource, item.idSources, item.infoSource, item.aadSource, item.info.toString('hex'), item.aad.toString('hex')])
|
|
25
|
+
if (!seen.has(key)) { seen.add(key); items.push(item) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const getCandidateIds = messageKey => {
|
|
29
|
+
const ordered = [
|
|
30
|
+
messageKey?.botType === 'full' ? { source: 'stanzaId', messageId: messageKey?.stanzaId } : { source: 'botEditTargetId', messageId: messageKey?.botEditTargetId },
|
|
31
|
+
{ source: 'targetId', messageId: messageKey?.targetId },
|
|
32
|
+
{ source: 'metaTargetId', messageId: messageKey?.metaTargetId },
|
|
33
|
+
{ source: 'stanzaId', messageId: messageKey?.stanzaId },
|
|
34
|
+
]
|
|
35
|
+
const candidates = Array.isArray(messageKey?.targetIdCandidates) ? messageKey.targetIdCandidates : []
|
|
36
|
+
for (let i = 0; i < candidates.length; i++) ordered.push({ source: `targetIdCandidates[${i}]`, messageId: candidates[i] })
|
|
37
|
+
const grouped = new Map()
|
|
38
|
+
for (const c of ordered) {
|
|
39
|
+
if (!c.messageId) continue
|
|
40
|
+
const id = String(c.messageId)
|
|
41
|
+
const existing = grouped.get(id)
|
|
42
|
+
if (existing) { if (!existing.idSources.includes(c.source)) existing.idSources.push(c.source) }
|
|
43
|
+
else grouped.set(id, { messageId: id, idSource: c.source, idSources: [c.source] })
|
|
44
|
+
}
|
|
45
|
+
return Array.from(grouped.values())
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const getJidCandidates = messageKey => {
|
|
49
|
+
const ordered = [
|
|
50
|
+
{ source: 'meId', jid: messageKey?.meId },
|
|
51
|
+
{ source: 'conversationJid', jid: messageKey?.conversationJid },
|
|
52
|
+
{ source: 'senderJid', jid: messageKey?.senderJid },
|
|
53
|
+
{ source: 'meLidNormalized', jid: jidNormalizedUser(messageKey?.meLid) },
|
|
54
|
+
]
|
|
55
|
+
const seen = new Set()
|
|
56
|
+
const result = []
|
|
57
|
+
for (const c of ordered) {
|
|
58
|
+
if (!c.jid) continue
|
|
59
|
+
const jid = String(c.jid)
|
|
60
|
+
if (!seen.has(jid)) { seen.add(jid); result.push({ source: c.source, jid, value: Buffer.from(jid) }) }
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const buildMsmsgDecryptionStrategies = messageKey => {
|
|
66
|
+
const botJid = String(messageKey?.participant || '')
|
|
67
|
+
const botJidBuffer = Buffer.from(botJid)
|
|
68
|
+
const targetIds = getCandidateIds(messageKey)
|
|
69
|
+
const jidCandidates = getJidCandidates(messageKey)
|
|
70
|
+
const primaryJid = jidCandidates[0]
|
|
71
|
+
const alternateJid = jidCandidates.find(c => c.source !== primaryJid?.source && c.jid !== botJid)
|
|
72
|
+
const strategies = [], seen = new Set()
|
|
73
|
+
for (const idCandidate of targetIds) {
|
|
74
|
+
for (const idForm of buildMessageIdRepresentations(idCandidate.messageId)) {
|
|
75
|
+
pushUnique(strategies, seen, { mode: '2step', idSource: idCandidate.idSource, idSources: idCandidate.idSources, infoSource: `${idForm.label}+meId+botJid`, aadSource: `${idForm.label}+0+botJid`, authTagLayout: 'trailing', messageId: idCandidate.messageId, info: Buffer.concat([idForm.value, primaryJid.value, botJidBuffer, Buffer.alloc(0)]), aad: Buffer.concat([idForm.value, Buffer.from([0]), botJidBuffer]), attemptLabel: `${idCandidate.idSource}:${idForm.label}:primary` })
|
|
76
|
+
if (alternateJid) pushUnique(strategies, seen, { mode: '2step', idSource: idCandidate.idSource, idSources: idCandidate.idSources, infoSource: `${idForm.label}+${alternateJid.source}+botJid`, aadSource: `${idForm.label}+0+${alternateJid.source}`, authTagLayout: 'trailing', messageId: idCandidate.messageId, info: Buffer.concat([idForm.value, alternateJid.value, botJidBuffer, Buffer.alloc(0)]), aad: Buffer.concat([idForm.value, Buffer.from([0]), alternateJid.value]), attemptLabel: `${idCandidate.idSource}:${idForm.label}:${alternateJid.source}` })
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return strategies.slice(0, 12)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const assertRequired = (value, label) => {
|
|
83
|
+
if (!value || (Buffer.isBuffer(value) && value.length === 0) || (value instanceof Uint8Array && value.byteLength === 0)) throw new Error(`Missing required ${label} for msmsg decryption`)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const decryptWithStrategy = (messageSecret, msMsg, strategy) => {
|
|
87
|
+
const baseSecret = Buffer.from(hkdf(toBuffer(messageSecret), KEY_LENGTH, { info: BOT_MESSAGE_INFO }))
|
|
88
|
+
const key = Buffer.from(hkdf(baseSecret, KEY_LENGTH, { info: strategy.info }))
|
|
89
|
+
const payload = toBuffer(msMsg.encPayload)
|
|
90
|
+
const ciphertextWithTag = Buffer.concat([payload.slice(0, -AUTH_TAG_LENGTH), payload.slice(-AUTH_TAG_LENGTH)])
|
|
91
|
+
return Buffer.from(aesDecryptGCM(ciphertextWithTag, key, toBuffer(msMsg.encIv), strategy.aad))
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export const decodeDecryptedMsmsgMessage = decrypted => {
|
|
95
|
+
const messageBuffer = toBuffer(decrypted)
|
|
96
|
+
try {
|
|
97
|
+
const unpadded = Buffer.from(unpadRandomMax16(messageBuffer))
|
|
98
|
+
const decoded = proto.Message.decode(unpadded)
|
|
99
|
+
if (Object.keys(decoded).some(key => key !== 'messageContextInfo' && decoded[key] != null)) return decoded
|
|
100
|
+
} catch { }
|
|
101
|
+
return proto.Message.decode(messageBuffer)
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const decryptMsmsgBotMessage = async (messageSecret, messageKey, msMsg) => {
|
|
105
|
+
assertRequired(messageSecret, 'messageSecret')
|
|
106
|
+
assertRequired(messageKey?.participant, 'participant')
|
|
107
|
+
assertRequired(messageKey?.meId, 'meId')
|
|
108
|
+
assertRequired(msMsg?.encIv, 'encIv')
|
|
109
|
+
assertRequired(msMsg?.encPayload, 'encPayload')
|
|
110
|
+
if (getCandidateIds(messageKey).length === 0) throw new Error('Missing required target message id for msmsg decryption')
|
|
111
|
+
const strategies = buildMsmsgDecryptionStrategies(messageKey)
|
|
112
|
+
const attemptedStrategies = []
|
|
113
|
+
let lastError
|
|
114
|
+
for (const strategy of strategies) {
|
|
115
|
+
try { return decryptWithStrategy(messageSecret, msMsg, strategy) }
|
|
116
|
+
catch (error) { attemptedStrategies.push({ idSource: strategy.idSource, idSources: strategy.idSources, infoSource: strategy.infoSource, aadSource: strategy.aadSource, messageId: strategy.messageId }); lastError = error }
|
|
117
|
+
}
|
|
118
|
+
const error = new Error('Failed to decrypt msmsg with bounded deterministic strategies')
|
|
119
|
+
error.attemptedStrategies = attemptedStrategies
|
|
120
|
+
error.cause = lastError
|
|
121
|
+
throw error
|
|
122
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { proto } from '../../WAProto/index.js'
|
|
2
|
+
import { BOT_RENDERING_CONFIG_METADATA } from '../Defaults/index.js'
|
|
3
|
+
import { BotCapabilityType, buildBotCapabilityMetadata } from './bot-capabilities.js'
|
|
4
|
+
import { delay } from './generics.js'
|
|
5
|
+
|
|
6
|
+
export const PlanningStepStatus = { IN_PROGRESS: 0, DONE: 1, FAILED: 2 }
|
|
7
|
+
|
|
8
|
+
export const buildProgressIndicator = (description, steps = [], estimatedMs) => {
|
|
9
|
+
const stepsMetadata = steps.map(s => {
|
|
10
|
+
const m = { statusTitle: s.title, status: s.status ?? PlanningStepStatus.IN_PROGRESS }
|
|
11
|
+
if (s.body) m.statusBody = s.body
|
|
12
|
+
if (s.isReasoning) m.isReasoning = true
|
|
13
|
+
if (s.isEnhancedSearch) m.isEnhancedSearch = true
|
|
14
|
+
return m
|
|
15
|
+
})
|
|
16
|
+
const indicator = { stepsMetadata }
|
|
17
|
+
if (description) indicator.progressDescription = description
|
|
18
|
+
if (estimatedMs != null) indicator.estimatedCompletionTime = estimatedMs
|
|
19
|
+
return indicator
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const buildCompositingPlaceholder = ({ description = 'Thinking…', steps = [], estimatedMs, placeholderText = '', verificationMetadata } = {}) => {
|
|
23
|
+
const progressIndicatorMetadata = buildProgressIndicator(description, steps, estimatedMs)
|
|
24
|
+
const unifiedData = new TextEncoder().encode(JSON.stringify({
|
|
25
|
+
response_id: crypto.randomUUID(),
|
|
26
|
+
sections: placeholderText ? [{ view_model: { primitive: { text: placeholderText, inline_entities: [], __typename: 'GenAIMarkdownTextUXPrimitive' }, __typename: 'GenAISingleLayoutViewModel' } }] : []
|
|
27
|
+
}))
|
|
28
|
+
const capabilities = [BotCapabilityType.PROGRESS_INDICATOR, ...(steps.length ? [BotCapabilityType.AGENTIC_PLANNING] : [])]
|
|
29
|
+
return {
|
|
30
|
+
messageContextInfo: {
|
|
31
|
+
botMetadata: {
|
|
32
|
+
capabilityMetadata: buildBotCapabilityMetadata(capabilities),
|
|
33
|
+
pluginMetadata: {},
|
|
34
|
+
progressIndicatorMetadata,
|
|
35
|
+
botRenderingConfigMetadata: BOT_RENDERING_CONFIG_METADATA,
|
|
36
|
+
...(verificationMetadata ? { verificationMetadata } : {}),
|
|
37
|
+
}
|
|
38
|
+
},
|
|
39
|
+
botForwardedMessage: {
|
|
40
|
+
message: {
|
|
41
|
+
richResponseMessage: {
|
|
42
|
+
messageType: proto.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
|
|
43
|
+
unifiedResponse: { data: unifiedData },
|
|
44
|
+
submessages: [],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export const buildPlainPlaceholder = (description = 'Thinking…', steps = [], placeholderText = '') => {
|
|
52
|
+
const lines = steps.map(s => {
|
|
53
|
+
const icon = s.status === PlanningStepStatus.DONE ? '✓' : s.status === PlanningStepStatus.FAILED ? '✗' : '○'
|
|
54
|
+
return `${icon} ${s.title}`
|
|
55
|
+
}).join('\n')
|
|
56
|
+
let text = `_${description}_`
|
|
57
|
+
if (lines) text += `\n\n${lines}`
|
|
58
|
+
if (placeholderText) text += `\n\n${placeholderText}`
|
|
59
|
+
return { text }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const metaTyping = async (sock, jid, { description = 'Thinking…', steps = [], estimatedMs, placeholderText = '' } = {}) => {
|
|
63
|
+
await sock.sendPresenceUpdate('composing', jid)
|
|
64
|
+
const pending = steps.map(s => ({ ...s, status: PlanningStepStatus.IN_PROGRESS }))
|
|
65
|
+
return sock.sendMessage(jid, buildPlainPlaceholder(description, pending, placeholderText))
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export const sendMetaComposited = async (sock, jid, content, { thinkingMs = 2000, description = 'Thinking…', steps = [], placeholderText = '', sendOptions = {} } = {}) => {
|
|
69
|
+
const placeholder = await metaTyping(sock, jid, { description, steps, estimatedMs: thinkingMs, placeholderText })
|
|
70
|
+
try { await delay(thinkingMs); if (placeholder?.key) await sock.sendMessage(jid, { delete: placeholder.key }) } catch (_) { }
|
|
71
|
+
await sock.sendPresenceUpdate('paused', jid)
|
|
72
|
+
return sock.sendMessage(jid, content, sendOptions)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export const buildSteps = (titles, status = PlanningStepStatus.IN_PROGRESS) => titles.map(title => ({ title, status }))
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { proto } from '../../WAProto/index.js'
|
|
2
|
+
|
|
3
|
+
const requireObject = (value, name) => {
|
|
4
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) throw new TypeError(`${name} must be an object`)
|
|
5
|
+
return value
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const requireKey = (value, name = 'key') => {
|
|
9
|
+
requireObject(value, name)
|
|
10
|
+
if (!value.id) throw new TypeError(`${name}.id is required`)
|
|
11
|
+
return value
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const enumValue = (enumObject, value, fallback) => {
|
|
15
|
+
if (typeof value === 'number') return value
|
|
16
|
+
if (typeof value === 'string') { const key = value.trim().toUpperCase(); if (key in enumObject) return enumObject[key] }
|
|
17
|
+
return fallback
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export const STATUS_NOTIFICATION_TYPES = Object.freeze({ ...proto.Message.StatusNotificationMessage.StatusNotificationType, STATUS_GROUP_STATUS_REPLY: 4 })
|
|
21
|
+
|
|
22
|
+
const toSeconds = value => value instanceof Date ? Math.floor(value.getTime() / 1000) : value
|
|
23
|
+
const toMilliseconds = value => value instanceof Date ? value.getTime() : value
|
|
24
|
+
|
|
25
|
+
const makeInnerCommentMessage = content => {
|
|
26
|
+
const data = requireObject(content, 'comment.content')
|
|
27
|
+
if (data.raw && typeof data.raw === 'object') return data.raw
|
|
28
|
+
if (typeof data.text === 'string') return { extendedTextMessage: proto.Message.ExtendedTextMessage.create({ text: data.text, contextInfo: data.contextInfo }) }
|
|
29
|
+
const keys = Object.keys(data)
|
|
30
|
+
if (keys.some(key => key.endsWith('Message'))) return data
|
|
31
|
+
throw new TypeError('comment.content supports text, raw, or protobuf message fields')
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export const makeQuestionMessage = input => {
|
|
35
|
+
const data = requireObject(input, 'question')
|
|
36
|
+
if (typeof data.text !== 'string' || !data.text.length) throw new TypeError('question.text is required')
|
|
37
|
+
return { questionMessage: proto.Message.FutureProofMessage.create({ message: { extendedTextMessage: proto.Message.ExtendedTextMessage.create({ text: data.text, contextInfo: { ...(data.contextInfo || {}), isQuestion: true } }) } }) }
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export const makeQuestionResponseMessage = input => {
|
|
41
|
+
const data = requireObject(input, 'questionResponse')
|
|
42
|
+
requireKey(data.key, 'questionResponse.key')
|
|
43
|
+
return { questionResponseMessage: proto.Message.QuestionResponseMessage.create({ key: data.key, text: data.text ?? '' }) }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const makeQuestionReplyMessage = input => {
|
|
47
|
+
const data = requireObject(input, 'questionReply')
|
|
48
|
+
if (typeof data.text !== 'string' || !data.text.length) throw new TypeError('questionReply.text is required')
|
|
49
|
+
if (data.serverQuestionId === undefined || data.serverQuestionId === null) throw new TypeError('questionReply.serverQuestionId is required')
|
|
50
|
+
const quoted = { serverQuestionId: Number(data.serverQuestionId) }
|
|
51
|
+
if (data.quotedQuestion) quoted.quotedQuestion = data.quotedQuestion
|
|
52
|
+
if (data.quotedResponse) quoted.quotedResponse = data.quotedResponse
|
|
53
|
+
return { questionReplyMessage: proto.Message.FutureProofMessage.create({ message: { extendedTextMessage: proto.Message.ExtendedTextMessage.create({ text: data.text, contextInfo: { ...(data.contextInfo || {}), questionReplyQuotedMessage: quoted } }) } }) }
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export const makeStatusQuestionAnswerMessage = input => {
|
|
57
|
+
const data = requireObject(input, 'statusQuestionAnswer')
|
|
58
|
+
requireKey(data.key, 'statusQuestionAnswer.key')
|
|
59
|
+
return { statusQuestionAnswerMessage: proto.Message.StatusQuestionAnswerMessage.create({ key: data.key, text: data.text ?? '' }) }
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export const makeStatusQuotedMessage = input => {
|
|
63
|
+
const data = requireObject(input, 'statusQuoted')
|
|
64
|
+
requireKey(data.originalStatusId, 'statusQuoted.originalStatusId')
|
|
65
|
+
const type = enumValue(proto.Message.StatusQuotedMessage.StatusQuotedMessageType, data.type, proto.Message.StatusQuotedMessage.StatusQuotedMessageType.QUESTION_ANSWER)
|
|
66
|
+
return { statusQuotedMessage: proto.Message.StatusQuotedMessage.create({ type, text: data.text, thumbnail: data.thumbnail, originalStatusId: data.originalStatusId }) }
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const makeStatusStickerInteractionMessage = input => {
|
|
70
|
+
const data = requireObject(input, 'statusStickerInteraction')
|
|
71
|
+
requireKey(data.key, 'statusStickerInteraction.key')
|
|
72
|
+
if (!data.stickerKey) throw new TypeError('statusStickerInteraction.stickerKey is required')
|
|
73
|
+
const type = enumValue(proto.Message.StatusStickerInteractionMessage.StatusStickerType, data.type, proto.Message.StatusStickerInteractionMessage.StatusStickerType.REACTION)
|
|
74
|
+
return { statusStickerInteractionMessage: proto.Message.StatusStickerInteractionMessage.create({ key: data.key, stickerKey: data.stickerKey, type }) }
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export const makeStatusNotificationMessage = input => {
|
|
78
|
+
const data = requireObject(input, 'statusNotification')
|
|
79
|
+
requireKey(data.responseMessageKey, 'statusNotification.responseMessageKey')
|
|
80
|
+
requireKey(data.originalMessageKey, 'statusNotification.originalMessageKey')
|
|
81
|
+
const type = enumValue(STATUS_NOTIFICATION_TYPES, data.type, STATUS_NOTIFICATION_TYPES.UNKNOWN)
|
|
82
|
+
return { statusNotificationMessage: proto.Message.StatusNotificationMessage.create({ responseMessageKey: data.responseMessageKey, originalMessageKey: data.originalMessageKey, type }) }
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export const makeNewsletterAdminInviteMessage = input => {
|
|
86
|
+
const data = requireObject(input, 'newsletterAdminInvite')
|
|
87
|
+
if (!data.newsletterJid) throw new TypeError('newsletterAdminInvite.newsletterJid is required')
|
|
88
|
+
return { newsletterAdminInviteMessage: proto.Message.NewsletterAdminInviteMessage.create({ newsletterJid: data.newsletterJid, newsletterName: data.newsletterName, jpegThumbnail: data.jpegThumbnail, caption: data.caption, inviteExpiration: data.inviteExpiration, contextInfo: data.contextInfo }) }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export const makeNewsletterFollowerInviteMessage = input => {
|
|
92
|
+
const data = requireObject(input, 'newsletterFollowerInvite')
|
|
93
|
+
if (!data.newsletterJid) throw new TypeError('newsletterFollowerInvite.newsletterJid is required')
|
|
94
|
+
return { newsletterFollowerInviteMessageV2: proto.Message.NewsletterFollowerInviteMessage.create({ newsletterJid: data.newsletterJid, newsletterName: data.newsletterName, jpegThumbnail: data.jpegThumbnail, caption: data.caption, contextInfo: data.contextInfo }) }
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export const makePollAddOptionMessage = input => {
|
|
98
|
+
const data = requireObject(input, 'pollAddOption')
|
|
99
|
+
requireKey(data.pollCreationMessageKey, 'pollAddOption.pollCreationMessageKey')
|
|
100
|
+
const addOption = typeof data.option === 'string' ? { optionName: data.option } : data.addOption || data.option
|
|
101
|
+
if (!addOption?.optionName) throw new TypeError('pollAddOption.option is required')
|
|
102
|
+
return { pollAddOptionMessage: proto.Message.PollAddOptionMessage.create({ pollCreationMessageKey: data.pollCreationMessageKey, addOption, metadata: data.metadata }) }
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export const makeCommentMessage = input => {
|
|
106
|
+
const data = requireObject(input, 'comment')
|
|
107
|
+
requireKey(data.targetMessageKey, 'comment.targetMessageKey')
|
|
108
|
+
const message = data.message || (data.content ? makeInnerCommentMessage(data.content) : null)
|
|
109
|
+
requireObject(message, 'comment.message')
|
|
110
|
+
return { commentMessage: proto.Message.CommentMessage.create({ message, targetMessageKey: data.targetMessageKey }) }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export const makeEventInviteMessage = input => {
|
|
114
|
+
const data = requireObject(input, 'eventInvite')
|
|
115
|
+
if (!data.eventId) throw new TypeError('eventInvite.eventId is required')
|
|
116
|
+
return { eventInviteMessage: proto.Message.EventInviteMessage.create({ contextInfo: data.contextInfo, eventId: data.eventId, eventTitle: data.eventTitle, jpegThumbnail: data.jpegThumbnail, startTime: toSeconds(data.startTime), caption: data.caption, isCanceled: data.isCanceled, endTime: toSeconds(data.endTime), callLink: data.callLink }) }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const makeScheduledCallCreationMessage = input => {
|
|
120
|
+
const data = requireObject(input, 'scheduledCall')
|
|
121
|
+
if (!data.scheduledTimestampMs) throw new TypeError('scheduledCall.scheduledTimestampMs is required')
|
|
122
|
+
const callType = enumValue(proto.Message.ScheduledCallCreationMessage.CallType, data.callType, proto.Message.ScheduledCallCreationMessage.CallType.UNKNOWN)
|
|
123
|
+
return { scheduledCallCreationMessage: proto.Message.ScheduledCallCreationMessage.create({ scheduledTimestampMs: toMilliseconds(data.scheduledTimestampMs), callType, title: data.title }) }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const makeScheduledCallEditMessage = input => {
|
|
127
|
+
const data = requireObject(input, 'scheduledCallEdit')
|
|
128
|
+
requireKey(data.key, 'scheduledCallEdit.key')
|
|
129
|
+
const editType = enumValue(proto.Message.ScheduledCallEditMessage.EditType, data.editType, proto.Message.ScheduledCallEditMessage.EditType.CANCEL)
|
|
130
|
+
return { scheduledCallEditMessage: proto.Message.ScheduledCallEditMessage.create({ key: data.key, editType }) }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export const makeGroupStatusReactionMessage = input => {
|
|
134
|
+
const data = requireObject(input, 'groupStatusReaction')
|
|
135
|
+
requireKey(data.key, 'groupStatusReaction.key')
|
|
136
|
+
return { groupStatusMessageV2: proto.Message.FutureProofMessage.create({ message: { reactionMessage: proto.Message.ReactionMessage.create({ key: data.key, text: data.text ?? '', groupingKey: data.groupingKey, senderTimestampMs: data.senderTimestampMs || Date.now() }) } }) }
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const makeNewsletterStatusAttribution = input => {
|
|
140
|
+
const data = requireObject(input, 'newsletterStatus')
|
|
141
|
+
if (!data.newsletterJid) throw new TypeError('newsletterStatus.newsletterJid is required')
|
|
142
|
+
if (data.messageId === undefined || data.messageId === null) throw new TypeError('newsletterStatus.messageId is required')
|
|
143
|
+
return proto.StatusAttribution.create({ type: proto.StatusAttribution.Type.NEWSLETTER_STATUS, actionUrl: data.actionUrl, statusReshare: { source: proto.StatusAttribution.StatusReshare.Source.CHANNEL_RESHARE, metadata: { duration: data.duration, channelJid: data.newsletterJid, channelMessageId: Number(data.messageId), hasMultipleReshares: data.hasMultipleReshares ?? false } } })
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export const makeGroupStatusAttribution = input => {
|
|
147
|
+
const data = requireObject(input, 'groupStatusAttribution')
|
|
148
|
+
if (!data.authorJid) throw new TypeError('groupStatusAttribution.authorJid is required')
|
|
149
|
+
return proto.StatusAttribution.create({ type: proto.StatusAttribution.Type.GROUP_STATUS, actionUrl: data.actionUrl, groupStatus: { authorJid: data.authorJid } })
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
export const STATUS_AUDIENCE_DEFAULT_LIST_NAME = 'Close friends'
|
|
153
|
+
export const STATUS_AUDIENCE_DEFAULT_EMOJI = '⭐'
|
|
154
|
+
|
|
155
|
+
export const makeStatusAudienceMetadata = input => {
|
|
156
|
+
const data = typeof input === 'string' ? { listName: input } : requireObject(input, 'statusAudience')
|
|
157
|
+
const audienceType = enumValue(proto.ContextInfo.StatusAudienceMetadata.AudienceType, data.audienceType, proto.ContextInfo.StatusAudienceMetadata.AudienceType.CLOSE_FRIENDS)
|
|
158
|
+
const listName = data.listName === undefined ? STATUS_AUDIENCE_DEFAULT_LIST_NAME : String(data.listName)
|
|
159
|
+
const listEmoji = data.listEmoji === undefined ? STATUS_AUDIENCE_DEFAULT_EMOJI : String(data.listEmoji)
|
|
160
|
+
return proto.ContextInfo.StatusAudienceMetadata.create({ audienceType, listName, listEmoji })
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const makeMessageAssociation = input => {
|
|
164
|
+
const data = requireObject(input, 'messageAssociation')
|
|
165
|
+
const parentMessageKey = data.parentMessageKey ?? data.key
|
|
166
|
+
requireKey(parentMessageKey, 'messageAssociation.parentMessageKey')
|
|
167
|
+
const associationType = enumValue(proto.MessageAssociation.AssociationType, data.associationType ?? data.type, proto.MessageAssociation.AssociationType.UNKNOWN)
|
|
168
|
+
return proto.MessageAssociation.create({ associationType, parentMessageKey, messageIndex: data.messageIndex })
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export const makeStatusAddYoursAssociation = input => {
|
|
172
|
+
const data = input && typeof input === 'object' && !Array.isArray(input) && input.id ? { key: input } : requireObject(input, 'addYours')
|
|
173
|
+
const parentMessageKey = data.key ?? data.parentMessageKey
|
|
174
|
+
requireKey(parentMessageKey, 'addYours.key')
|
|
175
|
+
const associationType = enumValue(proto.MessageAssociation.AssociationType, data.type ?? data.associationType, proto.MessageAssociation.AssociationType.STATUS_ADD_YOURS)
|
|
176
|
+
return proto.MessageAssociation.create({ associationType, parentMessageKey, messageIndex: data.messageIndex })
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
export const makeStatusMentionMessage = input => {
|
|
180
|
+
const data = requireObject(input, 'statusMention')
|
|
181
|
+
requireKey(data.key, 'statusMention.key')
|
|
182
|
+
const field = data.group ? 'groupStatusMentionMessage' : 'statusMentionMessage'
|
|
183
|
+
return { [field]: proto.Message.FutureProofMessage.create({ message: { protocolMessage: proto.Message.ProtocolMessage.create({ key: data.key, type: proto.Message.ProtocolMessage.Type.STATUS_MENTION_MESSAGE }) } }) }
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const modernBuilders = [
|
|
187
|
+
['question', makeQuestionMessage],
|
|
188
|
+
['questionReply', makeQuestionReplyMessage],
|
|
189
|
+
['questionResponse', makeQuestionResponseMessage],
|
|
190
|
+
['statusQuestionAnswer', makeStatusQuestionAnswerMessage],
|
|
191
|
+
['statusQuoted', makeStatusQuotedMessage],
|
|
192
|
+
['statusStickerInteraction', makeStatusStickerInteractionMessage],
|
|
193
|
+
['statusNotification', makeStatusNotificationMessage],
|
|
194
|
+
['newsletterAdminInvite', makeNewsletterAdminInviteMessage],
|
|
195
|
+
['newsletterFollowerInvite', makeNewsletterFollowerInviteMessage],
|
|
196
|
+
['pollAddOption', makePollAddOptionMessage],
|
|
197
|
+
['comment', makeCommentMessage],
|
|
198
|
+
['eventInvite', makeEventInviteMessage],
|
|
199
|
+
['scheduledCall', makeScheduledCallCreationMessage],
|
|
200
|
+
['scheduledCallEdit', makeScheduledCallEditMessage],
|
|
201
|
+
['groupStatusReaction', makeGroupStatusReactionMessage],
|
|
202
|
+
['statusMention', makeStatusMentionMessage]
|
|
203
|
+
]
|
|
204
|
+
|
|
205
|
+
export const prepareModernMessageContent = content => {
|
|
206
|
+
if (!content || typeof content !== 'object' || Array.isArray(content)) return content
|
|
207
|
+
let output = { ...content }
|
|
208
|
+
const attributions = Array.isArray(output.contextInfo?.statusAttributions) ? [...output.contextInfo.statusAttributions] : []
|
|
209
|
+
if (output.newsletterStatus) { attributions.push(makeNewsletterStatusAttribution(output.newsletterStatus)); delete output.newsletterStatus }
|
|
210
|
+
if (output.groupStatusAttribution) { attributions.push(makeGroupStatusAttribution(output.groupStatusAttribution)); delete output.groupStatusAttribution }
|
|
211
|
+
if (Array.isArray(output.statusAttributions)) { attributions.push(...output.statusAttributions); delete output.statusAttributions }
|
|
212
|
+
const carried = []
|
|
213
|
+
const audience = output.statusAudience
|
|
214
|
+
if (audience !== undefined && audience !== null) {
|
|
215
|
+
carried.push('statusAudience')
|
|
216
|
+
output.contextInfo = { ...(output.contextInfo || {}), statusAudienceMetadata: makeStatusAudienceMetadata(audience) }
|
|
217
|
+
delete output.statusAudience
|
|
218
|
+
}
|
|
219
|
+
if (attributions.length) output.contextInfo = { ...(output.contextInfo || {}), statusAttributions: attributions }
|
|
220
|
+
if (output.addYours !== undefined && output.addYours !== null) {
|
|
221
|
+
carried.push('addYours')
|
|
222
|
+
output.messageAssociation = makeStatusAddYoursAssociation(output.addYours)
|
|
223
|
+
delete output.addYours
|
|
224
|
+
} else if (output.messageAssociation !== undefined && output.messageAssociation !== null) {
|
|
225
|
+
carried.push('messageAssociation')
|
|
226
|
+
output.messageAssociation = makeMessageAssociation(output.messageAssociation)
|
|
227
|
+
}
|
|
228
|
+
for (const [key, builder] of modernBuilders) {
|
|
229
|
+
if (output[key] !== undefined && output[key] !== null) {
|
|
230
|
+
if (carried.length) throw new TypeError(`${carried.join(' and ')} cannot be combined with ${key}: ${key} builds its own message and keeps no room for them. Build it with the low-level makers and generateWAMessageFromContent instead.`)
|
|
231
|
+
return { raw: true, ...builder(output[key]) }
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return output
|
|
235
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const parseJsonObject = v => {
|
|
2
|
+
if (!v) return {}
|
|
3
|
+
if (typeof v === 'object') return v
|
|
4
|
+
try { const p = JSON.parse(v); return p && typeof p === 'object' ? p : {} } catch { return {} }
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const parseNativeFlowResponse = message => {
|
|
8
|
+
const content = message?.message || message
|
|
9
|
+
const response = content?.interactiveResponseMessage?.nativeFlowResponseMessage || content?.nativeFlowResponseMessage
|
|
10
|
+
if (!response) return null
|
|
11
|
+
const params = parseJsonObject(response.paramsJson)
|
|
12
|
+
const actionPayload = params.flow_action_payload || params.flowActionPayload || {}
|
|
13
|
+
const data = params.data || params.response || params.form_data || params.formData || actionPayload.data || {}
|
|
14
|
+
return {
|
|
15
|
+
name: response.name || '',
|
|
16
|
+
version: response.version,
|
|
17
|
+
paramsJson: response.paramsJson || '',
|
|
18
|
+
params,
|
|
19
|
+
isFlow: response.name === 'galaxy_message' || response.name === 'flow' || !!(params.flow_id || params.flow_token || params.flow_message_version),
|
|
20
|
+
flowId: params.flow_id || params.flowId,
|
|
21
|
+
flowToken: params.flow_token || params.flowToken,
|
|
22
|
+
screen: params.screen || actionPayload.screen,
|
|
23
|
+
action: params.flow_action || params.flowAction || actionPayload.action,
|
|
24
|
+
actionPayload,
|
|
25
|
+
data,
|
|
26
|
+
buttonId: params.id || params.button_id || params.buttonId,
|
|
27
|
+
displayText: params.display_text || params.displayText,
|
|
28
|
+
raw: response,
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export const parseWhatsAppFlowResponse = message => {
|
|
33
|
+
const response = parseNativeFlowResponse(message)
|
|
34
|
+
return response?.isFlow ? response : null
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const buildWhatsAppFlowButton = ({ flowId, flowToken, cta = 'Open form', action = 'navigate', actionPayload, mode = 'published', flowMessageVersion = '3', metadata, name = 'galaxy_message' } = {}) => {
|
|
38
|
+
if (!flowId) throw new TypeError('flowId is required')
|
|
39
|
+
if (!flowToken) throw new TypeError('flowToken is required')
|
|
40
|
+
return {
|
|
41
|
+
name,
|
|
42
|
+
flow_message_version: String(flowMessageVersion),
|
|
43
|
+
flow_token: String(flowToken),
|
|
44
|
+
flow_id: String(flowId),
|
|
45
|
+
flow_cta: String(cta),
|
|
46
|
+
flow_action: action,
|
|
47
|
+
...(actionPayload ? { flow_action_payload: actionPayload } : {}),
|
|
48
|
+
...(metadata ? { flow_metadata: metadata } : {}),
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const makeWhatsAppFlowButton = flow => ({
|
|
53
|
+
name: flow?.nativeFlowName || 'galaxy_message',
|
|
54
|
+
buttonParamsJson: JSON.stringify(buildWhatsAppFlowButton(flow)),
|
|
55
|
+
})
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import { Boom } from '@hapi/boom'
|
|
2
|
+
import { proto } from '../../WAProto/index.js'
|
|
3
|
+
import { NOISE_MODE, WA_CERT_DETAILS } from '../Defaults/index.js'
|
|
4
|
+
import { decodeBinaryNode } from '../WABinary/index.js'
|
|
5
|
+
import { aesDecryptGCM, aesEncryptGCM, Curve, hkdf, sha256 } from './crypto.js'
|
|
6
|
+
|
|
7
|
+
const IV_LENGTH = 12
|
|
8
|
+
const EMPTY_BUFFER = Buffer.alloc(0)
|
|
9
|
+
|
|
10
|
+
class TransportState {
|
|
11
|
+
constructor(encKey, decKey) {
|
|
12
|
+
this.encKey = encKey
|
|
13
|
+
this.decKey = decKey
|
|
14
|
+
this.readCounter = 0
|
|
15
|
+
this.writeCounter = 0
|
|
16
|
+
this.iv = new Uint8Array(IV_LENGTH)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#writeIV(c) {
|
|
20
|
+
this.iv[8] = (c >>> 24) & 0xff
|
|
21
|
+
this.iv[9] = (c >>> 16) & 0xff
|
|
22
|
+
this.iv[10] = (c >>> 8) & 0xff
|
|
23
|
+
this.iv[11] = c & 0xff
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
encrypt(plaintext) { this.#writeIV(this.writeCounter++); return aesEncryptGCM(plaintext, this.encKey, this.iv, EMPTY_BUFFER) }
|
|
27
|
+
decrypt(ciphertext) { this.#writeIV(this.readCounter++); return aesDecryptGCM(ciphertext, this.decKey, this.iv, EMPTY_BUFFER) }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const buildIntroHeader = (NOISE_HEADER, routingInfo) => {
|
|
31
|
+
if (!routingInfo) return Buffer.from(NOISE_HEADER)
|
|
32
|
+
const buf = Buffer.alloc(7 + routingInfo.byteLength + NOISE_HEADER.length)
|
|
33
|
+
buf.write('ED', 0, 'utf8')
|
|
34
|
+
buf.writeUint8(0, 2)
|
|
35
|
+
buf.writeUint8(1, 3)
|
|
36
|
+
buf.writeUint8(routingInfo.byteLength >> 16, 4)
|
|
37
|
+
buf.writeUint16BE(routingInfo.byteLength & 65535, 5)
|
|
38
|
+
buf.set(routingInfo, 7)
|
|
39
|
+
buf.set(NOISE_HEADER, 7 + routingInfo.byteLength)
|
|
40
|
+
return buf
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export const makeNoiseHandler = ({ keyPair: { private: privateKey, public: publicKey }, NOISE_HEADER, logger, routingInfo }) => {
|
|
44
|
+
logger = logger.child({ class: 'ns' })
|
|
45
|
+
|
|
46
|
+
const data = Buffer.from(NOISE_MODE)
|
|
47
|
+
let hash = data.byteLength === 32 ? data : sha256(data)
|
|
48
|
+
let salt = hash
|
|
49
|
+
let encKey = hash
|
|
50
|
+
let decKey = hash
|
|
51
|
+
let counter = 0
|
|
52
|
+
let sentIntro = false
|
|
53
|
+
|
|
54
|
+
let inBytes = Buffer.alloc(0)
|
|
55
|
+
let transport = null
|
|
56
|
+
let isWaitingForTransport = false
|
|
57
|
+
let pendingOnFrame = null
|
|
58
|
+
|
|
59
|
+
const introHeader = buildIntroHeader(NOISE_HEADER, routingInfo)
|
|
60
|
+
|
|
61
|
+
const generateIV = c => {
|
|
62
|
+
const iv = new ArrayBuffer(IV_LENGTH)
|
|
63
|
+
new DataView(iv).setUint32(8, c)
|
|
64
|
+
return new Uint8Array(iv)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const authenticate = d => { if (!transport) hash = sha256(Buffer.concat([hash, d])) }
|
|
68
|
+
|
|
69
|
+
const localHKDF = d => {
|
|
70
|
+
const key = hkdf(Buffer.from(d), 64, { salt, info: '' })
|
|
71
|
+
return [key.subarray(0, 32), key.subarray(32)]
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const mixIntoKey = d => {
|
|
75
|
+
const [write, read] = localHKDF(d)
|
|
76
|
+
salt = write; encKey = read; decKey = read; counter = 0
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const encrypt = plaintext => {
|
|
80
|
+
if (transport) return transport.encrypt(plaintext)
|
|
81
|
+
const result = aesEncryptGCM(plaintext, encKey, generateIV(counter++), hash)
|
|
82
|
+
authenticate(result)
|
|
83
|
+
return result
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const decrypt = ciphertext => {
|
|
87
|
+
if (transport) return transport.decrypt(ciphertext)
|
|
88
|
+
const result = aesDecryptGCM(ciphertext, decKey, generateIV(counter++), hash)
|
|
89
|
+
authenticate(ciphertext)
|
|
90
|
+
return result
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const processData = async onFrame => {
|
|
94
|
+
while (inBytes.length >= 3) {
|
|
95
|
+
const size = (inBytes[0] << 16) | (inBytes[1] << 8) | inBytes[2]
|
|
96
|
+
if (inBytes.length < size + 3) return
|
|
97
|
+
let frame = inBytes.subarray(3, size + 3)
|
|
98
|
+
inBytes = inBytes.subarray(size + 3)
|
|
99
|
+
if (transport) frame = await decodeBinaryNode(transport.decrypt(frame))
|
|
100
|
+
onFrame(frame)
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const finishInit = async () => {
|
|
105
|
+
isWaitingForTransport = true
|
|
106
|
+
const [write, read] = localHKDF(new Uint8Array(0))
|
|
107
|
+
transport = new TransportState(write, read)
|
|
108
|
+
isWaitingForTransport = false
|
|
109
|
+
if (pendingOnFrame) { await processData(pendingOnFrame); pendingOnFrame = null }
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
authenticate(NOISE_HEADER)
|
|
113
|
+
authenticate(publicKey)
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
encrypt,
|
|
117
|
+
decrypt,
|
|
118
|
+
authenticate,
|
|
119
|
+
mixIntoKey,
|
|
120
|
+
finishInit,
|
|
121
|
+
processHandshake: ({ serverHello }, noiseKey) => {
|
|
122
|
+
authenticate(serverHello.ephemeral)
|
|
123
|
+
mixIntoKey(Curve.sharedKey(privateKey, serverHello.ephemeral))
|
|
124
|
+
|
|
125
|
+
const decStaticContent = decrypt(serverHello.static)
|
|
126
|
+
mixIntoKey(Curve.sharedKey(privateKey, decStaticContent))
|
|
127
|
+
|
|
128
|
+
const { intermediate: certIntermediate, leaf } = proto.CertChain.decode(decrypt(serverHello.payload))
|
|
129
|
+
|
|
130
|
+
if (!leaf?.details || !leaf?.signature) throw new Boom('invalid noise leaf certificate', { statusCode: 400 })
|
|
131
|
+
if (!certIntermediate?.details || !certIntermediate?.signature) throw new Boom('invalid noise intermediate certificate', { statusCode: 400 })
|
|
132
|
+
|
|
133
|
+
const details = proto.CertChain.NoiseCertificate.Details.decode(certIntermediate.details)
|
|
134
|
+
|
|
135
|
+
if (!Curve.verify(details.key, leaf.details, leaf.signature)) throw new Boom('noise certificate signature invalid', { statusCode: 400 })
|
|
136
|
+
if (!Curve.verify(WA_CERT_DETAILS.PUBLIC_KEY, certIntermediate.details, certIntermediate.signature)) throw new Boom('noise intermediate certificate signature invalid', { statusCode: 400 })
|
|
137
|
+
if (details.issuerSerial !== WA_CERT_DETAILS.SERIAL) throw new Boom('certification match failed', { statusCode: 400 })
|
|
138
|
+
|
|
139
|
+
const keyEnc = encrypt(noiseKey.public)
|
|
140
|
+
mixIntoKey(Curve.sharedKey(noiseKey.private, serverHello.ephemeral))
|
|
141
|
+
return keyEnc
|
|
142
|
+
},
|
|
143
|
+
encodeFrame: data => {
|
|
144
|
+
if (transport) data = transport.encrypt(data)
|
|
145
|
+
const dataLen = data.byteLength
|
|
146
|
+
const introSize = sentIntro ? 0 : introHeader.length
|
|
147
|
+
const frame = Buffer.allocUnsafe(introSize + 3 + dataLen)
|
|
148
|
+
if (!sentIntro) { frame.set(introHeader); sentIntro = true }
|
|
149
|
+
frame[introSize] = (dataLen >>> 16) & 0xff
|
|
150
|
+
frame[introSize + 1] = (dataLen >>> 8) & 0xff
|
|
151
|
+
frame[introSize + 2] = dataLen & 0xff
|
|
152
|
+
frame.set(data, introSize + 3)
|
|
153
|
+
return frame
|
|
154
|
+
},
|
|
155
|
+
decodeFrame: async (newData, onFrame) => {
|
|
156
|
+
if (isWaitingForTransport) { inBytes = Buffer.concat([inBytes, newData]); pendingOnFrame = onFrame; return }
|
|
157
|
+
inBytes = inBytes.length === 0 ? Buffer.from(newData) : Buffer.concat([inBytes, newData])
|
|
158
|
+
await processData(onFrame)
|
|
159
|
+
},
|
|
160
|
+
}
|
|
161
|
+
}
|