whalibmob 5.5.56 → 5.5.59
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/README.md +526 -58
- package/cli.js +43 -20
- package/index.js +36 -6
- package/lib/Attestation.js +212 -0
- package/lib/Client.js +842 -330
- package/lib/DeviceManager.js +700 -182
- package/lib/HistorySyncHandler.js +759 -0
- package/lib/MediaService.js +62 -45
- package/lib/Registration.js +202 -184
- package/lib/Store.js +76 -19
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +55 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +76 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +29 -0
- package/lib/WAUSync/Protocols/USyncLIDProtocol.js +34 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +40 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +27 -0
- package/lib/WAUSync/Protocols/index.js +17 -0
- package/lib/WAUSync/USyncQuery.js +113 -0
- package/lib/WAUSync/USyncUser.js +40 -0
- package/lib/WAUSync/index.js +11 -0
- package/lib/auth-utils.js +693 -0
- package/lib/logger.js +36 -0
- package/lib/messages/MessageSender.js +386 -113
- package/lib/messages/TcTokenStore.js +116 -0
- package/lib/noise.js +43 -8
- package/lib/proto/MessageProto.js +28 -2
- package/lib/signal/SenderKey.js +12 -0
- package/lib/signal/SignalProtocol.js +70 -46
- package/lib/signal/SignalStore.js +20 -2
- package/lib/signal/WaSignalGroup/ciphertext-message.js +15 -0
- package/lib/signal/WaSignalGroup/group-session-builder.js +45 -0
- package/lib/signal/WaSignalGroup/group_cipher.js +92 -0
- package/lib/signal/WaSignalGroup/index.js +27 -0
- package/lib/signal/WaSignalGroup/keyhelper.js +24 -0
- package/lib/signal/WaSignalGroup/sender-chain-key.js +35 -0
- package/lib/signal/WaSignalGroup/sender-key-distribution-message.js +136 -0
- package/lib/signal/WaSignalGroup/sender-key-message.js +139 -0
- package/lib/signal/WaSignalGroup/sender-key-name.js +59 -0
- package/lib/signal/WaSignalGroup/sender-key-record.js +63 -0
- package/lib/signal/WaSignalGroup/sender-key-state.js +101 -0
- package/lib/signal/WaSignalGroup/sender-message-key.js +34 -0
- package/lib/signal/libsignal/curve.js +3 -1
- package/lib/signal/libsignal/queue_job.js +3 -1
- package/lib/signal/libsignal/session_builder.js +3 -1
- package/lib/signal/libsignal/session_cipher.js +3 -1
- package/lib/signal/libsignal/session_record.js +9 -9
- package/package.json +5 -8
- package/.env.example +0 -62
- package/lib/AttestationClient.js +0 -259
package/cli.js
CHANGED
|
@@ -35,7 +35,9 @@ const {
|
|
|
35
35
|
loadStore
|
|
36
36
|
} = require('./lib/Client');
|
|
37
37
|
|
|
38
|
-
const
|
|
38
|
+
const { assertMeId, initAuthCreds } = require('./lib/auth-utils');
|
|
39
|
+
|
|
40
|
+
const VERSION = '5.5.59';
|
|
39
41
|
|
|
40
42
|
// ─── output helpers ───────────────────────────────────────────────────────────
|
|
41
43
|
|
|
@@ -194,9 +196,10 @@ const HELP = `
|
|
|
194
196
|
/biz <phone|jid> query business profile
|
|
195
197
|
|
|
196
198
|
Registration
|
|
197
|
-
/reg check <phone>
|
|
198
|
-
/reg code <phone> [sms|voice|wa_old]
|
|
199
|
-
/reg
|
|
199
|
+
/reg check <phone> check if number has WhatsApp
|
|
200
|
+
/reg code <phone> [sms|voice|wa_old] request verification code
|
|
201
|
+
/reg code <phone> email <address> request code via email
|
|
202
|
+
/reg confirm <phone> <code> complete registration
|
|
200
203
|
|
|
201
204
|
Connection
|
|
202
205
|
/connect <phone> connect to WhatsApp
|
|
@@ -435,14 +438,18 @@ async function handleLine(line) {
|
|
|
435
438
|
if (_client) { try { _client.disconnect(); } catch (_) {} }
|
|
436
439
|
process.exit(0);
|
|
437
440
|
|
|
438
|
-
case '/session':
|
|
441
|
+
case '/session': {
|
|
439
442
|
if (!_client || !_client.store) { fail('not connected'); break; }
|
|
443
|
+
let _meJid = '—';
|
|
444
|
+
try { _meJid = assertMeId(_client.store); } catch (_) {}
|
|
440
445
|
hr();
|
|
441
446
|
kv('phone', _client.store.phoneNumber);
|
|
447
|
+
kv('jid', _meJid);
|
|
442
448
|
kv('name', _client.store.pushName || _client.store.name || '—');
|
|
443
449
|
kv('session', _sessDir);
|
|
444
450
|
hr();
|
|
445
451
|
break;
|
|
452
|
+
}
|
|
446
453
|
|
|
447
454
|
case '/connect': {
|
|
448
455
|
const ph = p[1];
|
|
@@ -1147,13 +1154,19 @@ async function handleLine(line) {
|
|
|
1147
1154
|
}
|
|
1148
1155
|
else if (sub === 'code') {
|
|
1149
1156
|
const ph = normalizePhone(p[2]);
|
|
1150
|
-
const method = p[3] || 'sms';
|
|
1151
|
-
|
|
1157
|
+
const method = (p[3] || 'sms').toLowerCase();
|
|
1158
|
+
// email method: /reg code <phone> email <address>
|
|
1159
|
+
const emailAddr = method === 'email' ? (p[4] || '') : '';
|
|
1160
|
+
if (!ph) { fail('usage: /reg code <phone> [sms|voice|wa_old|email <address>]'); break; }
|
|
1161
|
+
if (method === 'email' && !emailAddr) {
|
|
1162
|
+
fail('email method requires an address — usage: /reg code <phone> email <address>');
|
|
1163
|
+
break;
|
|
1164
|
+
}
|
|
1152
1165
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
1153
1166
|
const sessFile = path.join(_sessDir, `${ph}.json`);
|
|
1154
1167
|
let store = loadStore(sessFile);
|
|
1155
1168
|
if (!store) {
|
|
1156
|
-
store =
|
|
1169
|
+
store = initAuthCreds(ph);
|
|
1157
1170
|
saveStore(store, sessFile);
|
|
1158
1171
|
} else if (!store.codePending && !store.registered) {
|
|
1159
1172
|
// Only check /exist when keys were never used to request a code.
|
|
@@ -1162,13 +1175,15 @@ async function handleLine(line) {
|
|
|
1162
1175
|
const fresh = await assertRegistrationKeys(store, waVersion);
|
|
1163
1176
|
if (!fresh) {
|
|
1164
1177
|
out(' device keys already registered — generating new keys...');
|
|
1165
|
-
store =
|
|
1178
|
+
store = initAuthCreds(ph);
|
|
1166
1179
|
saveStore(store, sessFile);
|
|
1167
1180
|
out(' new keys saved — proceed with code below');
|
|
1168
1181
|
}
|
|
1169
1182
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1183
|
+
const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
|
|
1184
|
+
out('requesting ' + methodLabel + ' code for +' + ph + '...');
|
|
1185
|
+
const codeOpts = method === 'email' ? { email: emailAddr } : {};
|
|
1186
|
+
const r = await requestSmsCode(store, method, codeOpts);
|
|
1172
1187
|
store.codePending = true;
|
|
1173
1188
|
saveStore(store, sessFile);
|
|
1174
1189
|
out(' status ' + (r && r.status));
|
|
@@ -1180,7 +1195,7 @@ async function handleLine(line) {
|
|
|
1180
1195
|
const code = p[3];
|
|
1181
1196
|
if (!ph || !code) { fail('usage: /reg confirm <phone> <code>'); break; }
|
|
1182
1197
|
const file = path.join(_sessDir, `${ph}.json`);
|
|
1183
|
-
const store = loadStore(file) ||
|
|
1198
|
+
const store = loadStore(file) || initAuthCreds(ph);
|
|
1184
1199
|
out('verifying...');
|
|
1185
1200
|
const r = await verifyCode(store, code);
|
|
1186
1201
|
if (r && (r.status === 'ok' || r.status === 'sent' || r.status === 'verified')) {
|
|
@@ -1421,7 +1436,8 @@ usage:
|
|
|
1421
1436
|
|
|
1422
1437
|
options:
|
|
1423
1438
|
--session <dir> session directory (default: ~/.waSession)
|
|
1424
|
-
--method sms | voice | wa_old (default: sms)
|
|
1439
|
+
--method sms | voice | wa_old | email (default: sms)
|
|
1440
|
+
--email <address> email address (required when --method email)
|
|
1425
1441
|
|
|
1426
1442
|
after connecting, type /help for all available commands.
|
|
1427
1443
|
`.trim();
|
|
@@ -1469,15 +1485,20 @@ async function main() {
|
|
|
1469
1485
|
}
|
|
1470
1486
|
|
|
1471
1487
|
if (flags['request-code'] !== undefined) {
|
|
1472
|
-
const ph
|
|
1473
|
-
const method
|
|
1488
|
+
const ph = phone || normalizePhone(pos[0] || '');
|
|
1489
|
+
const method = (flags.method || 'sms').toLowerCase();
|
|
1490
|
+
const emailAddr = flags.email || '';
|
|
1474
1491
|
if (!ph) { fail('phone number required'); process.exit(1); }
|
|
1492
|
+
if (method === 'email' && !emailAddr) {
|
|
1493
|
+
fail('--method email requires --email <address>');
|
|
1494
|
+
process.exit(1);
|
|
1495
|
+
}
|
|
1475
1496
|
if (!fs.existsSync(_sessDir)) fs.mkdirSync(_sessDir, { recursive: true });
|
|
1476
1497
|
const sessFile = path.join(_sessDir, `${ph}.json`);
|
|
1477
1498
|
let store = loadStore(sessFile);
|
|
1478
1499
|
if (!store) {
|
|
1479
1500
|
// Brand new — generate fresh keys, save immediately, no need to check /exist
|
|
1480
|
-
store =
|
|
1501
|
+
store = initAuthCreds(ph);
|
|
1481
1502
|
saveStore(store, sessFile);
|
|
1482
1503
|
} else if (!store.codePending && !store.registered) {
|
|
1483
1504
|
// Existing store but code was never sent and not registered — check if
|
|
@@ -1488,15 +1509,17 @@ async function main() {
|
|
|
1488
1509
|
const fresh = await assertRegistrationKeys(store, waVersion);
|
|
1489
1510
|
if (!fresh) {
|
|
1490
1511
|
out(' device keys already registered — generating new keys...');
|
|
1491
|
-
store =
|
|
1512
|
+
store = initAuthCreds(ph);
|
|
1492
1513
|
saveStore(store, sessFile);
|
|
1493
1514
|
}
|
|
1494
1515
|
}
|
|
1495
1516
|
// If store.codePending === true, keys were already accepted by WhatsApp in a
|
|
1496
1517
|
// prior /code request — reuse the exact same store without any /exist call.
|
|
1497
|
-
|
|
1518
|
+
const methodLabel = method === 'email' ? ('email → ' + emailAddr) : method;
|
|
1519
|
+
out('requesting ' + methodLabel + ' code for +' + ph + '...');
|
|
1498
1520
|
try {
|
|
1499
|
-
const
|
|
1521
|
+
const codeOpts = method === 'email' ? { email: emailAddr } : {};
|
|
1522
|
+
const r = await requestSmsCode(store, method, codeOpts);
|
|
1500
1523
|
store.codePending = true;
|
|
1501
1524
|
saveStore(store, sessFile);
|
|
1502
1525
|
out(' status ' + (r && r.status));
|
|
@@ -1518,7 +1541,7 @@ async function main() {
|
|
|
1518
1541
|
if (!ph) { fail('phone number required'); process.exit(1); }
|
|
1519
1542
|
if (!code) { fail('--code is required'); process.exit(1); }
|
|
1520
1543
|
const file = path.join(_sessDir, `${ph}.json`);
|
|
1521
|
-
const store = loadStore(file) ||
|
|
1544
|
+
const store = loadStore(file) || initAuthCreds(ph);
|
|
1522
1545
|
out('verifying code for +' + ph + '...');
|
|
1523
1546
|
try {
|
|
1524
1547
|
const r = await verifyCode(store, code);
|
package/index.js
CHANGED
|
@@ -11,7 +11,25 @@ const { SenderKeyStore, SenderKeyCrypto } = require('./lib/signal/SenderKey');
|
|
|
11
11
|
const { DeviceManager } = require('./lib/DeviceManager');
|
|
12
12
|
const { encryptMedia, decryptMedia, uploadMedia, downloadMedia } = require('./lib/MediaService');
|
|
13
13
|
const { MessageSender, makeJid, generateMessageId } = require('./lib/messages/MessageSender');
|
|
14
|
-
const {
|
|
14
|
+
const {
|
|
15
|
+
makeCacheableSignalKeyStore,
|
|
16
|
+
addTransactionCapability,
|
|
17
|
+
assertMeId,
|
|
18
|
+
initAuthCreds
|
|
19
|
+
} = require('./lib/auth-utils');
|
|
20
|
+
const {
|
|
21
|
+
processHistorySyncNotification,
|
|
22
|
+
decodeHistorySyncNotification,
|
|
23
|
+
decodeHistorySync,
|
|
24
|
+
decodeAppStateSyncKeyShare,
|
|
25
|
+
saveAppStateSyncKeys,
|
|
26
|
+
loadAppStateSyncKeys,
|
|
27
|
+
mergeHistoryToStore,
|
|
28
|
+
decryptHistoryBlob,
|
|
29
|
+
deriveHistoryKeys,
|
|
30
|
+
HistorySyncType,
|
|
31
|
+
HistorySyncTypeName
|
|
32
|
+
} = require('./lib/HistorySyncHandler');
|
|
15
33
|
|
|
16
34
|
module.exports = {
|
|
17
35
|
WhalibmobClient,
|
|
@@ -50,9 +68,21 @@ module.exports = {
|
|
|
50
68
|
MessageSender,
|
|
51
69
|
makeJid,
|
|
52
70
|
generateMessageId,
|
|
53
|
-
//
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
// Auth utilities (mirrors Baileys' auth-utils)
|
|
72
|
+
makeCacheableSignalKeyStore,
|
|
73
|
+
addTransactionCapability,
|
|
74
|
+
assertMeId,
|
|
75
|
+
initAuthCreds,
|
|
76
|
+
// History sync
|
|
77
|
+
processHistorySyncNotification,
|
|
78
|
+
decodeHistorySyncNotification,
|
|
79
|
+
decodeHistorySync,
|
|
80
|
+
decodeAppStateSyncKeyShare,
|
|
81
|
+
saveAppStateSyncKeys,
|
|
82
|
+
loadAppStateSyncKeys,
|
|
83
|
+
mergeHistoryToStore,
|
|
84
|
+
decryptHistoryBlob,
|
|
85
|
+
deriveHistoryKeys,
|
|
86
|
+
HistorySyncType,
|
|
87
|
+
HistorySyncTypeName
|
|
58
88
|
};
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─── Device attestation client ────────────────────────────────────────────────
|
|
4
|
+
//
|
|
5
|
+
// Talks to the on-device Frida attestation server shipped in ./frida (Android
|
|
6
|
+
// Play Integrity / Keystore attestation, iOS App Attest) and folds its output
|
|
7
|
+
// into the registration request, mirroring how Auties00/cobalt consumes the
|
|
8
|
+
// same scripts.
|
|
9
|
+
//
|
|
10
|
+
// Design (matches Cobalt's LinkedWhatsAppClientDeviceAttestor):
|
|
11
|
+
// • Two data shapes are produced —
|
|
12
|
+
// Android: { gpia, gg, gi, gp, ge, ga } (Play Integrity legs)
|
|
13
|
+
// iOS: { attestation, assertion, keyId } (App Attest)
|
|
14
|
+
// • A NONE fallback returns empty strings for every field. The WhatsApp
|
|
15
|
+
// registration server tolerates empty attestation as a low-trust signal,
|
|
16
|
+
// so registration still works with no rooted/jailbroken device attached.
|
|
17
|
+
//
|
|
18
|
+
// Activation: set WA_FRIDA_HOST (e.g. 127.0.0.1) — and optionally WA_FRIDA_PORT
|
|
19
|
+
// (default 1119 for WhatsApp consumer, 1120 for Business) — to make the client
|
|
20
|
+
// query the local Frida server. When WA_FRIDA_HOST is unset every method
|
|
21
|
+
// returns the empty NONE fallback and performs no network I/O.
|
|
22
|
+
|
|
23
|
+
const crypto = require('crypto');
|
|
24
|
+
const http = require('http');
|
|
25
|
+
|
|
26
|
+
const DEFAULT_PORT = 1119; // WhatsApp consumer; Business build listens on 1120
|
|
27
|
+
const REQUEST_TIMEMS = 15000;
|
|
28
|
+
|
|
29
|
+
// ─── Empty (NONE) fallbacks ───────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
const EMPTY_ANDROID = { gpia: '', gg: '', gi: '', gp: '', ge: '', ga: '' };
|
|
32
|
+
const EMPTY_IOS = { attestation: '', assertion: '', keyId: '' };
|
|
33
|
+
const EMPTY_BODY = { bodyAttestation: null, authorizationHeader: null };
|
|
34
|
+
|
|
35
|
+
function fridaHost() {
|
|
36
|
+
return process.env.WA_FRIDA_HOST || null;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function fridaPort() {
|
|
40
|
+
return parseInt(process.env.WA_FRIDA_PORT, 10) || DEFAULT_PORT;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isEnabled() {
|
|
44
|
+
return !!fridaHost();
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ─── base64 / hex helpers ─────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
function toBase64Url(buf) {
|
|
50
|
+
return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function sha256(buf) {
|
|
54
|
+
return crypto.createHash('sha256').update(buf).digest();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function toHex(buf) {
|
|
58
|
+
return Buffer.from(buf).toString('hex');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Local Frida HTTP GET (best-effort; never throws) ─────────────────────────
|
|
62
|
+
// Returns the parsed JSON object on success, or null on any failure/timeout so
|
|
63
|
+
// the caller can fall back to the empty NONE attestation.
|
|
64
|
+
|
|
65
|
+
function fridaGet(pathname, query) {
|
|
66
|
+
return new Promise((resolve) => {
|
|
67
|
+
const host = fridaHost();
|
|
68
|
+
if (!host) return resolve(null);
|
|
69
|
+
|
|
70
|
+
const qs = query
|
|
71
|
+
? '?' + Object.keys(query)
|
|
72
|
+
.filter(k => query[k] != null)
|
|
73
|
+
.map(k => k + '=' + encodeURIComponent(query[k]))
|
|
74
|
+
.join('&')
|
|
75
|
+
: '';
|
|
76
|
+
|
|
77
|
+
const req = http.request(
|
|
78
|
+
{ host, port: fridaPort(), path: pathname + qs, method: 'GET', timeout: REQUEST_TIMEMS },
|
|
79
|
+
(res) => {
|
|
80
|
+
const chunks = [];
|
|
81
|
+
res.on('data', d => chunks.push(d));
|
|
82
|
+
res.on('end', () => {
|
|
83
|
+
try { resolve(JSON.parse(Buffer.concat(chunks).toString('utf8'))); }
|
|
84
|
+
catch (_) { resolve(null); }
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
req.on('error', () => resolve(null));
|
|
89
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
90
|
+
req.end();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ─── Android: Play Integrity legs (gpia + _gg _gi _gp _ge _ga) ────────────────
|
|
95
|
+
//
|
|
96
|
+
// Queries /integrity?authKey=<nonce>. The Frida server returns { token }, which
|
|
97
|
+
// becomes the Play Integrity verdict token (gpia). The _gg/_gi/_gp/_ge/_ga GMS
|
|
98
|
+
// legs are only produced by the full native attestor, so a Frida-only setup
|
|
99
|
+
// leaves them empty — the server accepts a gpia-only attestation.
|
|
100
|
+
//
|
|
101
|
+
// nonceB64 is the URL-safe base64 request nonce (see attestationFieldsAndroid).
|
|
102
|
+
async function androidIntegrity(nonceB64) {
|
|
103
|
+
if (!isEnabled()) return { ...EMPTY_ANDROID };
|
|
104
|
+
const res = await fridaGet('/integrity', { authKey: nonceB64 });
|
|
105
|
+
if (!res || !res.token) return { ...EMPTY_ANDROID };
|
|
106
|
+
return { gpia: res.token, gg: '', gi: '', gp: '', ge: '', ga: '' };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ─── Android: Keystore attestation over the ENC body (&H= + Authorization) ────
|
|
110
|
+
//
|
|
111
|
+
// Queries /cert?authKey=<authKeyB64>&enc=<encB64>. Both parameters are decoded
|
|
112
|
+
// with the STANDARD base64 decoder on the device, so they must not be URL-safe:
|
|
113
|
+
// authKey — the 32-byte raw auth key (the Noise public key, no 0x05 prefix);
|
|
114
|
+
// the server embeds it in the Keystore attestation challenge and
|
|
115
|
+
// locates it in the certificate by its 64-hex-char representation.
|
|
116
|
+
// enc — the UTF-8 bytes of the ENC payload string, base64-encoded; the
|
|
117
|
+
// server signs exactly those bytes.
|
|
118
|
+
// The server returns { signature, certificate }: signature → the &H= body
|
|
119
|
+
// suffix, certificate → the Authorization cert-chain header.
|
|
120
|
+
async function androidAttestBody(encB64, authKeyB64) {
|
|
121
|
+
if (!isEnabled()) return { ...EMPTY_BODY };
|
|
122
|
+
const encParam = Buffer.from(encB64, 'utf8').toString('base64');
|
|
123
|
+
const res = await fridaGet('/cert', { authKey: authKeyB64, enc: encParam });
|
|
124
|
+
if (!res || !res.signature || !res.certificate) return { ...EMPTY_BODY };
|
|
125
|
+
return { bodyAttestation: res.signature, authorizationHeader: res.certificate };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── Android: device /info (apk hashes / signature / secret key) ──────────────
|
|
129
|
+
async function androidInfo() {
|
|
130
|
+
if (!isEnabled()) return null;
|
|
131
|
+
return fridaGet('/info');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ─── iOS: App Attest (attestation + assertion + keyId) ────────────────────────
|
|
135
|
+
//
|
|
136
|
+
// Queries /integrity?authKey=<clientDataHashB64>. The Frida server returns
|
|
137
|
+
// { attestation, assertion }; the keyId is generated per call on-device and
|
|
138
|
+
// echoed by the server when available. clientDataHash is the base64 of the
|
|
139
|
+
// Noise public key (the server SHA-256-hashes it internally).
|
|
140
|
+
async function iosAppAttest(noisePubB64) {
|
|
141
|
+
if (!isEnabled()) return { ...EMPTY_IOS };
|
|
142
|
+
const res = await fridaGet('/integrity', { authKey: noisePubB64 });
|
|
143
|
+
if (!res || !res.attestation || !res.assertion) return { ...EMPTY_IOS };
|
|
144
|
+
return {
|
|
145
|
+
attestation: res.attestation,
|
|
146
|
+
assertion: res.assertion,
|
|
147
|
+
keyId: res.keyId || ''
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// ─── High-level: attestation fields shipped on every attested endpoint ────────
|
|
152
|
+
//
|
|
153
|
+
// Returns the alternating name/value pairs Cobalt ships via attestationFields():
|
|
154
|
+
// Android → gpia,_gg,_gi,_gp,_ge,_ga,push_token
|
|
155
|
+
// iOS → push_token
|
|
156
|
+
//
|
|
157
|
+
// These fields are part of the plaintext that gets AES-GCM-sealed into the
|
|
158
|
+
// ENC envelope, so the Play Integrity nonce is derived from the store (a stable
|
|
159
|
+
// per-session value), NOT from the ENC body. pushToken comes from the optional
|
|
160
|
+
// push client — empty by default.
|
|
161
|
+
async function attestationFields(device, nonceB64, opts) {
|
|
162
|
+
opts = opts || {};
|
|
163
|
+
const pushToken = opts.pushToken || '';
|
|
164
|
+
|
|
165
|
+
if (device && device.os === 'android') {
|
|
166
|
+
const a = await androidIntegrity(nonceB64 || '');
|
|
167
|
+
return [
|
|
168
|
+
'gpia', a.gpia,
|
|
169
|
+
'_gg', a.gg,
|
|
170
|
+
'_gi', a.gi,
|
|
171
|
+
'_gp', a.gp,
|
|
172
|
+
'_ge', a.ge,
|
|
173
|
+
'_ga', a.ga,
|
|
174
|
+
'push_token', pushToken
|
|
175
|
+
];
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// iOS ships only push_token in the form body; App Attest rides in &H= + header.
|
|
179
|
+
return ['push_token', pushToken];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ─── High-level: body attestation (&H= suffix + Authorization header) ─────────
|
|
183
|
+
//
|
|
184
|
+
// encB64 — the base64url ENC payload (without the "ENC=" prefix)
|
|
185
|
+
// device — { os }
|
|
186
|
+
// keys.noisePubB64 — standard base64 of the 32-byte Noise public key. Android
|
|
187
|
+
// uses it as the Keystore attestation challenge auth key;
|
|
188
|
+
// iOS uses it as the App Attest clientDataHash source.
|
|
189
|
+
async function attestBody(encB64, device, keys) {
|
|
190
|
+
keys = keys || {};
|
|
191
|
+
if (device && device.os === 'android') {
|
|
192
|
+
return androidAttestBody(encB64, keys.noisePubB64 || '');
|
|
193
|
+
}
|
|
194
|
+
const data = await iosAppAttest(keys.noisePubB64 || '');
|
|
195
|
+
if (!data.attestation || !data.assertion || !data.keyId) return { ...EMPTY_BODY };
|
|
196
|
+
return {
|
|
197
|
+
bodyAttestation: '{"assertion":"' + data.assertion + '"}',
|
|
198
|
+
authorizationHeader: data.attestation + '|' + data.keyId
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
module.exports = {
|
|
203
|
+
isEnabled,
|
|
204
|
+
attestationFields,
|
|
205
|
+
attestBody,
|
|
206
|
+
androidInfo,
|
|
207
|
+
toHex,
|
|
208
|
+
toBase64Url,
|
|
209
|
+
EMPTY_ANDROID,
|
|
210
|
+
EMPTY_IOS,
|
|
211
|
+
EMPTY_BODY
|
|
212
|
+
};
|