nodejs-insta-private-api-mqt 1.3.78 → 1.3.79

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.
@@ -1,838 +1,806 @@
1
- // AccountRepository.js
2
- // Complete file with improved certificate generation using node-forge
3
- // Ready to copy and use. Requires: npm install node-forge uuid
4
-
5
1
  const Repository = require('../core/repository');
6
2
  const crypto = require('crypto');
7
- const forge = require('node-forge');
8
- const { v4: uuidv4 } = require('uuid');
9
3
 
10
4
  class AccountRepository extends Repository {
11
- constructor(client) {
12
- super(client);
13
- this.maxRetries = 3;
14
- }
15
-
16
- async requestWithRetry(requestFn, retries = 0) {
17
- try {
18
- const result = await requestFn();
19
- return result;
20
- } catch (error) {
21
- const shouldRetry =
22
- (error.data?.error_type === 'server_error' ||
23
- error.data?.error_type === 'rate_limited') &&
24
- retries < this.maxRetries;
25
- if (shouldRetry) {
26
- const delay = 1000 * (retries + 1);
27
- await new Promise(resolve => setTimeout(resolve, delay));
28
- return this.requestWithRetry(requestFn, retries + 1);
29
- }
30
- throw error;
31
- }
32
- }
33
-
34
- async login(credentialsOrUsername, passwordArg) {
35
- let username, password;
36
- if (typeof credentialsOrUsername === 'object' && credentialsOrUsername !== null) {
37
- username = credentialsOrUsername.username;
38
- password = credentialsOrUsername.password;
39
- } else {
40
- username = credentialsOrUsername;
41
- password = passwordArg;
42
- }
43
- if (!username || !password) {
44
- throw new Error('Username and password are required');
45
- }
46
-
47
- if (!this.client.state.passwordEncryptionPubKey) {
48
- await this.syncLoginExperiments();
49
- }
50
-
51
- const { encrypted, time } = this.encryptPassword(password);
52
-
53
- return this.requestWithRetry(async () => {
54
- const aacInitTimestamp = Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 50);
55
- const aacjid = crypto.randomUUID ? crypto.randomUUID() : uuidv4();
56
- const aaccs = crypto.randomBytes(32).toString('base64url');
57
-
58
- const clientInputParams = {
59
- aac: JSON.stringify({
60
- aac_init_timestamp: aacInitTimestamp,
61
- aacjid: aacjid,
62
- aaccs: aaccs,
63
- }),
64
- sim_phones: [],
65
- aymh_accounts: [],
66
- network_bssid: null,
67
- secure_family_device_id: '',
68
- has_granted_read_contacts_permissions: 0,
69
- auth_secure_device_id: '',
70
- has_whatsapp_installed: 1,
71
- password: `#PWD_INSTAGRAM:4:${time}:${encrypted}`,
72
- sso_token_map_json_string: '{}',
73
- block_store_machine_id: '',
74
- ig_vetted_device_nonces: '{}',
75
- contact_point: username,
76
- machine_id: this.client.state.mid || '',
77
- login_attempt_count: '0',
78
- reg_flow_taken: 'phone',
79
- device_id: this.client.state.uuid,
80
- phone_id: this.client.state.phoneId,
81
- family_device_id: this.client.state.phoneId,
82
- encryption_enabled: '1',
83
- has_dbl_tap_login: 0,
84
- jazoest: AccountRepository.createJazoest(this.client.state.phoneId || ''),
85
- openid_tokens: '{}',
86
- };
87
-
88
- const serverParams = {
89
- credential_type: 'password',
90
- device_id: this.client.state.uuid,
91
- };
92
-
93
- const paramsJson = JSON.stringify({
94
- client_input_params: clientInputParams,
95
- server_params: serverParams,
96
- });
97
-
98
- // Generate attest params using node-forge to create more standard X.509 certs
99
- const attestParams = AccountRepository.generateAttestParams(this.client.state);
100
-
101
- // Build headers using values from payload or fallbacks from client.state
102
- const bloksHeaders = {
103
- 'accept-language': 'ro-RO, en-US',
104
- 'content-type': 'application/x-www-form-urlencoded; charset=UTF-8',
105
- 'ig-intended-user-id': '0',
106
- 'priority': 'u=3',
107
- 'x-bloks-is-layout-rtl': 'false',
108
- 'x-bloks-prism-ax-base-colors-enabled': 'false',
109
- 'x-bloks-prism-button-version': 'CONTROL',
110
- 'x-bloks-prism-colors-enabled': 'true',
111
- 'x-bloks-prism-font-enabled': 'false',
112
- 'x-bloks-prism-indigo-link-version': '0',
113
- 'x-bloks-version-id': this.client.state.bloksVersionId || '5e47baf35c5a270b44c8906c8b99063564b30ef69779f3dee0b828bee2e4ef5b',
114
- 'x-fb-client-ip': 'True',
115
- 'x-fb-connection-type': this.client.state.connectionType || 'MOBILE.LTE',
116
- 'x-fb-friendly-name': 'IgApi: bloks/async_action/com.bloks.www.bloks.caa.login.async.send_login_request/',
117
- 'x-fb-network-properties': this.client.state.fbNetworkProperties || 'VPN;Metered;Validated;LocalAddrs=/10.0.0.2,;',
118
- 'x-fb-request-analytics-tags': JSON.stringify({
119
- network_tags: {
120
- product: this.client.state.fbAnalyticsApplicationId || '567067343352427',
121
- purpose: 'fetch',
122
- surface: 'undefined',
123
- request_category: 'api',
124
- retry_attempt: '0',
125
- },
126
- }),
127
- 'x-fb-server-cluster': 'True',
128
- 'x-ig-android-id': this.client.state.androidId || `android-${crypto.randomBytes(8).toString('hex')}`,
129
- 'x-ig-app-id': this.client.state.fbAnalyticsApplicationId || '567067343352427',
130
- 'x-ig-app-locale': this.client.state.locale || 'ro_RO',
131
- 'x-ig-attest-params': JSON.stringify(attestParams),
132
- 'x-ig-bandwidth-speed-kbps': String(this.client.state.bandwidthSpeedKbps || '2357.000'),
133
- 'x-ig-bandwidth-totalbytes-b': String(this.client.state.bandwidthTotalBytes || '0'),
134
- 'x-ig-bandwidth-totaltime-ms': String(this.client.state.bandwidthTotalTimeMs || '0'),
135
- 'x-ig-client-endpoint': 'com.bloks.www.caa.login.aymh_single_profile_screen_entry',
136
- 'x-ig-capabilities': this.client.state.capabilities || '3brTv10=',
137
- 'x-ig-connection-type': this.client.state.connectionType || 'MOBILE(LTE)',
138
- 'x-ig-device-id': this.client.state.deviceId || (this.client.state.uuid || ''),
139
- 'x-ig-device-locale': this.client.state.locale || 'ro_RO',
140
- 'x-ig-family-device-id': this.client.state.familyDeviceId || this.client.state.phoneId || '',
141
- 'x-ig-mapped-locale': this.client.state.locale || 'ro_RO',
142
- 'x-ig-nav-chain': this.client.state.navChain || '',
143
- 'x-ig-timezone-offset': String(this.client.state.timezoneOffset || (new Date().getTimezoneOffset() * -60)),
144
- 'x-ig-www-claim': this.client.state.igWWWClaim || '0',
145
- 'x-mid': this.client.state.mid || '',
146
- 'x-pigeon-rawclienttime': String(this.client.state.pigeonRawClientTime || Math.floor(Date.now() / 1000)),
147
- 'x-pigeon-session-id': this.client.state.pigeonSessionId || `UFS-${uuidv4()}`,
148
- 'x-tigon-is-retry': 'False',
149
- 'accept-encoding': 'gzip, deflate',
150
- 'user-agent': this.client.state.userAgent || 'Instagram 371.0.0.0.23 Android (30/11; 320dpi; 720x1449; Xiaomi/Redmi; M2006C3MNG; angelican; mt6765; ro_RO; 703217507)',
151
- 'x-fb-conn-uuid-client': this.client.state.connUuidClient || crypto.randomBytes(16).toString('hex'),
152
- 'x-fb-http-engine': this.client.state.fbHttpEngine || 'Liger',
153
- 'x-fb-rmd': this.client.state.fbRmd || 'state=URL_ELIGIBLE',
154
- };
155
-
156
- // Remove undefined/null headers
157
- Object.keys(bloksHeaders).forEach(k => {
158
- if (bloksHeaders[k] === undefined || bloksHeaders[k] === null) delete bloksHeaders[k];
159
- });
160
-
161
- const response = await this.client.request.send({
162
- method: 'POST',
163
- url: '/api/v1/bloks/async_action/com.bloks.www.bloks.caa.login.async.send_login_request/',
164
- form: { params: paramsJson },
165
- headers: bloksHeaders,
166
- });
167
-
168
- const body = response.body;
169
-
170
- if (body.two_factor_required) {
171
- const err = new Error('Two factor authentication required');
172
- err.name = 'IgLoginTwoFactorRequiredError';
173
- err.twoFactorInfo = body.two_factor_info;
174
- throw err;
175
- }
176
- if (body.error_type === 'bad_password') {
177
- const err = new Error('Bad password');
178
- err.name = 'IgLoginBadPasswordError';
179
- throw err;
180
- }
181
- if (body.error_type === 'invalid_user') {
182
- const err = new Error('Invalid user');
183
- err.name = 'IgLoginInvalidUserError';
184
- throw err;
185
- }
186
- if (body.message === 'challenge_required') {
187
- const err = new Error('Challenge required');
188
- err.name = 'IgCheckpointError';
189
- err.challengeInfo = body.challenge;
190
- throw err;
191
- }
192
-
193
- if (body.layout) {
194
- const layoutStr = typeof body.layout === 'string' ? body.layout : JSON.stringify(body.layout);
195
-
196
- if (layoutStr.includes('two_factor_required') || layoutStr.includes('"two_factor_info"')) {
197
- let twoFactorInfo = null;
198
- try {
199
- const match = layoutStr.match(/"two_factor_info"\s*:\s*(\{[^}]+\})/);
200
- if (match) twoFactorInfo = JSON.parse(match[1]);
201
- } catch (e) {}
202
- const err = new Error('Two factor authentication required');
203
- err.name = 'IgLoginTwoFactorRequiredError';
204
- err.twoFactorInfo = twoFactorInfo;
205
- throw err;
206
- }
207
-
208
- if (layoutStr.includes('bad_password') || layoutStr.includes('incorrect_password')) {
209
- const err = new Error('Bad password');
210
- err.name = 'IgLoginBadPasswordError';
211
- throw err;
212
- }
213
-
214
- if (layoutStr.includes('invalid_user') || layoutStr.includes('user_not_found')) {
215
- const err = new Error('Invalid user');
216
- err.name = 'IgLoginInvalidUserError';
217
- throw err;
218
- }
219
-
220
- if (layoutStr.includes('challenge_required')) {
221
- const err = new Error('Challenge required');
222
- err.name = 'IgCheckpointError';
223
- err.challengeInfo = body.challenge || null;
224
- throw err;
225
- }
226
-
227
- const bearerMatch = layoutStr.match(/Bearer IGT:2:[A-Za-z0-9+\/=]+/);
228
- if (bearerMatch) {
229
- this.client.state.authorization = bearerMatch[0];
230
- if (typeof this.client.state.updateAuthorization === 'function') {
231
- this.client.state.updateAuthorization();
232
- }
233
- }
234
-
235
- const pkMatch = layoutStr.match(/\\"pk\\":(\d+)/);
236
- if (pkMatch) {
237
- this.client.state.cookieUserId = pkMatch[1];
238
- }
239
-
240
- const wwwClaimMatch = layoutStr.match(/IG-Set-WWW-Claim[\\"\s:]+([a-f0-9]+)/i);
241
- if (wwwClaimMatch) {
242
- this.client.state.igWWWClaim = wwwClaimMatch[1];
243
- }
244
-
245
- const encKeyIdMatch = layoutStr.match(/IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i);
246
- if (encKeyIdMatch) {
247
- this.client.state.passwordEncryptionKeyId = parseInt(encKeyIdMatch[1]);
248
- }
249
-
250
- const encPubKeyMatch = layoutStr.match(/IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i);
251
- if (encPubKeyMatch) {
252
- this.client.state.passwordEncryptionPubKey = encPubKeyMatch[1];
253
- }
254
-
255
- const loginResponseMatch = layoutStr.match(/\\"logged_in_user\\":\{[^}]*\\"pk\\":(\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/);
256
- if (loginResponseMatch) {
257
- return {
258
- pk: parseInt(loginResponseMatch[1]),
259
- pk_id: loginResponseMatch[1],
260
- username: loginResponseMatch[2],
261
- };
262
- }
263
- }
264
-
265
- if (body.logged_in_user) {
266
- return body.logged_in_user;
267
- }
268
-
269
- try {
270
- const userInfo = await this.currentUser();
271
- return userInfo.user || userInfo;
272
- } catch (e) {
273
- return body;
274
- }
275
- });
276
-
277
- }
278
-
279
- async twoFactorLogin(username, verificationCode, twoFactorIdentifier, verificationMethod = '1') {
280
- return this.requestWithRetry(async () => {
281
- const response = await this.client.request.send({
282
- method: 'POST',
283
- url: '/api/v1/accounts/two_factor_login/',
284
- form: this.client.request.sign({
285
- username,
286
- verification_code: verificationCode,
287
- two_factor_identifier: twoFactorIdentifier,
288
- verification_method: verificationMethod,
289
- trust_this_device: '1',
290
- guid: this.client.state.uuid,
291
- device_id: this.client.state.deviceId,
292
- phone_id: this.client.state.phoneId,
293
- }),
294
- });
295
- return response.body;
296
- });
297
- }
298
-
299
- async logout() {
300
- return this.requestWithRetry(async () => {
301
- const response = await this.client.request.send({
302
- method: 'POST',
303
- url: '/api/v1/accounts/logout/',
304
- form: this.client.request.sign({
305
- _uuid: this.client.state.uuid,
306
- }),
307
- });
308
- return response.body;
309
- });
310
- }
311
-
312
- async currentUser() {
313
- return this.requestWithRetry(async () => {
314
- const response = await this.client.request.send({
315
- method: 'GET',
316
- url: '/api/v1/accounts/current_user/',
317
- qs: { edit: true },
318
- });
319
- return response.body;
320
- });
321
- }
322
-
323
- async accountInfo() {
324
- return this.currentUser();
325
- }
326
-
327
- async editProfile({ externalUrl, phoneNumber, username, fullName, biography, email } = {}) {
328
- return this.requestWithRetry(async () => {
329
- const current = await this.currentUser();
330
- const user = current.user || current;
331
- const response = await this.client.request.send({
332
- method: 'POST',
333
- url: '/api/v1/accounts/edit_profile/',
334
- form: this.client.request.sign({
335
- _uuid: this.client.state.uuid,
336
- external_url: externalUrl !== undefined ? externalUrl : (user.external_url || ''),
337
- phone_number: phoneNumber !== undefined ? phoneNumber : (user.phone_number || ''),
338
- username: username !== undefined ? username : user.username,
339
- full_name: fullName !== undefined ? fullName : (user.full_name || ''),
340
- biography: biography !== undefined ? biography : (user.biography || ''),
341
- email: email !== undefined ? email : (user.email || ''),
342
- }),
343
- });
344
- return response.body;
345
- });
346
- }
347
-
348
- async setBiography(biography) {
349
- return this.editProfile({ biography });
350
- }
351
-
352
- async setExternalUrl(url) {
353
- return this.editProfile({ externalUrl: url });
354
- }
355
-
356
- async removeBioLinks() {
357
- return this.editProfile({ externalUrl: '' });
358
- }
359
-
360
- async setGender(gender) {
361
- return this.requestWithRetry(async () => {
362
- const response = await this.client.request.send({
363
- method: 'POST',
364
- url: '/api/v1/accounts/set_gender/',
365
- form: this.client.request.sign({
366
- _uuid: this.client.state.uuid,
367
- gender,
368
- }),
369
- });
370
- return response.body;
371
- });
372
- }
373
-
374
- async setPrivate() {
375
- return this.requestWithRetry(async () => {
376
- const response = await this.client.request.send({
377
- method: 'POST',
378
- url: '/api/v1/accounts/set_private/',
379
- form: this.client.request.sign({
380
- _uuid: this.client.state.uuid,
381
- }),
382
- });
383
- return response.body;
384
- });
385
- }
386
-
387
- async setPublic() {
388
- return this.requestWithRetry(async () => {
389
- const response = await this.client.request.send({
390
- method: 'POST',
391
- url: '/api/v1/accounts/set_public/',
392
- form: this.client.request.sign({
393
- _uuid: this.client.state.uuid,
394
- }),
395
- });
396
- return response.body;
397
- });
398
- }
399
-
400
- async changePassword(oldPassword, newPassword) {
401
- const oldEnc = this.encryptPassword(oldPassword);
402
- const newEnc = this.encryptPassword(newPassword);
403
- return this.requestWithRetry(async () => {
404
- const response = await this.client.request.send({
405
- method: 'POST',
406
- url: '/api/v1/accounts/change_password/',
407
- form: this.client.request.sign({
408
- _uuid: this.client.state.uuid,
409
- enc_old_password: #PWD_INSTAGRAM:4:${oldEnc.time}:${oldEnc.encrypted},
410
- enc_new_password1: #PWD_INSTAGRAM:4:${newEnc.time}:${newEnc.encrypted},
411
- enc_new_password2: #PWD_INSTAGRAM:4:${newEnc.time}:${newEnc.encrypted},
412
- }),
413
- });
414
- return response.body;
415
- });
416
- }
417
-
418
- async sendConfirmEmail() {
419
- return this.requestWithRetry(async () => {
420
- const response = await this.client.request.send({
421
- method: 'POST',
422
- url: '/api/v1/accounts/send_confirm_email/',
423
- form: this.client.request.sign({
424
- _uuid: this.client.state.uuid,
425
- send_source: 'edit_profile',
426
- }),
427
- });
428
- return response.body;
429
- });
430
- }
431
-
432
- async sendConfirmPhoneNumber(phoneNumber) {
433
- return this.requestWithRetry(async () => {
434
- const response = await this.client.request.send({
435
- method: 'POST',
436
- url: '/api/v1/accounts/send_confirm_phone_number/',
437
- form: this.client.request.sign({
438
- _uuid: this.client.state.uuid,
439
- phone_number: phoneNumber,
440
- }),
441
- });
442
- return response.body;
443
- });
444
- }
445
-
446
- async profilePictureChange(uploadId) {
447
- return this.requestWithRetry(async () => {
448
- const response = await this.client.request.send({
449
- method: 'POST',
450
- url: '/api/v1/accounts/change_profile_picture/',
451
- form: this.client.request.sign({
452
- _uuid: this.client.state.uuid,
453
- use_fbuploader: 'true',
454
- upload_id: uploadId,
455
- }),
456
- });
457
- return response.body;
458
- });
459
- }
460
-
461
- async profilePictureRemove() {
462
- return this.requestWithRetry(async () => {
463
- const response = await this.client.request.send({
464
- method: 'POST',
465
- url: '/api/v1/accounts/remove_profile_picture/',
466
- form: this.client.request.sign({
467
- _uuid: this.client.state.uuid,
468
- }),
469
- });
470
- return response.body;
471
- });
472
- }
473
-
474
- async newsInbox() {
475
- return this.requestWithRetry(async () => {
476
- const response = await this.client.request.send({
477
- method: 'GET',
478
- url: '/api/v1/news/inbox/',
479
- });
480
- return response.body;
481
- });
482
- }
483
-
484
- async syncLoginExperiments() {
485
- return this.requestWithRetry(async () => {
486
- const response = await this.client.request.send({
487
- method: 'POST',
488
- url: '/api/v1/qe/sync/',
489
- form: this.client.request.sign({
490
- id: this.client.state.uuid,
491
- server_config_retrieval: '1',
492
- experiments: this.client.state.constants?.LOGIN_EXPERIMENTS,
493
- }),
494
- });
495
- return response.body;
496
- });
497
- }
498
-
499
- async syncPostLoginExperiments() {
500
- let userId;
501
- try { userId = this.client.state.cookieUserId; } catch { userId = '0'; }
502
- return this.requestWithRetry(async () => {
503
- const response = await this.client.request.send({
504
- method: 'POST',
505
- url: '/api/v1/qe/sync/',
506
- form: this.client.request.sign({
507
- id: userId,
508
- _uid: userId,
509
- _uuid: this.client.state.uuid,
510
- server_config_retrieval: '1',
511
- experiments: this.client.state.constants?.EXPERIMENTS,
512
- }),
513
- });
514
- return response.body;
515
- });
516
- }
517
-
518
- async syncLauncher(preLogin = true) {
519
- const data = {
520
- id: this.client.state.uuid,
521
- server_config_retrieval: '1',
522
- };
523
- if (!preLogin) {
524
- try {
525
- data._uid = this.client.state.cookieUserId;
526
- data._uuid = this.client.state.uuid;
527
- } catch {}
528
- }
529
- return this.requestWithRetry(async () => {
530
- const response = await this.client.request.send({
531
- method: 'POST',
532
- url: '/api/v1/launcher/sync/',
533
- form: this.client.request.sign(data),
534
- });
535
- return response.body;
536
- });
537
- }
538
-
539
- async syncDeviceFeatures() {
540
- return this.requestWithRetry(async () => {
541
- const response = await this.client.request.send({
542
- method: 'POST',
543
- url: '/api/v1/devices/sync/',
544
- form: this.client.request.sign({
545
- id: this.client.state.uuid,
546
- server_config_retrieval: '1',
547
- }),
548
- });
549
- return response.body;
550
- });
551
- }
552
-
553
- async getPrefillCandidates() {
554
- return this.requestWithRetry(async () => {
555
- const response = await this.client.request.send({
556
- method: 'POST',
557
- url: '/api/v1/accounts/get_prefill_candidates/',
558
- form: this.client.request.sign({
559
- android_device_id: this.client.state.deviceId,
560
- phone_id: this.client.state.phoneId,
561
- usages: '["account_recovery_omnibox"]',
562
- device_id: this.client.state.uuid,
563
- }),
564
- });
565
- return response.body;
566
- });
567
- }
568
-
569
- async contactPointPrefill(usage = 'prefill') {
570
- return this.requestWithRetry(async () => {
571
- const response = await this.client.request.send({
572
- method: 'POST',
573
- url: '/api/v1/accounts/contact_point_prefill/',
574
- form: this.client.request.sign({
575
- phone_id: this.client.state.phoneId,
576
- usage,
577
- }),
578
- });
579
- return response.body;
580
- });
581
- }
582
-
583
- async getZrToken(params = {}) {
584
- return this.requestWithRetry(async () => {
585
- const response = await this.client.request.send({
586
- method: 'GET',
587
- url: '/api/v1/zr/token/result/',
588
- qs: {
589
- device_id: this.client.state.deviceId,
590
- custom_device_id: this.client.state.uuid,
591
- fetch_reason: 'token_expired',
592
- token_hash: '',
593
- ...params,
594
- },
595
- });
596
- return response.body;
597
- });
598
- }
599
-
600
- async getConsentSignupConfig() {
601
- return this.requestWithRetry(async () => {
602
- const response = await this.client.request.send({
603
- method: 'GET',
604
- url: '/api/v1/consent/get_signup_config/',
605
- qs: {
606
- guid: this.client.state.uuid,
607
- main_account_selected: false,
608
- },
609
- });
610
- return response.body;
611
- });
612
- }
613
-
614
- async sendRecoveryFlowEmail(query) {
615
- return this.requestWithRetry(async () => {
616
- const response = await this.client.request.send({
617
- url: '/api/v1/accounts/send_recovery_flow_email/',
618
- method: 'POST',
619
- form: this.client.request.sign({
620
- adid: '',
621
- guid: this.client.state.uuid,
622
- device_id: this.client.state.deviceId,
623
- query,
624
- }),
625
- });
626
- return response.body;
627
- });
628
- }
629
-
630
- async sendRecoveryFlowSms(query) {
631
- return this.requestWithRetry(async () => {
632
- const response = await this.client.request.send({
633
- url: '/api/v1/accounts/send_recovery_flow_sms/',
634
- method: 'POST',
635
- form: this.client.request.sign({
636
- adid: '',
637
- guid: this.client.state.uuid,
638
- device_id: this.client.state.deviceId,
639
- query,
640
- }),
641
- });
642
- return response.body;
643
- });
644
- }
645
-
646
- static generateAttestParams(state) {
647
- // Use node-forge to create a more standard self-signed certificate chain.
648
- // Note: This is still a self-signed chain and not hardware-backed attestation.
649
- // It is more syntactically correct and includes standard X.509 extensions.
650
-
651
- // Generate RSA keypair (2048 bits) using node-forge
652
- const keypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
653
-
654
- // Create leaf certificate
655
- const leafCert = AccountRepository._generateSelfSignedCertForge(keypair.privateKey, keypair.publicKey, 'Android Keystore Key');
656
-
657
- // Create intermediate certificate (self-signed for simulation)
658
- const intermediateKeypair = forge.pki.rsa.generateKeyPair({ bits: 2048, e: 0x10001 });
659
- const intermediateCert = AccountRepository._generateSelfSignedCertForge(intermediateKeypair.privateKey, intermediateKeypair.publicKey, 'TEE');
660
-
661
- // Certificate chain: leaf + intermediate
662
- const certificateChain = leafCert + '\n' + intermediateCert;
663
-
664
- // challenge nonce
665
- const challengeNonce = crypto.randomBytes(24).toString('base64url');
666
-
667
- // Sign the challengeNonce with the private key (forge)
668
- const md = forge.md.sha256.create();
669
- md.update(challengeNonce, 'utf8');
670
- const signedBytes = keypair.privateKey.sign(md);
671
- const signedNonce = Buffer.from(signedBytes, 'binary').toString('base64');
672
-
673
- // Compute key hash (sha256 of public key DER)
674
- const publicKeyAsn1 = forge.pki.publicKeyToAsn1(keypair.publicKey);
675
- const publicKeyDer = forge.asn1.toDer(publicKeyAsn1).getBytes();
676
- const keyHash = crypto.createHash('sha256').update(Buffer.from(publicKeyDer, 'binary')).digest('hex');
677
-
678
- return {
679
- attestation: [{
680
- version: 2,
681
- type: 'keystore',
682
- errors: [0],
683
- challenge_nonce: challengeNonce,
684
- signed_nonce: signedNonce,
685
- key_hash: keyHash,
686
- certificate_chain: certificateChain,
687
- }],
688
- };
689
-
690
- }
691
-
692
- static _generateSelfSignedCertForge(privateKeyForge, publicKeyForge, cn) {
693
- // Build a certificate using node-forge with standard extensions
694
- const cert = forge.pki.createCertificate();
695
- cert.publicKey = publicKeyForge;
696
- cert.serialNumber = (Math.floor(Math.random() * 1e16)).toString(16);
697
- const now = new Date();
698
- cert.validity.notBefore = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
699
- cert.validity.notAfter = new Date(now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
700
-
701
- const attrs = [
702
- { name: 'commonName', value: cn },
703
- { name: 'organizationName', value: 'com.instagram.android' },
704
- ];
705
- cert.setSubject(attrs);
706
- cert.setIssuer(attrs);
707
-
708
- cert.setExtensions([
709
- { name: 'basicConstraints', cA: false },
710
- { name: 'keyUsage', digitalSignature: true, keyEncipherment: true },
711
- { name: 'extKeyUsage', serverAuth: true, clientAuth: true },
712
- { name: 'subjectKeyIdentifier' },
713
- ]);
714
-
715
- // Sign certificate with private key
716
- cert.sign(privateKeyForge, forge.md.sha256.create());
717
-
718
- // Convert to PEM
719
- const pem = forge.pki.certificateToPem(cert);
720
- return pem;
721
-
722
- }
723
-
724
- static createJazoest(input) {
725
- const buf = Buffer.from(String(input || ''), 'ascii');
726
- let sum = 0;
727
- for (let i = 0; i < buf.byteLength; i++) {
728
- sum += buf.readUInt8(i);
729
- }
730
- return 2${sum};
731
- }
732
-
733
- encryptPassword(password) {
734
- if (!this.client.state.passwordEncryptionPubKey) {
735
- return { time: Math.floor(Date.now() / 1000).toString(), encrypted: password };
736
- }
737
-
738
- const randKey = crypto.randomBytes(32);
739
- const iv = crypto.randomBytes(12);
740
-
741
- const rsaEncrypted = crypto.publicEncrypt({
742
- key: Buffer.from(this.client.state.passwordEncryptionPubKey, 'base64').toString(),
743
- padding: crypto.constants.RSA_PKCS1_PADDING,
744
- }, randKey);
745
-
746
- const cipher = crypto.createCipheriv('aes-256-gcm', randKey, iv);
747
- const time = Math.floor(Date.now() / 1000).toString();
748
- cipher.setAAD(Buffer.from(time));
749
-
750
- const aesEncrypted = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]);
751
- const sizeBuffer = Buffer.alloc(2, 0);
752
- sizeBuffer.writeInt16LE(rsaEncrypted.byteLength, 0);
753
- const authTag = cipher.getAuthTag();
754
-
755
- return {
756
- time,
757
- encrypted: Buffer.concat([
758
- Buffer.from([1, this.client.state.passwordEncryptionKeyId || 0]),
759
- iv,
760
- sizeBuffer,
761
- rsaEncrypted,
762
- authTag,
763
- aesEncrypted
764
- ]).toString('base64')
765
- };
766
-
767
- }
768
-
769
- async passwordPublicKeys() {
770
- const response = await this.client.request.send({
771
- method: 'GET',
772
- url: '/api/v1/qe/sync/',
773
- });
774
- const headers = response.headers || {};
775
- const keyId = parseInt(headers['ig-set-password-encryption-key-id'] || '0');
776
- const pubKey = headers['ig-set-password-encryption-pub-key'] || '';
777
- return { keyId, pubKey };
778
- }
779
-
780
- async setPresenceDisabled(disabled = true) {
781
- return this.requestWithRetry(async () => {
782
- const response = await this.client.request.send({
783
- method: 'POST',
784
- url: '/api/v1/accounts/set_presence_disabled/',
785
- form: this.client.request.sign({
786
- _uuid: this.client.state.uuid,
787
- disabled: disabled ? '1' : '0',
788
- }),
789
- });
790
- return response.body;
791
- });
792
- }
793
-
794
- async getCommentFilter() {
795
- return this.requestWithRetry(async () => {
796
- const response = await this.client.request.send({
797
- method: 'GET',
798
- url: '/api/v1/accounts/get_comment_filter/',
799
- });
800
- return response.body;
801
- });
802
- }
803
-
804
- async setCommentFilter(configValue = 0) {
805
- return this.requestWithRetry(async () => {
806
- const response = await this.client.request.send({
807
- method: 'POST',
808
- url: '/api/v1/accounts/set_comment_filter/',
809
- form: this.client.request.sign({
810
- _uuid: this.client.state.uuid,
811
- config_value: String(configValue),
812
- }),
813
- });
814
- return response.body;
815
- });
816
- }
817
-
818
- async pushPreferences(preferences = 'default') {
819
- return this.requestWithRetry(async () => {
820
- const response = await this.client.request.send({
821
- method: 'POST',
822
- url: '/api/v1/push/register/',
823
- form: this.client.request.sign({
824
- _uuid: this.client.state.uuid,
825
- device_type: 'android_mqtt',
826
- is_main_push_channel: 'true',
827
- phone_id: this.client.state.phoneId,
828
- device_token: '',
829
- guid: this.client.state.uuid,
830
- users: preferences,
831
- }),
832
- });
833
- return response.body;
834
- });
835
- }
5
+ constructor(client) {
6
+ super(client);
7
+ this.maxRetries = 3;
8
+ }
9
+
10
+ async requestWithRetry(requestFn, retries = 0) {
11
+ try {
12
+ const result = await requestFn();
13
+ return result;
14
+ } catch (error) {
15
+ const shouldRetry =
16
+ (error.data?.error_type === 'server_error' ||
17
+ error.data?.error_type === 'rate_limited') &&
18
+ retries < this.maxRetries;
19
+ if (shouldRetry) {
20
+ const delay = 1000 * (retries + 1);
21
+ await new Promise(resolve => setTimeout(resolve, delay));
22
+ return this.requestWithRetry(requestFn, retries + 1);
23
+ }
24
+ throw error;
25
+ }
26
+ }
27
+
28
+ async login(credentialsOrUsername, passwordArg) {
29
+ let username, password;
30
+ if (typeof credentialsOrUsername === 'object' && credentialsOrUsername !== null) {
31
+ username = credentialsOrUsername.username;
32
+ password = credentialsOrUsername.password;
33
+ } else {
34
+ username = credentialsOrUsername;
35
+ password = passwordArg;
36
+ }
37
+ if (!username || !password) {
38
+ throw new Error('Username and password are required');
39
+ }
40
+
41
+ if (!this.client.state.passwordEncryptionPubKey) {
42
+ await this.syncLoginExperiments();
43
+ }
44
+
45
+ const { encrypted, time } = this.encryptPassword(password);
46
+
47
+ return this.requestWithRetry(async () => {
48
+ const aacInitTimestamp = Math.floor(Date.now() / 1000) - Math.floor(Math.random() * 50);
49
+ const aacjid = crypto.randomUUID ? crypto.randomUUID() : require('uuid').v4();
50
+ const aaccs = crypto.randomBytes(32).toString('base64url');
51
+
52
+ const clientInputParams = {
53
+ aac: JSON.stringify({
54
+ aac_init_timestamp: aacInitTimestamp,
55
+ aacjid: aacjid,
56
+ aaccs: aaccs,
57
+ }),
58
+ sim_phones: [],
59
+ aymh_accounts: [],
60
+ network_bssid: null,
61
+ secure_family_device_id: '',
62
+ has_granted_read_contacts_permissions: 0,
63
+ auth_secure_device_id: '',
64
+ has_whatsapp_installed: 1,
65
+ password: `#PWD_INSTAGRAM:4:${time}:${encrypted}`,
66
+ sso_token_map_json_string: '{}',
67
+ block_store_machine_id: '',
68
+ ig_vetted_device_nonces: '{}',
69
+ contact_point: username,
70
+ machine_id: this.client.state.mid || '',
71
+ login_attempt_count: '0',
72
+ reg_flow_taken: 'phone',
73
+ device_id: this.client.state.uuid,
74
+ phone_id: this.client.state.phoneId,
75
+ family_device_id: this.client.state.phoneId,
76
+ encryption_enabled: '1',
77
+ has_dbl_tap_login: 0,
78
+ jazoest: AccountRepository.createJazoest(this.client.state.phoneId),
79
+ openid_tokens: '{}',
80
+ };
81
+
82
+ const serverParams = {
83
+ credential_type: 'password',
84
+ device_id: this.client.state.uuid,
85
+ };
86
+
87
+ const paramsJson = JSON.stringify({
88
+ client_input_params: clientInputParams,
89
+ server_params: serverParams,
90
+ });
91
+
92
+ const attestParams = AccountRepository.generateAttestParams(this.client.state);
93
+
94
+ const bloksHeaders = {
95
+ 'X-Bloks-Prism-Ax-Base-Colors-Enabled': 'false',
96
+ 'X-Bloks-Prism-Button-Version': 'CONTROL',
97
+ 'X-Bloks-Prism-Colors-Enabled': 'true',
98
+ 'X-Bloks-Prism-Font-Enabled': 'false',
99
+ 'X-Bloks-Prism-Indigo-Link-Version': '0',
100
+ 'X-FB-Friendly-Name': 'IgApi: bloks/async_action/com.bloks.www.bloks.caa.login.async.send_login_request/',
101
+ 'X-FB-Request-Analytics-Tags': JSON.stringify({
102
+ network_tags: {
103
+ product: this.client.state.fbAnalyticsApplicationId,
104
+ purpose: 'fetch',
105
+ surface: 'undefined',
106
+ request_category: 'api',
107
+ retry_attempt: '0',
108
+ },
109
+ }),
110
+ 'X-IG-Client-Endpoint': 'com.bloks.www.caa.login.aymh_single_profile_screen_entry',
111
+ 'X-Tigon-Is-Retry': 'False',
112
+ 'X-FB-RMD': 'state=URL_ELIGIBLE',
113
+ 'X-IG-Attest-Params': JSON.stringify(attestParams),
114
+ 'X-FB-Connection-Type': 'MOBILE.LTE',
115
+ 'X-FB-Network-Properties': 'VPN;Metered;Validated;LocalAddrs=/10.0.0.2,;',
116
+ 'X-FB-Conn-UUID-Client': crypto.randomBytes(16).toString('hex'),
117
+ 'Accept-Encoding': 'gzip, deflate',
118
+ 'X-FB-HTTP-Engine': 'Liger',
119
+ };
120
+
121
+ const response = await this.client.request.send({
122
+ method: 'POST',
123
+ url: '/api/v1/bloks/async_action/com.bloks.www.bloks.caa.login.async.send_login_request/',
124
+ form: { params: paramsJson },
125
+ headers: bloksHeaders,
126
+ });
127
+
128
+ const body = response.body;
129
+
130
+ if (body.two_factor_required) {
131
+ const err = new Error('Two factor authentication required');
132
+ err.name = 'IgLoginTwoFactorRequiredError';
133
+ err.twoFactorInfo = body.two_factor_info;
134
+ throw err;
135
+ }
136
+ if (body.error_type === 'bad_password') {
137
+ const err = new Error('Bad password');
138
+ err.name = 'IgLoginBadPasswordError';
139
+ throw err;
140
+ }
141
+ if (body.error_type === 'invalid_user') {
142
+ const err = new Error('Invalid user');
143
+ err.name = 'IgLoginInvalidUserError';
144
+ throw err;
145
+ }
146
+ if (body.message === 'challenge_required') {
147
+ const err = new Error('Challenge required');
148
+ err.name = 'IgCheckpointError';
149
+ err.challengeInfo = body.challenge;
150
+ throw err;
151
+ }
152
+
153
+ if (body.layout) {
154
+ const layoutStr = typeof body.layout === 'string' ? body.layout : JSON.stringify(body.layout);
155
+
156
+ if (layoutStr.includes('two_factor_required') || layoutStr.includes('"two_factor_info"')) {
157
+ let twoFactorInfo = null;
158
+ try {
159
+ const match = layoutStr.match(/"two_factor_info"\s*:\s*(\{[^}]+\})/);
160
+ if (match) twoFactorInfo = JSON.parse(match[1]);
161
+ } catch (e) {}
162
+ const err = new Error('Two factor authentication required');
163
+ err.name = 'IgLoginTwoFactorRequiredError';
164
+ err.twoFactorInfo = twoFactorInfo;
165
+ throw err;
166
+ }
167
+
168
+ if (layoutStr.includes('bad_password') || layoutStr.includes('incorrect_password')) {
169
+ const err = new Error('Bad password');
170
+ err.name = 'IgLoginBadPasswordError';
171
+ throw err;
172
+ }
173
+
174
+ if (layoutStr.includes('invalid_user') || layoutStr.includes('user_not_found')) {
175
+ const err = new Error('Invalid user');
176
+ err.name = 'IgLoginInvalidUserError';
177
+ throw err;
178
+ }
179
+
180
+ if (layoutStr.includes('challenge_required')) {
181
+ const err = new Error('Challenge required');
182
+ err.name = 'IgCheckpointError';
183
+ err.challengeInfo = body.challenge || null;
184
+ throw err;
185
+ }
186
+
187
+ const bearerMatch = layoutStr.match(/Bearer IGT:2:[A-Za-z0-9+\/=]+/);
188
+ if (bearerMatch) {
189
+ this.client.state.authorization = bearerMatch[0];
190
+ this.client.state.updateAuthorization();
191
+ }
192
+
193
+ const pkMatch = layoutStr.match(/\\"pk\\":(\d+)/);
194
+ if (pkMatch) {
195
+ this.client.state.cookieUserId = pkMatch[1];
196
+ }
197
+
198
+ const wwwClaimMatch = layoutStr.match(/IG-Set-WWW-Claim[\\"\s:]+([a-f0-9]+)/i);
199
+ if (wwwClaimMatch) {
200
+ this.client.state.igWWWClaim = wwwClaimMatch[1];
201
+ }
202
+
203
+ const encKeyIdMatch = layoutStr.match(/IG-Set-Password-Encryption-Key-Id[\\"\s:]+(\d+)/i);
204
+ if (encKeyIdMatch) {
205
+ this.client.state.passwordEncryptionKeyId = parseInt(encKeyIdMatch[1]);
206
+ }
207
+
208
+ const encPubKeyMatch = layoutStr.match(/IG-Set-Password-Encryption-Pub-Key[\\"\s:]+([A-Za-z0-9+\/=]+)/i);
209
+ if (encPubKeyMatch) {
210
+ this.client.state.passwordEncryptionPubKey = encPubKeyMatch[1];
211
+ }
212
+
213
+ const loginResponseMatch = layoutStr.match(/\\"logged_in_user\\":\{[^}]*\\"pk\\":(\d+)[^}]*\\"username\\":\\"([^"\\]+)\\"/);
214
+ if (loginResponseMatch) {
215
+ return {
216
+ pk: parseInt(loginResponseMatch[1]),
217
+ pk_id: loginResponseMatch[1],
218
+ username: loginResponseMatch[2],
219
+ };
220
+ }
221
+ }
222
+
223
+ if (body.logged_in_user) {
224
+ return body.logged_in_user;
225
+ }
226
+
227
+ try {
228
+ const userInfo = await this.currentUser();
229
+ return userInfo.user || userInfo;
230
+ } catch (e) {
231
+ return body;
232
+ }
233
+ });
234
+ }
235
+
236
+ async twoFactorLogin(username, verificationCode, twoFactorIdentifier, verificationMethod = '1') {
237
+ return this.requestWithRetry(async () => {
238
+ const response = await this.client.request.send({
239
+ method: 'POST',
240
+ url: '/api/v1/accounts/two_factor_login/',
241
+ form: this.client.request.sign({
242
+ username,
243
+ verification_code: verificationCode,
244
+ two_factor_identifier: twoFactorIdentifier,
245
+ verification_method: verificationMethod,
246
+ trust_this_device: '1',
247
+ guid: this.client.state.uuid,
248
+ device_id: this.client.state.deviceId,
249
+ phone_id: this.client.state.phoneId,
250
+ }),
251
+ });
252
+ return response.body;
253
+ });
254
+ }
255
+
256
+ async logout() {
257
+ return this.requestWithRetry(async () => {
258
+ const response = await this.client.request.send({
259
+ method: 'POST',
260
+ url: '/api/v1/accounts/logout/',
261
+ form: this.client.request.sign({
262
+ _uuid: this.client.state.uuid,
263
+ }),
264
+ });
265
+ return response.body;
266
+ });
267
+ }
268
+
269
+ async currentUser() {
270
+ return this.requestWithRetry(async () => {
271
+ const response = await this.client.request.send({
272
+ method: 'GET',
273
+ url: '/api/v1/accounts/current_user/',
274
+ qs: { edit: true },
275
+ });
276
+ return response.body;
277
+ });
278
+ }
279
+
280
+ async accountInfo() {
281
+ return this.currentUser();
282
+ }
283
+
284
+ async editProfile({ externalUrl, phoneNumber, username, fullName, biography, email } = {}) {
285
+ return this.requestWithRetry(async () => {
286
+ const current = await this.currentUser();
287
+ const user = current.user || current;
288
+ const response = await this.client.request.send({
289
+ method: 'POST',
290
+ url: '/api/v1/accounts/edit_profile/',
291
+ form: this.client.request.sign({
292
+ _uuid: this.client.state.uuid,
293
+ external_url: externalUrl !== undefined ? externalUrl : (user.external_url || ''),
294
+ phone_number: phoneNumber !== undefined ? phoneNumber : (user.phone_number || ''),
295
+ username: username !== undefined ? username : user.username,
296
+ full_name: fullName !== undefined ? fullName : (user.full_name || ''),
297
+ biography: biography !== undefined ? biography : (user.biography || ''),
298
+ email: email !== undefined ? email : (user.email || ''),
299
+ }),
300
+ });
301
+ return response.body;
302
+ });
303
+ }
304
+
305
+ async setBiography(biography) {
306
+ return this.editProfile({ biography });
307
+ }
308
+
309
+ async setExternalUrl(url) {
310
+ return this.editProfile({ externalUrl: url });
311
+ }
312
+
313
+ async removeBioLinks() {
314
+ return this.editProfile({ externalUrl: '' });
315
+ }
316
+
317
+ async setGender(gender) {
318
+ return this.requestWithRetry(async () => {
319
+ const response = await this.client.request.send({
320
+ method: 'POST',
321
+ url: '/api/v1/accounts/set_gender/',
322
+ form: this.client.request.sign({
323
+ _uuid: this.client.state.uuid,
324
+ gender,
325
+ }),
326
+ });
327
+ return response.body;
328
+ });
329
+ }
330
+
331
+ async setPrivate() {
332
+ return this.requestWithRetry(async () => {
333
+ const response = await this.client.request.send({
334
+ method: 'POST',
335
+ url: '/api/v1/accounts/set_private/',
336
+ form: this.client.request.sign({
337
+ _uuid: this.client.state.uuid,
338
+ }),
339
+ });
340
+ return response.body;
341
+ });
342
+ }
343
+
344
+ async setPublic() {
345
+ return this.requestWithRetry(async () => {
346
+ const response = await this.client.request.send({
347
+ method: 'POST',
348
+ url: '/api/v1/accounts/set_public/',
349
+ form: this.client.request.sign({
350
+ _uuid: this.client.state.uuid,
351
+ }),
352
+ });
353
+ return response.body;
354
+ });
355
+ }
356
+
357
+ async changePassword(oldPassword, newPassword) {
358
+ const oldEnc = this.encryptPassword(oldPassword);
359
+ const newEnc = this.encryptPassword(newPassword);
360
+ return this.requestWithRetry(async () => {
361
+ const response = await this.client.request.send({
362
+ method: 'POST',
363
+ url: '/api/v1/accounts/change_password/',
364
+ form: this.client.request.sign({
365
+ _uuid: this.client.state.uuid,
366
+ enc_old_password: `#PWD_INSTAGRAM:4:${oldEnc.time}:${oldEnc.encrypted}`,
367
+ enc_new_password1: `#PWD_INSTAGRAM:4:${newEnc.time}:${newEnc.encrypted}`,
368
+ enc_new_password2: `#PWD_INSTAGRAM:4:${newEnc.time}:${newEnc.encrypted}`,
369
+ }),
370
+ });
371
+ return response.body;
372
+ });
373
+ }
374
+
375
+ async sendConfirmEmail() {
376
+ return this.requestWithRetry(async () => {
377
+ const response = await this.client.request.send({
378
+ method: 'POST',
379
+ url: '/api/v1/accounts/send_confirm_email/',
380
+ form: this.client.request.sign({
381
+ _uuid: this.client.state.uuid,
382
+ send_source: 'edit_profile',
383
+ }),
384
+ });
385
+ return response.body;
386
+ });
387
+ }
388
+
389
+ async sendConfirmPhoneNumber(phoneNumber) {
390
+ return this.requestWithRetry(async () => {
391
+ const response = await this.client.request.send({
392
+ method: 'POST',
393
+ url: '/api/v1/accounts/send_confirm_phone_number/',
394
+ form: this.client.request.sign({
395
+ _uuid: this.client.state.uuid,
396
+ phone_number: phoneNumber,
397
+ }),
398
+ });
399
+ return response.body;
400
+ });
401
+ }
402
+
403
+ async profilePictureChange(uploadId) {
404
+ return this.requestWithRetry(async () => {
405
+ const response = await this.client.request.send({
406
+ method: 'POST',
407
+ url: '/api/v1/accounts/change_profile_picture/',
408
+ form: this.client.request.sign({
409
+ _uuid: this.client.state.uuid,
410
+ use_fbuploader: 'true',
411
+ upload_id: uploadId,
412
+ }),
413
+ });
414
+ return response.body;
415
+ });
416
+ }
417
+
418
+ async profilePictureRemove() {
419
+ return this.requestWithRetry(async () => {
420
+ const response = await this.client.request.send({
421
+ method: 'POST',
422
+ url: '/api/v1/accounts/remove_profile_picture/',
423
+ form: this.client.request.sign({
424
+ _uuid: this.client.state.uuid,
425
+ }),
426
+ });
427
+ return response.body;
428
+ });
429
+ }
430
+
431
+ async newsInbox() {
432
+ return this.requestWithRetry(async () => {
433
+ const response = await this.client.request.send({
434
+ method: 'GET',
435
+ url: '/api/v1/news/inbox/',
436
+ });
437
+ return response.body;
438
+ });
439
+ }
440
+
441
+ async syncLoginExperiments() {
442
+ return this.requestWithRetry(async () => {
443
+ const response = await this.client.request.send({
444
+ method: 'POST',
445
+ url: '/api/v1/qe/sync/',
446
+ form: this.client.request.sign({
447
+ id: this.client.state.uuid,
448
+ server_config_retrieval: '1',
449
+ experiments: this.client.state.constants.LOGIN_EXPERIMENTS,
450
+ }),
451
+ });
452
+ return response.body;
453
+ });
454
+ }
455
+
456
+ async syncPostLoginExperiments() {
457
+ let userId;
458
+ try { userId = this.client.state.cookieUserId; } catch { userId = '0'; }
459
+ return this.requestWithRetry(async () => {
460
+ const response = await this.client.request.send({
461
+ method: 'POST',
462
+ url: '/api/v1/qe/sync/',
463
+ form: this.client.request.sign({
464
+ id: userId,
465
+ _uid: userId,
466
+ _uuid: this.client.state.uuid,
467
+ server_config_retrieval: '1',
468
+ experiments: this.client.state.constants.EXPERIMENTS,
469
+ }),
470
+ });
471
+ return response.body;
472
+ });
473
+ }
474
+
475
+ async syncLauncher(preLogin = true) {
476
+ const data = {
477
+ id: this.client.state.uuid,
478
+ server_config_retrieval: '1',
479
+ };
480
+ if (!preLogin) {
481
+ try {
482
+ data._uid = this.client.state.cookieUserId;
483
+ data._uuid = this.client.state.uuid;
484
+ } catch {}
485
+ }
486
+ return this.requestWithRetry(async () => {
487
+ const response = await this.client.request.send({
488
+ method: 'POST',
489
+ url: '/api/v1/launcher/sync/',
490
+ form: this.client.request.sign(data),
491
+ });
492
+ return response.body;
493
+ });
494
+ }
495
+
496
+ async syncDeviceFeatures() {
497
+ return this.requestWithRetry(async () => {
498
+ const response = await this.client.request.send({
499
+ method: 'POST',
500
+ url: '/api/v1/devices/sync/',
501
+ form: this.client.request.sign({
502
+ id: this.client.state.uuid,
503
+ server_config_retrieval: '1',
504
+ }),
505
+ });
506
+ return response.body;
507
+ });
508
+ }
509
+
510
+ async getPrefillCandidates() {
511
+ return this.requestWithRetry(async () => {
512
+ const response = await this.client.request.send({
513
+ method: 'POST',
514
+ url: '/api/v1/accounts/get_prefill_candidates/',
515
+ form: this.client.request.sign({
516
+ android_device_id: this.client.state.deviceId,
517
+ phone_id: this.client.state.phoneId,
518
+ usages: '["account_recovery_omnibox"]',
519
+ device_id: this.client.state.uuid,
520
+ }),
521
+ });
522
+ return response.body;
523
+ });
524
+ }
525
+
526
+ async contactPointPrefill(usage = 'prefill') {
527
+ return this.requestWithRetry(async () => {
528
+ const response = await this.client.request.send({
529
+ method: 'POST',
530
+ url: '/api/v1/accounts/contact_point_prefill/',
531
+ form: this.client.request.sign({
532
+ phone_id: this.client.state.phoneId,
533
+ usage,
534
+ }),
535
+ });
536
+ return response.body;
537
+ });
538
+ }
539
+
540
+ async getZrToken(params = {}) {
541
+ return this.requestWithRetry(async () => {
542
+ const response = await this.client.request.send({
543
+ method: 'GET',
544
+ url: '/api/v1/zr/token/result/',
545
+ qs: {
546
+ device_id: this.client.state.deviceId,
547
+ custom_device_id: this.client.state.uuid,
548
+ fetch_reason: 'token_expired',
549
+ token_hash: '',
550
+ ...params,
551
+ },
552
+ });
553
+ return response.body;
554
+ });
555
+ }
556
+
557
+ async getConsentSignupConfig() {
558
+ return this.requestWithRetry(async () => {
559
+ const response = await this.client.request.send({
560
+ method: 'GET',
561
+ url: '/api/v1/consent/get_signup_config/',
562
+ qs: {
563
+ guid: this.client.state.uuid,
564
+ main_account_selected: false,
565
+ },
566
+ });
567
+ return response.body;
568
+ });
569
+ }
570
+
571
+ async sendRecoveryFlowEmail(query) {
572
+ return this.requestWithRetry(async () => {
573
+ const response = await this.client.request.send({
574
+ url: '/api/v1/accounts/send_recovery_flow_email/',
575
+ method: 'POST',
576
+ form: this.client.request.sign({
577
+ adid: '',
578
+ guid: this.client.state.uuid,
579
+ device_id: this.client.state.deviceId,
580
+ query,
581
+ }),
582
+ });
583
+ return response.body;
584
+ });
585
+ }
586
+
587
+ async sendRecoveryFlowSms(query) {
588
+ return this.requestWithRetry(async () => {
589
+ const response = await this.client.request.send({
590
+ url: '/api/v1/accounts/send_recovery_flow_sms/',
591
+ method: 'POST',
592
+ form: this.client.request.sign({
593
+ adid: '',
594
+ guid: this.client.state.uuid,
595
+ device_id: this.client.state.deviceId,
596
+ query,
597
+ }),
598
+ });
599
+ return response.body;
600
+ });
601
+ }
602
+
603
+ static generateAttestParams(state) {
604
+ const challengeNonce = crypto.randomBytes(24).toString('base64url');
605
+
606
+ const { privateKey, publicKey } = crypto.generateKeyPairSync('ec', {
607
+ namedCurve: 'prime256v1',
608
+ });
609
+
610
+ const signedData = crypto.sign(null, Buffer.from(challengeNonce), privateKey);
611
+ const signedNonce = signedData.toString('base64');
612
+
613
+ const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
614
+ const keyHash = crypto.createHash('sha256').update(publicKeyDer).digest('hex');
615
+
616
+ const leafCertPem = AccountRepository._generateSelfSignedCert(privateKey, publicKey, 'Android Keystore Key');
617
+ const intermediateCertPem = AccountRepository._generateSelfSignedCert(privateKey, publicKey, 'TEE');
618
+
619
+ const certificateChain = leafCertPem + '\n' + intermediateCertPem;
620
+
621
+ return {
622
+ attestation: [{
623
+ version: 2,
624
+ type: 'keystore',
625
+ errors: [0],
626
+ challenge_nonce: challengeNonce,
627
+ signed_nonce: signedNonce,
628
+ key_hash: keyHash,
629
+ certificate_chain: certificateChain,
630
+ }],
631
+ };
632
+ }
633
+
634
+ static _generateSelfSignedCert(privateKey, publicKey, cn) {
635
+ const publicKeyDer = publicKey.export({ type: 'spki', format: 'der' });
636
+ const serialNumber = crypto.randomBytes(8);
637
+ const now = new Date();
638
+ const notBefore = new Date(now.getTime() - 365 * 24 * 60 * 60 * 1000);
639
+ const notAfter = new Date(now.getTime() + 10 * 365 * 24 * 60 * 60 * 1000);
640
+
641
+ const cnBytes = Buffer.from(cn, 'utf8');
642
+ const cnSeq = Buffer.concat([
643
+ Buffer.from([0x30, cnBytes.length + 13]),
644
+ Buffer.from([0x31, cnBytes.length + 11]),
645
+ Buffer.from([0x30, cnBytes.length + 9]),
646
+ Buffer.from([0x06, 0x03, 0x55, 0x04, 0x03]),
647
+ Buffer.from([0x0c, cnBytes.length]),
648
+ cnBytes,
649
+ ]);
650
+
651
+ function encodeTime(date) {
652
+ const str = date.toISOString().replace(/[-:T]/g, '').slice(2, 14) + 'Z';
653
+ return Buffer.concat([Buffer.from([0x17, str.length]), Buffer.from(str)]);
654
+ }
655
+
656
+ const validityBuf = Buffer.concat([
657
+ encodeTime(notBefore),
658
+ encodeTime(notAfter),
659
+ ]);
660
+ const validity = Buffer.concat([Buffer.from([0x30, validityBuf.length]), validityBuf]);
661
+
662
+ const tbs = Buffer.concat([
663
+ Buffer.from([0xa0, 0x03, 0x02, 0x01, 0x02]),
664
+ Buffer.from([0x02, serialNumber.length]), serialNumber,
665
+ Buffer.from([0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]),
666
+ cnSeq,
667
+ validity,
668
+ cnSeq,
669
+ publicKeyDer,
670
+ ]);
671
+
672
+ const tbsSeq = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), tbs]);
673
+ tbsSeq.writeUInt16BE(tbs.length, 2);
674
+
675
+ const signature = crypto.sign(null, tbsSeq, privateKey);
676
+
677
+ const sigBitString = Buffer.concat([
678
+ Buffer.from([0x03, signature.length + 1, 0x00]),
679
+ signature,
680
+ ]);
681
+
682
+ const algId = Buffer.from([0x30, 0x0a, 0x06, 0x08, 0x2a, 0x86, 0x48, 0xce, 0x3d, 0x04, 0x03, 0x02]);
683
+
684
+ const certBody = Buffer.concat([tbsSeq, algId, sigBitString]);
685
+ const cert = Buffer.concat([Buffer.from([0x30, 0x82]), Buffer.alloc(2), certBody]);
686
+ cert.writeUInt16BE(certBody.length, 2);
687
+
688
+ const b64 = cert.toString('base64');
689
+ const lines = b64.match(/.{1,76}/g) || [b64];
690
+ return '-----BEGIN CERTIFICATE-----\n' + lines.join('\n') + '\n-----END CERTIFICATE-----';
691
+ }
692
+
693
+ static createJazoest(input) {
694
+ const buf = Buffer.from(input, 'ascii');
695
+ let sum = 0;
696
+ for (let i = 0; i < buf.byteLength; i++) {
697
+ sum += buf.readUInt8(i);
698
+ }
699
+ return `2${sum}`;
700
+ }
701
+
702
+ encryptPassword(password) {
703
+ if (!this.client.state.passwordEncryptionPubKey) {
704
+ return { time: Math.floor(Date.now() / 1000).toString(), encrypted: password };
705
+ }
706
+
707
+ const randKey = crypto.randomBytes(32);
708
+ const iv = crypto.randomBytes(12);
709
+
710
+ const rsaEncrypted = crypto.publicEncrypt({
711
+ key: Buffer.from(this.client.state.passwordEncryptionPubKey, 'base64').toString(),
712
+ padding: crypto.constants.RSA_PKCS1_PADDING,
713
+ }, randKey);
714
+
715
+ const cipher = crypto.createCipheriv('aes-256-gcm', randKey, iv);
716
+ const time = Math.floor(Date.now() / 1000).toString();
717
+ cipher.setAAD(Buffer.from(time));
718
+
719
+ const aesEncrypted = Buffer.concat([cipher.update(password, 'utf8'), cipher.final()]);
720
+ const sizeBuffer = Buffer.alloc(2, 0);
721
+ sizeBuffer.writeInt16LE(rsaEncrypted.byteLength, 0);
722
+ const authTag = cipher.getAuthTag();
723
+
724
+ return {
725
+ time,
726
+ encrypted: Buffer.concat([
727
+ Buffer.from([1, this.client.state.passwordEncryptionKeyId || 0]),
728
+ iv,
729
+ sizeBuffer,
730
+ rsaEncrypted,
731
+ authTag,
732
+ aesEncrypted
733
+ ]).toString('base64')
734
+ };
735
+ }
736
+
737
+ async passwordPublicKeys() {
738
+ const response = await this.client.request.send({
739
+ method: 'GET',
740
+ url: '/api/v1/qe/sync/',
741
+ });
742
+ const headers = response.headers || {};
743
+ const keyId = parseInt(headers['ig-set-password-encryption-key-id'] || '0');
744
+ const pubKey = headers['ig-set-password-encryption-pub-key'] || '';
745
+ return { keyId, pubKey };
746
+ }
747
+
748
+ async setPresenceDisabled(disabled = true) {
749
+ return this.requestWithRetry(async () => {
750
+ const response = await this.client.request.send({
751
+ method: 'POST',
752
+ url: '/api/v1/accounts/set_presence_disabled/',
753
+ form: this.client.request.sign({
754
+ _uuid: this.client.state.uuid,
755
+ disabled: disabled ? '1' : '0',
756
+ }),
757
+ });
758
+ return response.body;
759
+ });
760
+ }
761
+
762
+ async getCommentFilter() {
763
+ return this.requestWithRetry(async () => {
764
+ const response = await this.client.request.send({
765
+ method: 'GET',
766
+ url: '/api/v1/accounts/get_comment_filter/',
767
+ });
768
+ return response.body;
769
+ });
770
+ }
771
+
772
+ async setCommentFilter(configValue = 0) {
773
+ return this.requestWithRetry(async () => {
774
+ const response = await this.client.request.send({
775
+ method: 'POST',
776
+ url: '/api/v1/accounts/set_comment_filter/',
777
+ form: this.client.request.sign({
778
+ _uuid: this.client.state.uuid,
779
+ config_value: String(configValue),
780
+ }),
781
+ });
782
+ return response.body;
783
+ });
784
+ }
785
+
786
+ async pushPreferences(preferences = 'default') {
787
+ return this.requestWithRetry(async () => {
788
+ const response = await this.client.request.send({
789
+ method: 'POST',
790
+ url: '/api/v1/push/register/',
791
+ form: this.client.request.sign({
792
+ _uuid: this.client.state.uuid,
793
+ device_type: 'android_mqtt',
794
+ is_main_push_channel: 'true',
795
+ phone_id: this.client.state.phoneId,
796
+ device_token: '',
797
+ guid: this.client.state.uuid,
798
+ users: preferences,
799
+ }),
800
+ });
801
+ return response.body;
802
+ });
803
+ }
836
804
  }
837
805
 
838
806
  module.exports = AccountRepository;