You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			550 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			550 lines
		
	
	
		
			18 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global
 | |
|   window,
 | |
|   textsecure,
 | |
|   libsignal,
 | |
|   mnemonic,
 | |
|   btoa,
 | |
|   Signal,
 | |
|   getString,
 | |
|   Event,
 | |
|   dcodeIO,
 | |
|   StringView,
 | |
|   log,
 | |
|   storage,
 | |
|   Event,
 | |
|   ConversationController
 | |
| */
 | |
| 
 | |
| /* eslint-disable more/no-then */
 | |
| /* eslint-disable no-unused-vars */
 | |
| /* eslint-disable no-await-in-loop */
 | |
| 
 | |
| // eslint-disable-next-line func-names
 | |
| (function() {
 | |
|   window.textsecure = window.textsecure || {};
 | |
| 
 | |
|   const ARCHIVE_AGE = 7 * 24 * 60 * 60 * 1000;
 | |
| 
 | |
|   function AccountManager(username, password) {
 | |
|     // this.server = window.WebAPI.connect({ username, password });
 | |
|     this.pending = Promise.resolve();
 | |
|   }
 | |
| 
 | |
|   function getNumber(numberId) {
 | |
|     if (!numberId || !numberId.length) {
 | |
|       return numberId;
 | |
|     }
 | |
| 
 | |
|     const parts = numberId.split('.');
 | |
|     if (!parts.length) {
 | |
|       return numberId;
 | |
|     }
 | |
| 
 | |
|     return parts[0];
 | |
|   }
 | |
| 
 | |
|   AccountManager.prototype = new textsecure.EventTarget();
 | |
|   AccountManager.prototype.extend({
 | |
|     constructor: AccountManager,
 | |
|     requestVoiceVerification(number) {
 | |
|       // return this.server.requestVerificationVoice(number);
 | |
|     },
 | |
|     requestSMSVerification(number) {
 | |
|       // return this.server.requestVerificationSMS(number);
 | |
|     },
 | |
|     async encryptDeviceName(name, providedIdentityKey) {
 | |
|       if (!name) {
 | |
|         return null;
 | |
|       }
 | |
|       const identityKey =
 | |
|         providedIdentityKey ||
 | |
|         (await textsecure.storage.protocol.getIdentityKeyPair());
 | |
|       if (!identityKey) {
 | |
|         throw new Error(
 | |
|           'Identity key was not provided and is not in database!'
 | |
|         );
 | |
|       }
 | |
|       const encrypted = await Signal.Crypto.encryptDeviceName(
 | |
|         name,
 | |
|         identityKey.pubKey
 | |
|       );
 | |
| 
 | |
|       const proto = new textsecure.protobuf.DeviceName();
 | |
|       proto.ephemeralPublic = encrypted.ephemeralPublic;
 | |
|       proto.syntheticIv = encrypted.syntheticIv;
 | |
|       proto.ciphertext = encrypted.ciphertext;
 | |
| 
 | |
|       const arrayBuffer = proto.encode().toArrayBuffer();
 | |
|       return Signal.Crypto.arrayBufferToBase64(arrayBuffer);
 | |
|     },
 | |
|     async decryptDeviceName(base64) {
 | |
|       const identityKey = await textsecure.storage.protocol.getIdentityKeyPair();
 | |
| 
 | |
|       const arrayBuffer = Signal.Crypto.base64ToArrayBuffer(base64);
 | |
|       const proto = textsecure.protobuf.DeviceName.decode(arrayBuffer);
 | |
|       const encrypted = {
 | |
|         ephemeralPublic: proto.ephemeralPublic.toArrayBuffer(),
 | |
|         syntheticIv: proto.syntheticIv.toArrayBuffer(),
 | |
|         ciphertext: proto.ciphertext.toArrayBuffer(),
 | |
|       };
 | |
| 
 | |
|       const name = await Signal.Crypto.decryptDeviceName(
 | |
|         encrypted,
 | |
|         identityKey.privKey
 | |
|       );
 | |
| 
 | |
|       return name;
 | |
|     },
 | |
|     async maybeUpdateDeviceName() {
 | |
|       const isNameEncrypted = textsecure.storage.user.getDeviceNameEncrypted();
 | |
|       if (isNameEncrypted) {
 | |
|         return;
 | |
|       }
 | |
|       const deviceName = await textsecure.storage.user.getDeviceName();
 | |
|       const base64 = await this.encryptDeviceName(deviceName);
 | |
| 
 | |
|       await this.server.updateDeviceName(base64);
 | |
|     },
 | |
|     async deviceNameIsEncrypted() {
 | |
|       await textsecure.storage.user.setDeviceNameEncrypted();
 | |
|     },
 | |
|     async maybeDeleteSignalingKey() {
 | |
|       const key = await textsecure.storage.user.getSignalingKey();
 | |
|       if (key) {
 | |
|         await this.server.removeSignalingKey();
 | |
|       }
 | |
|     },
 | |
|     registerSingleDevice(mnemonic, mnemonicLanguage, profileName) {
 | |
|       const createAccount = this.createAccount.bind(this);
 | |
|       const clearSessionsAndPreKeys = this.clearSessionsAndPreKeys.bind(this);
 | |
|       const generateKeys = this.generateKeys.bind(this, 0);
 | |
|       const confirmKeys = this.confirmKeys.bind(this);
 | |
|       const registrationDone = this.registrationDone.bind(this);
 | |
|       let generateKeypair;
 | |
|       if (mnemonic) {
 | |
|         generateKeypair = () => {
 | |
|           const seedHex = window.mnemonic.mn_decode(mnemonic, mnemonicLanguage);
 | |
|           const privKeyHex = window.mnemonic.sc_reduce32(seedHex);
 | |
|           const privKey = dcodeIO.ByteBuffer.wrap(
 | |
|             privKeyHex,
 | |
|             'hex'
 | |
|           ).toArrayBuffer();
 | |
|           return libsignal.Curve.async.createKeyPair(privKey);
 | |
|         };
 | |
|       } else {
 | |
|         generateKeypair = libsignal.KeyHelper.generateIdentityKeyPair;
 | |
|       }
 | |
|       return this.queueTask(() =>
 | |
|         generateKeypair().then(async identityKeyPair =>
 | |
|           createAccount(identityKeyPair)
 | |
|             .then(() => this.saveMnemonic(mnemonic))
 | |
|             .then(clearSessionsAndPreKeys)
 | |
|             .then(generateKeys)
 | |
|             .then(confirmKeys)
 | |
|             .then(() => {
 | |
|               const pubKeyString = StringView.arrayBufferToHex(
 | |
|                 identityKeyPair.pubKey
 | |
|               );
 | |
|               registrationDone(pubKeyString, profileName);
 | |
|             })
 | |
|         )
 | |
|       );
 | |
|     },
 | |
|     async addMockContact(doSave) {
 | |
|       if (doSave === undefined) {
 | |
|         // eslint-disable-next-line no-param-reassign
 | |
|         doSave = true;
 | |
|       }
 | |
|       const keyPair = await libsignal.KeyHelper.generateIdentityKeyPair();
 | |
|       const pubKey = StringView.arrayBufferToHex(keyPair.pubKey);
 | |
|       const privKey = StringView.arrayBufferToHex(keyPair.privKey);
 | |
|       log.info(`contact pubkey ${pubKey}`);
 | |
|       log.info(`contact privkey ${privKey}`);
 | |
|       const signedKeyId = Math.floor(Math.random() * 1000 + 1);
 | |
| 
 | |
|       const signedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
 | |
|         keyPair,
 | |
|         signedKeyId
 | |
|       );
 | |
|       const contactSignedPreKey = {
 | |
|         publicKey: signedPreKey.keyPair.pubKey,
 | |
|         signature: signedPreKey.signature,
 | |
|         keyId: signedPreKey.keyId,
 | |
|       };
 | |
|       if (doSave) {
 | |
|         await textsecure.storage.protocol.storeContactSignedPreKey(
 | |
|           pubKey,
 | |
|           contactSignedPreKey
 | |
|         );
 | |
|       } else {
 | |
|         log.info(
 | |
|           `signed prekey:
 | |
|           ${StringView.arrayBufferToHex(contactSignedPreKey.publicKey)}`
 | |
|         );
 | |
|         log.info(
 | |
|           `signature:
 | |
|           ${StringView.arrayBufferToHex(contactSignedPreKey.signature)}`
 | |
|         );
 | |
|       }
 | |
| 
 | |
|       for (let keyId = 0; keyId < 10; keyId += 1) {
 | |
|         const preKey = await libsignal.KeyHelper.generatePreKey(keyId);
 | |
|         if (doSave) {
 | |
|           await textsecure.storage.protocol.storeContactPreKey(pubKey, {
 | |
|             publicKey: preKey.keyPair.pubKey,
 | |
|             keyId,
 | |
|           });
 | |
|         } else {
 | |
|           log.info(
 | |
|             `signed prekey:
 | |
|             ${StringView.arrayBufferToHex(preKey.keyPair.pubKey)}`
 | |
|           );
 | |
|         }
 | |
|       }
 | |
|       log.info('Added mock contact');
 | |
|     },
 | |
|     registerSecondDevice(setProvisioningUrl, confirmNumber, progressCallback) {
 | |
|       throw new Error(
 | |
|         'account_manager: registerSecondDevice has not been implemented!'
 | |
|       );
 | |
|     },
 | |
|     refreshPreKeys() {
 | |
|       // const generateKeys = this.generateKeys.bind(this, 0);
 | |
|       // const registerKeys = this.server.registerKeys.bind(this.server);
 | |
|       // return this.queueTask(() =>
 | |
|       //   this.server.getMyKeys().then(preKeyCount => {
 | |
|       //     window.log.info(`prekey count ${preKeyCount}`);
 | |
|       //     if (preKeyCount < 10) {
 | |
|       //       return generateKeys().then(registerKeys);
 | |
|       //     }
 | |
|       //     return null;
 | |
|       //   })
 | |
|       // );
 | |
|     },
 | |
|     rotateSignedPreKey() {
 | |
|       return this.queueTask(() => {
 | |
|         const signedKeyId = textsecure.storage.get('signedKeyId', 1);
 | |
|         if (typeof signedKeyId !== 'number') {
 | |
|           throw new Error('Invalid signedKeyId');
 | |
|         }
 | |
| 
 | |
|         const store = textsecure.storage.protocol;
 | |
|         const { cleanSignedPreKeys } = this;
 | |
| 
 | |
|         return store
 | |
|           .getIdentityKeyPair()
 | |
|           .then(
 | |
|             identityKey =>
 | |
|               libsignal.KeyHelper.generateSignedPreKey(
 | |
|                 identityKey,
 | |
|                 signedKeyId
 | |
|               ),
 | |
|             () => {
 | |
|               // We swallow any error here, because we don't want to get into
 | |
|               //   a loop of repeated retries.
 | |
|               window.log.error(
 | |
|                 'Failed to get identity key. Canceling key rotation.'
 | |
|               );
 | |
|             }
 | |
|           )
 | |
|           .then(res => {
 | |
|             if (!res) {
 | |
|               return null;
 | |
|             }
 | |
|             window.log.info('Saving new signed prekey', res.keyId);
 | |
|             return Promise.all([
 | |
|               textsecure.storage.put('signedKeyId', signedKeyId + 1),
 | |
|               store.storeSignedPreKey(
 | |
|                 res.keyId,
 | |
|                 res.keyPair,
 | |
|                 undefined,
 | |
|                 res.signature
 | |
|               ),
 | |
|             ])
 | |
|               .then(() => {
 | |
|                 const confirmed = true;
 | |
|                 window.log.info('Confirming new signed prekey', res.keyId);
 | |
|                 return Promise.all([
 | |
|                   textsecure.storage.remove('signedKeyRotationRejected'),
 | |
|                   store.storeSignedPreKey(
 | |
|                     res.keyId,
 | |
|                     res.keyPair,
 | |
|                     confirmed,
 | |
|                     res.signature
 | |
|                   ),
 | |
|                 ]);
 | |
|               })
 | |
|               .then(() => cleanSignedPreKeys());
 | |
|           })
 | |
|           .catch(e => {
 | |
|             window.log.error(
 | |
|               'rotateSignedPrekey error:',
 | |
|               e && e.stack ? e.stack : e
 | |
|             );
 | |
| 
 | |
|             if (
 | |
|               e instanceof Error &&
 | |
|               e.name === 'HTTPError' &&
 | |
|               e.code >= 400 &&
 | |
|               e.code <= 599
 | |
|             ) {
 | |
|               const rejections =
 | |
|                 1 + textsecure.storage.get('signedKeyRotationRejected', 0);
 | |
|               textsecure.storage.put('signedKeyRotationRejected', rejections);
 | |
|               window.log.error(
 | |
|                 'Signed key rotation rejected count:',
 | |
|                 rejections
 | |
|               );
 | |
|             } else {
 | |
|               throw e;
 | |
|             }
 | |
|           });
 | |
|       });
 | |
|     },
 | |
|     queueTask(task) {
 | |
|       const taskWithTimeout = textsecure.createTaskWithTimeout(task);
 | |
|       this.pending = this.pending.then(taskWithTimeout, taskWithTimeout);
 | |
| 
 | |
|       return this.pending;
 | |
|     },
 | |
|     cleanSignedPreKeys() {
 | |
|       const MINIMUM_KEYS = 3;
 | |
|       const store = textsecure.storage.protocol;
 | |
|       return store.loadSignedPreKeys().then(allKeys => {
 | |
|         allKeys.sort((a, b) => (a.created_at || 0) - (b.created_at || 0));
 | |
|         allKeys.reverse(); // we want the most recent first
 | |
|         let confirmed = allKeys.filter(key => key.confirmed);
 | |
|         const unconfirmed = allKeys.filter(key => !key.confirmed);
 | |
| 
 | |
|         const recent = allKeys[0] ? allKeys[0].keyId : 'none';
 | |
|         const recentConfirmed = confirmed[0] ? confirmed[0].keyId : 'none';
 | |
|         window.log.info(`Most recent signed key: ${recent}`);
 | |
|         window.log.info(`Most recent confirmed signed key: ${recentConfirmed}`);
 | |
|         window.log.info(
 | |
|           'Total signed key count:',
 | |
|           allKeys.length,
 | |
|           '-',
 | |
|           confirmed.length,
 | |
|           'confirmed'
 | |
|         );
 | |
| 
 | |
|         let confirmedCount = confirmed.length;
 | |
| 
 | |
|         // Keep MINIMUM_KEYS confirmed keys, then drop if older than a week
 | |
|         confirmed = confirmed.forEach((key, index) => {
 | |
|           if (index < MINIMUM_KEYS) {
 | |
|             return;
 | |
|           }
 | |
|           const createdAt = key.created_at || 0;
 | |
|           const age = Date.now() - createdAt;
 | |
| 
 | |
|           if (age > ARCHIVE_AGE) {
 | |
|             window.log.info(
 | |
|               'Removing confirmed signed prekey:',
 | |
|               key.keyId,
 | |
|               'with timestamp:',
 | |
|               createdAt
 | |
|             );
 | |
|             store.removeSignedPreKey(key.keyId);
 | |
|             confirmedCount -= 1;
 | |
|           }
 | |
|         });
 | |
| 
 | |
|         const stillNeeded = MINIMUM_KEYS - confirmedCount;
 | |
| 
 | |
|         // If we still don't have enough total keys, we keep as many unconfirmed
 | |
|         // keys as necessary. If not necessary, and over a week old, we drop.
 | |
|         unconfirmed.forEach((key, index) => {
 | |
|           if (index < stillNeeded) {
 | |
|             return;
 | |
|           }
 | |
| 
 | |
|           const createdAt = key.created_at || 0;
 | |
|           const age = Date.now() - createdAt;
 | |
|           if (age > ARCHIVE_AGE) {
 | |
|             window.log.info(
 | |
|               'Removing unconfirmed signed prekey:',
 | |
|               key.keyId,
 | |
|               'with timestamp:',
 | |
|               createdAt
 | |
|             );
 | |
|             store.removeSignedPreKey(key.keyId);
 | |
|           }
 | |
|         });
 | |
|       });
 | |
|     },
 | |
|     async createAccount(identityKeyPair, userAgent, readReceipts) {
 | |
|       const signalingKey = libsignal.crypto.getRandomBytes(32 + 20);
 | |
|       let password = btoa(getString(libsignal.crypto.getRandomBytes(16)));
 | |
|       password = password.substring(0, password.length - 2);
 | |
|       const registrationId = libsignal.KeyHelper.generateRegistrationId();
 | |
| 
 | |
|       await Promise.all([
 | |
|         textsecure.storage.remove('identityKey'),
 | |
|         textsecure.storage.remove('signaling_key'),
 | |
|         textsecure.storage.remove('password'),
 | |
|         textsecure.storage.remove('registrationId'),
 | |
|         textsecure.storage.remove('number_id'),
 | |
|         textsecure.storage.remove('device_name'),
 | |
|         textsecure.storage.remove('userAgent'),
 | |
|         textsecure.storage.remove('read-receipt-setting'),
 | |
|         textsecure.storage.remove('typing-indicators-setting'),
 | |
|         textsecure.storage.remove('regionCode'),
 | |
|       ]);
 | |
| 
 | |
|       // update our own identity key, which may have changed
 | |
|       // if we're relinking after a reinstall on the master device
 | |
|       const pubKeyString = StringView.arrayBufferToHex(identityKeyPair.pubKey);
 | |
|       await textsecure.storage.protocol.saveIdentityWithAttributes(
 | |
|         pubKeyString,
 | |
|         {
 | |
|           id: pubKeyString,
 | |
|           publicKey: identityKeyPair.pubKey,
 | |
|           firstUse: true,
 | |
|           timestamp: Date.now(),
 | |
|           verified: textsecure.storage.protocol.VerifiedStatus.VERIFIED,
 | |
|           nonblockingApproval: true,
 | |
|         }
 | |
|       );
 | |
| 
 | |
|       await textsecure.storage.put('identityKey', identityKeyPair);
 | |
|       await textsecure.storage.put('signaling_key', signalingKey);
 | |
|       await textsecure.storage.put('password', password);
 | |
|       await textsecure.storage.put('registrationId', registrationId);
 | |
|       if (userAgent) {
 | |
|         await textsecure.storage.put('userAgent', userAgent);
 | |
|       }
 | |
| 
 | |
|       await textsecure.storage.put(
 | |
|         'read-receipt-setting',
 | |
|         Boolean(readReceipts)
 | |
|       );
 | |
| 
 | |
|       // Enable typing indicators by default
 | |
|       await textsecure.storage.put('typing-indicators-setting', Boolean(true));
 | |
| 
 | |
|       await textsecure.storage.user.setNumberAndDeviceId(pubKeyString, 1);
 | |
|       await textsecure.storage.put('regionCode', null);
 | |
|     },
 | |
|     async clearSessionsAndPreKeys() {
 | |
|       const store = textsecure.storage.protocol;
 | |
| 
 | |
|       window.log.info('clearing all sessions, prekeys, and signed prekeys');
 | |
|       await Promise.all([
 | |
|         store.clearPreKeyStore(),
 | |
|         store.clearContactPreKeysStore(),
 | |
|         store.clearSignedPreKeysStore(),
 | |
|         store.clearContactSignedPreKeysStore(),
 | |
|         store.clearSessionStore(),
 | |
|       ]);
 | |
|     },
 | |
|     // Takes the same object returned by generateKeys
 | |
|     async confirmKeys(keys) {
 | |
|       const store = textsecure.storage.protocol;
 | |
|       const key = keys.signedPreKey;
 | |
|       const confirmed = true;
 | |
| 
 | |
|       window.log.info('confirmKeys: confirming key', key.keyId);
 | |
|       await store.storeSignedPreKey(
 | |
|         key.keyId,
 | |
|         key.keyPair,
 | |
|         confirmed,
 | |
|         key.signature
 | |
|       );
 | |
|     },
 | |
|     generateKeys(count, providedProgressCallback) {
 | |
|       const progressCallback =
 | |
|         typeof providedProgressCallback === 'function'
 | |
|           ? providedProgressCallback
 | |
|           : null;
 | |
|       const startId = textsecure.storage.get('maxPreKeyId', 1);
 | |
|       const signedKeyId = textsecure.storage.get('signedKeyId', 1);
 | |
| 
 | |
|       if (typeof startId !== 'number') {
 | |
|         throw new Error('Invalid maxPreKeyId');
 | |
|       }
 | |
|       if (typeof signedKeyId !== 'number') {
 | |
|         throw new Error('Invalid signedKeyId');
 | |
|       }
 | |
| 
 | |
|       const store = textsecure.storage.protocol;
 | |
|       return store.getIdentityKeyPair().then(identityKey => {
 | |
|         const result = { preKeys: [], identityKey: identityKey.pubKey };
 | |
|         const promises = [];
 | |
| 
 | |
|         for (let keyId = startId; keyId < startId + count; keyId += 1) {
 | |
|           promises.push(
 | |
|             libsignal.KeyHelper.generatePreKey(keyId).then(res => {
 | |
|               store.storePreKey(res.keyId, res.keyPair);
 | |
|               result.preKeys.push({
 | |
|                 keyId: res.keyId,
 | |
|                 publicKey: res.keyPair.pubKey,
 | |
|               });
 | |
|               if (progressCallback) {
 | |
|                 progressCallback();
 | |
|               }
 | |
|             })
 | |
|           );
 | |
|         }
 | |
| 
 | |
|         promises.push(
 | |
|           libsignal.KeyHelper.generateSignedPreKey(
 | |
|             identityKey,
 | |
|             signedKeyId
 | |
|           ).then(res => {
 | |
|             store.storeSignedPreKey(
 | |
|               res.keyId,
 | |
|               res.keyPair,
 | |
|               undefined,
 | |
|               res.signature
 | |
|             );
 | |
|             result.signedPreKey = {
 | |
|               keyId: res.keyId,
 | |
|               publicKey: res.keyPair.pubKey,
 | |
|               signature: res.signature,
 | |
|               // server.registerKeys doesn't use keyPair, confirmKeys does
 | |
|               keyPair: res.keyPair,
 | |
|             };
 | |
|           })
 | |
|         );
 | |
| 
 | |
|         textsecure.storage.put('maxPreKeyId', startId + count);
 | |
|         textsecure.storage.put('signedKeyId', signedKeyId + 1);
 | |
|         return Promise.all(promises).then(() =>
 | |
|           // This is primarily for the signed prekey summary it logs out
 | |
|           this.cleanSignedPreKeys().then(() => result)
 | |
|         );
 | |
|       });
 | |
|     },
 | |
|     async generateMnemonic(language = 'english') {
 | |
|       const keys = await libsignal.KeyHelper.generateIdentityKeyPair();
 | |
|       const hex = StringView.arrayBufferToHex(keys.privKey);
 | |
|       return mnemonic.mn_encode(hex, language);
 | |
|     },
 | |
|     getCurrentMnemonic() {
 | |
|       return textsecure.storage.get('mnemonic');
 | |
|     },
 | |
|     saveMnemonic(mnemonic) {
 | |
|       return textsecure.storage.put('mnemonic', mnemonic);
 | |
|     },
 | |
|     async registrationDone(number, profileName) {
 | |
|       window.log.info('registration done');
 | |
| 
 | |
|       // Ensure that we always have a conversation for ourself
 | |
|       const conversation = await ConversationController.getOrCreateAndWait(
 | |
|         number,
 | |
|         'private'
 | |
|       );
 | |
| 
 | |
|       await storage.setProfileName(profileName);
 | |
| 
 | |
|       // Update the conversation if we have it
 | |
|       const newProfile = storage.getLocalProfile();
 | |
|       await conversation.setProfile(newProfile);
 | |
| 
 | |
|       this.dispatchEvent(new Event('registration'));
 | |
|     },
 | |
|   });
 | |
|   textsecure.AccountManager = AccountManager;
 | |
| })();
 |