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.
		
		
		
		
		
			
		
			
				
	
	
		
			388 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			388 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global window, libsignal, textsecure, Signal,
 | |
|    lokiFileServerAPI, ConversationController */
 | |
| 
 | |
| // eslint-disable-next-line func-names
 | |
| (function() {
 | |
|   window.libloki = window.libloki || {};
 | |
| 
 | |
|   const timers = {};
 | |
|   const REFRESH_DELAY = 60 * 1000;
 | |
| 
 | |
|   async function getPreKeyBundleForContact(pubKey) {
 | |
|     const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
 | |
|     const identityKey = myKeyPair.pubKey;
 | |
| 
 | |
|     // Retrieve ids. The ids stored are always the latest generated + 1
 | |
|     const signedKeyId = textsecure.storage.get('signedKeyId', 2) - 1;
 | |
| 
 | |
|     const [signedKey, preKey] = await Promise.all([
 | |
|       textsecure.storage.protocol.loadSignedPreKey(signedKeyId),
 | |
|       new Promise(async resolve => {
 | |
|         // retrieve existing prekey if we already generated one for that recipient
 | |
|         const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
 | |
|           pubKey
 | |
|         );
 | |
|         if (storedPreKey) {
 | |
|           resolve({ pubKey: storedPreKey.pubKey, keyId: storedPreKey.keyId });
 | |
|         } else {
 | |
|           // generate and store new prekey
 | |
|           const preKeyId = textsecure.storage.get('maxPreKeyId', 1);
 | |
|           textsecure.storage.put('maxPreKeyId', preKeyId + 1);
 | |
|           const newPreKey = await libsignal.KeyHelper.generatePreKey(preKeyId);
 | |
|           await textsecure.storage.protocol.storePreKey(
 | |
|             newPreKey.keyId,
 | |
|             newPreKey.keyPair,
 | |
|             pubKey
 | |
|           );
 | |
|           resolve({ pubKey: newPreKey.keyPair.pubKey, keyId: preKeyId });
 | |
|         }
 | |
|       }),
 | |
|     ]);
 | |
| 
 | |
|     return {
 | |
|       identityKey: new Uint8Array(identityKey),
 | |
|       deviceId: 1, // TODO: fetch from somewhere
 | |
|       preKeyId: preKey.keyId,
 | |
|       signedKeyId,
 | |
|       preKey: new Uint8Array(preKey.pubKey),
 | |
|       signedKey: new Uint8Array(signedKey.pubKey),
 | |
|       signature: new Uint8Array(signedKey.signature),
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   async function saveContactPreKeyBundle({
 | |
|     pubKey,
 | |
|     preKeyId,
 | |
|     preKey,
 | |
|     signedKeyId,
 | |
|     signedKey,
 | |
|     signature,
 | |
|   }) {
 | |
|     const signedPreKey = {
 | |
|       keyId: signedKeyId,
 | |
|       publicKey: signedKey,
 | |
|       signature,
 | |
|     };
 | |
| 
 | |
|     const signedKeyPromise = textsecure.storage.protocol.storeContactSignedPreKey(
 | |
|       pubKey,
 | |
|       signedPreKey
 | |
|     );
 | |
| 
 | |
|     const preKeyObject = {
 | |
|       publicKey: preKey,
 | |
|       keyId: preKeyId,
 | |
|     };
 | |
| 
 | |
|     const preKeyPromise = textsecure.storage.protocol.storeContactPreKey(
 | |
|       pubKey,
 | |
|       preKeyObject
 | |
|     );
 | |
| 
 | |
|     await Promise.all([signedKeyPromise, preKeyPromise]);
 | |
|   }
 | |
| 
 | |
|   async function removeContactPreKeyBundle(pubKey) {
 | |
|     await Promise.all([
 | |
|       textsecure.storage.protocol.removeContactPreKey(pubKey),
 | |
|       textsecure.storage.protocol.removeContactSignedPreKey(pubKey),
 | |
|     ]);
 | |
|   }
 | |
| 
 | |
|   async function verifyFriendRequestAcceptPreKey(pubKey, buffer) {
 | |
|     const storedPreKey = await textsecure.storage.protocol.loadPreKeyForContact(
 | |
|       pubKey
 | |
|     );
 | |
|     if (!storedPreKey) {
 | |
|       throw new Error(
 | |
|         'Received a friend request from a pubkey for which no prekey bundle was created'
 | |
|       );
 | |
|     }
 | |
|     // need to pop the version
 | |
|     // eslint-disable-next-line no-unused-vars
 | |
|     const version = buffer.readUint8();
 | |
|     const preKeyProto = window.textsecure.protobuf.PreKeyWhisperMessage.decode(
 | |
|       buffer
 | |
|     );
 | |
|     if (!preKeyProto) {
 | |
|       throw new Error(
 | |
|         'Could not decode PreKeyWhisperMessage while attempting to match the preKeyId'
 | |
|       );
 | |
|     }
 | |
|     const { preKeyId } = preKeyProto;
 | |
|     if (storedPreKey.keyId !== preKeyId) {
 | |
|       throw new Error(
 | |
|         'Received a preKeyWhisperMessage (friend request accept) from an unknown source'
 | |
|       );
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   // fetches device mappings from server.
 | |
|   async function getPrimaryDeviceMapping(pubKey) {
 | |
|     if (typeof lokiFileServerAPI === 'undefined') {
 | |
|       // If this is not defined then we are initiating a pairing
 | |
|       return [];
 | |
|     }
 | |
|     const deviceMapping = await lokiFileServerAPI.getUserDeviceMapping(pubKey);
 | |
|     if (!deviceMapping) {
 | |
|       return [];
 | |
|     }
 | |
|     let authorisations = deviceMapping.authorisations || [];
 | |
|     if (deviceMapping.isPrimary === '0') {
 | |
|       const { primaryDevicePubKey } =
 | |
|         authorisations.find(
 | |
|           authorisation =>
 | |
|             authorisation && authorisation.secondaryDevicePubKey === pubKey
 | |
|         ) || {};
 | |
|       if (primaryDevicePubKey) {
 | |
|         // do NOT call getprimaryDeviceMapping recursively
 | |
|         // in case both devices are out of sync and think they are
 | |
|         // each others' secondary pubkey.
 | |
|         const primaryDeviceMapping = await lokiFileServerAPI.getUserDeviceMapping(
 | |
|           primaryDevicePubKey
 | |
|         );
 | |
|         if (!primaryDeviceMapping) {
 | |
|           return [];
 | |
|         }
 | |
|         ({ authorisations } = primaryDeviceMapping);
 | |
|       }
 | |
|     }
 | |
|     return authorisations || [];
 | |
|   }
 | |
| 
 | |
|   // if the device is a secondary device,
 | |
|   // fetch the device mappings for its primary device
 | |
|   async function saveAllPairingAuthorisationsFor(pubKey) {
 | |
|     // Will be false if there is no timer
 | |
|     const cacheValid = timers[pubKey] > Date.now();
 | |
|     if (cacheValid) {
 | |
|       return;
 | |
|     }
 | |
|     timers[pubKey] = Date.now() + REFRESH_DELAY;
 | |
|     const authorisations = await getPrimaryDeviceMapping(pubKey);
 | |
|     await Promise.all(
 | |
|       authorisations.map(authorisation =>
 | |
|         savePairingAuthorisation(authorisation)
 | |
|       )
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   async function savePairingAuthorisation(authorisation) {
 | |
|     // Ensure that we have a conversation for all the devices
 | |
|     const conversation = await ConversationController.getOrCreateAndWait(
 | |
|       authorisation.secondaryDevicePubKey,
 | |
|       'private'
 | |
|     );
 | |
|     await conversation.setSecondaryStatus(
 | |
|       true,
 | |
|       authorisation.primaryDevicePubKey
 | |
|     );
 | |
|     await window.Signal.Data.createOrUpdatePairingAuthorisation(authorisation);
 | |
|   }
 | |
| 
 | |
|   function removePairingAuthorisationForSecondaryPubKey(pubKey) {
 | |
|     return window.Signal.Data.removePairingAuthorisationForSecondaryPubKey(
 | |
|       pubKey
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   // Transforms signatures from base64 to ArrayBuffer!
 | |
|   async function getGrantAuthorisationForSecondaryPubKey(secondaryPubKey) {
 | |
|     const conversation = ConversationController.get(secondaryPubKey);
 | |
|     if (!conversation || conversation.isPublic() || conversation.isRss()) {
 | |
|       return null;
 | |
|     }
 | |
|     await saveAllPairingAuthorisationsFor(secondaryPubKey);
 | |
|     const authorisation = await window.Signal.Data.getGrantAuthorisationForSecondaryPubKey(
 | |
|       secondaryPubKey
 | |
|     );
 | |
|     if (!authorisation) {
 | |
|       return null;
 | |
|     }
 | |
|     return {
 | |
|       ...authorisation,
 | |
|       requestSignature: Signal.Crypto.base64ToArrayBuffer(
 | |
|         authorisation.requestSignature
 | |
|       ),
 | |
|       grantSignature: Signal.Crypto.base64ToArrayBuffer(
 | |
|         authorisation.grantSignature
 | |
|       ),
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   // Transforms signatures from base64 to ArrayBuffer!
 | |
|   async function getAuthorisationForSecondaryPubKey(secondaryPubKey) {
 | |
|     await saveAllPairingAuthorisationsFor(secondaryPubKey);
 | |
|     const authorisation = await window.Signal.Data.getAuthorisationForSecondaryPubKey(
 | |
|       secondaryPubKey
 | |
|     );
 | |
|     if (!authorisation) {
 | |
|       return null;
 | |
|     }
 | |
|     return {
 | |
|       ...authorisation,
 | |
|       requestSignature: Signal.Crypto.base64ToArrayBuffer(
 | |
|         authorisation.requestSignature
 | |
|       ),
 | |
|       grantSignature: authorisation.grantSignature
 | |
|         ? Signal.Crypto.base64ToArrayBuffer(authorisation.grantSignature)
 | |
|         : null,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   function getSecondaryDevicesFor(primaryDevicePubKey) {
 | |
|     return window.Signal.Data.getSecondaryDevicesFor(primaryDevicePubKey);
 | |
|   }
 | |
| 
 | |
|   async function getAllDevicePubKeysForPrimaryPubKey(primaryDevicePubKey) {
 | |
|     await saveAllPairingAuthorisationsFor(primaryDevicePubKey);
 | |
|     const secondaryPubKeys =
 | |
|       (await getSecondaryDevicesFor(primaryDevicePubKey)) || [];
 | |
|     return secondaryPubKeys.concat(primaryDevicePubKey);
 | |
|   }
 | |
| 
 | |
|   function getPairedDevicesFor(pubkey) {
 | |
|     return window.Signal.Data.getPairedDevicesFor(pubkey);
 | |
|   }
 | |
| 
 | |
|   window.libloki.storage = {
 | |
|     getPreKeyBundleForContact,
 | |
|     saveContactPreKeyBundle,
 | |
|     removeContactPreKeyBundle,
 | |
|     verifyFriendRequestAcceptPreKey,
 | |
|     savePairingAuthorisation,
 | |
|     saveAllPairingAuthorisationsFor,
 | |
|     removePairingAuthorisationForSecondaryPubKey,
 | |
|     getGrantAuthorisationForSecondaryPubKey,
 | |
|     getAuthorisationForSecondaryPubKey,
 | |
|     getPairedDevicesFor,
 | |
|     getAllDevicePubKeysForPrimaryPubKey,
 | |
|     getSecondaryDevicesFor,
 | |
|     getPrimaryDeviceMapping,
 | |
|   };
 | |
| 
 | |
|   // Libloki protocol store
 | |
| 
 | |
|   const store = window.SignalProtocolStore.prototype;
 | |
| 
 | |
|   store.storeContactPreKey = async (pubKey, preKey) => {
 | |
|     const key = {
 | |
|       // id: (autoincrement)
 | |
|       identityKeyString: pubKey,
 | |
|       publicKey: preKey.publicKey,
 | |
|       keyId: preKey.keyId,
 | |
|     };
 | |
| 
 | |
|     await window.Signal.Data.createOrUpdateContactPreKey(key);
 | |
|   };
 | |
| 
 | |
|   store.loadContactPreKey = async pubKey => {
 | |
|     const preKey = await window.Signal.Data.getContactPreKeyByIdentityKey(
 | |
|       pubKey
 | |
|     );
 | |
|     if (preKey) {
 | |
|       return {
 | |
|         id: preKey.id,
 | |
|         keyId: preKey.keyId,
 | |
|         publicKey: preKey.publicKey,
 | |
|         identityKeyString: preKey.identityKeyString,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     window.log.warn('Failed to fetch contact prekey:', pubKey);
 | |
|     return undefined;
 | |
|   };
 | |
| 
 | |
|   store.loadContactPreKeys = async filters => {
 | |
|     const { keyId, identityKeyString } = filters;
 | |
|     const keys = await window.Signal.Data.getContactPreKeys(
 | |
|       keyId,
 | |
|       identityKeyString
 | |
|     );
 | |
|     if (keys) {
 | |
|       return keys.map(preKey => ({
 | |
|         id: preKey.id,
 | |
|         keyId: preKey.keyId,
 | |
|         publicKey: preKey.publicKey,
 | |
|         identityKeyString: preKey.identityKeyString,
 | |
|       }));
 | |
|     }
 | |
| 
 | |
|     window.log.warn('Failed to fetch signed prekey with filters', filters);
 | |
|     return undefined;
 | |
|   };
 | |
| 
 | |
|   store.removeContactPreKey = async pubKey => {
 | |
|     await window.Signal.Data.removeContactPreKeyByIdentityKey(pubKey);
 | |
|   };
 | |
| 
 | |
|   store.clearContactPreKeysStore = async () => {
 | |
|     await window.Signal.Data.removeAllContactPreKeys();
 | |
|   };
 | |
| 
 | |
|   store.storeContactSignedPreKey = async (pubKey, signedPreKey) => {
 | |
|     const key = {
 | |
|       // id: (autoincrement)
 | |
|       identityKeyString: pubKey,
 | |
|       keyId: signedPreKey.keyId,
 | |
|       publicKey: signedPreKey.publicKey,
 | |
|       signature: signedPreKey.signature,
 | |
|       created_at: Date.now(),
 | |
|       confirmed: false,
 | |
|     };
 | |
|     await window.Signal.Data.createOrUpdateContactSignedPreKey(key);
 | |
|   };
 | |
| 
 | |
|   store.loadContactSignedPreKey = async pubKey => {
 | |
|     const preKey = await window.Signal.Data.getContactSignedPreKeyByIdentityKey(
 | |
|       pubKey
 | |
|     );
 | |
|     if (preKey) {
 | |
|       return {
 | |
|         id: preKey.id,
 | |
|         identityKeyString: preKey.identityKeyString,
 | |
|         publicKey: preKey.publicKey,
 | |
|         signature: preKey.signature,
 | |
|         created_at: preKey.created_at,
 | |
|         keyId: preKey.keyId,
 | |
|         confirmed: preKey.confirmed,
 | |
|       };
 | |
|     }
 | |
|     window.log.warn('Failed to fetch contact signed prekey:', pubKey);
 | |
|     return undefined;
 | |
|   };
 | |
| 
 | |
|   store.loadContactSignedPreKeys = async filters => {
 | |
|     const { keyId, identityKeyString } = filters;
 | |
|     const keys = await window.Signal.Data.getContactSignedPreKeys(
 | |
|       keyId,
 | |
|       identityKeyString
 | |
|     );
 | |
|     if (keys) {
 | |
|       return keys.map(preKey => ({
 | |
|         id: preKey.id,
 | |
|         identityKeyString: preKey.identityKeyString,
 | |
|         publicKey: preKey.publicKey,
 | |
|         signature: preKey.signature,
 | |
|         created_at: preKey.created_at,
 | |
|         keyId: preKey.keyId,
 | |
|         confirmed: preKey.confirmed,
 | |
|       }));
 | |
|     }
 | |
| 
 | |
|     window.log.warn(
 | |
|       'Failed to fetch contact signed prekey with filters',
 | |
|       filters
 | |
|     );
 | |
|     return undefined;
 | |
|   };
 | |
| 
 | |
|   store.removeContactSignedPreKey = async pubKey => {
 | |
|     await window.Signal.Data.removeContactSignedPreKeyByIdentityKey(pubKey);
 | |
|   };
 | |
| 
 | |
|   store.clearContactSignedPreKeysStore = async () => {
 | |
|     await window.Signal.Data.removeAllContactSignedPreKeys();
 | |
|   };
 | |
| })();
 |