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.
		
		
		
		
		
			
		
			
				
	
	
		
			325 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			325 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global
 | |
|   window,
 | |
|   libsignal,
 | |
|   textsecure,
 | |
|   StringView,
 | |
|   Multibase,
 | |
|   TextEncoder,
 | |
|   TextDecoder,
 | |
|   crypto,
 | |
|   dcodeIO,
 | |
|   libloki
 | |
| */
 | |
| 
 | |
| // eslint-disable-next-line func-names
 | |
| (function() {
 | |
|   window.libloki = window.libloki || {};
 | |
| 
 | |
|   const IV_LENGTH = 16;
 | |
|   const NONCE_LENGTH = 12;
 | |
| 
 | |
|   async function DHEncrypt(symmetricKey, plainText) {
 | |
|     const iv = libsignal.crypto.getRandomBytes(IV_LENGTH);
 | |
|     const ciphertext = await libsignal.crypto.encrypt(
 | |
|       symmetricKey,
 | |
|       plainText,
 | |
|       iv
 | |
|     );
 | |
|     const ivAndCiphertext = new Uint8Array(
 | |
|       iv.byteLength + ciphertext.byteLength
 | |
|     );
 | |
|     ivAndCiphertext.set(new Uint8Array(iv));
 | |
|     ivAndCiphertext.set(new Uint8Array(ciphertext), iv.byteLength);
 | |
|     return ivAndCiphertext;
 | |
|   }
 | |
| 
 | |
|   async function deriveSymmetricKey(pubkey, seckey) {
 | |
|     const ephemeralSecret = await libsignal.Curve.async.calculateAgreement(
 | |
|       pubkey,
 | |
|       seckey
 | |
|     );
 | |
| 
 | |
|     const salt = window.Signal.Crypto.bytesFromString('LOKI');
 | |
| 
 | |
|     const key = await crypto.subtle.importKey(
 | |
|       'raw',
 | |
|       salt,
 | |
|       { name: 'HMAC', hash: { name: 'SHA-256' } },
 | |
|       false,
 | |
|       ['sign']
 | |
|     );
 | |
|     const symmetricKey = await crypto.subtle.sign(
 | |
|       { name: 'HMAC', hash: 'SHA-256' },
 | |
|       key,
 | |
|       ephemeralSecret
 | |
|     );
 | |
| 
 | |
|     return symmetricKey;
 | |
|   }
 | |
| 
 | |
|   async function encryptForPubkey(pubkeyX25519, payloadBytes) {
 | |
|     const ephemeral = await libloki.crypto.generateEphemeralKeyPair();
 | |
| 
 | |
|     const snPubkey = StringView.hexToArrayBuffer(pubkeyX25519);
 | |
| 
 | |
|     const symmetricKey = await deriveSymmetricKey(snPubkey, ephemeral.privKey);
 | |
| 
 | |
|     const ciphertext = await EncryptGCM(symmetricKey, payloadBytes);
 | |
| 
 | |
|     return { ciphertext, symmetricKey, ephemeralKey: ephemeral.pubKey };
 | |
|   }
 | |
| 
 | |
|   async function decryptForPubkey(seckeyX25519, ephemKey, ciphertext) {
 | |
|     const symmetricKey = await deriveSymmetricKey(ephemKey, seckeyX25519);
 | |
| 
 | |
|     const plaintext = await DecryptGCM(symmetricKey, ciphertext);
 | |
| 
 | |
|     return plaintext;
 | |
|   }
 | |
| 
 | |
|   async function EncryptGCM(symmetricKey, plaintext) {
 | |
|     const nonce = crypto.getRandomValues(new Uint8Array(NONCE_LENGTH));
 | |
| 
 | |
|     const key = await crypto.subtle.importKey(
 | |
|       'raw',
 | |
|       symmetricKey,
 | |
|       { name: 'AES-GCM' },
 | |
|       false,
 | |
|       ['encrypt']
 | |
|     );
 | |
| 
 | |
|     const ciphertext = await crypto.subtle.encrypt(
 | |
|       { name: 'AES-GCM', iv: nonce, tagLength: 128 },
 | |
|       key,
 | |
|       plaintext
 | |
|     );
 | |
| 
 | |
|     const ivAndCiphertext = new Uint8Array(
 | |
|       NONCE_LENGTH + ciphertext.byteLength
 | |
|     );
 | |
| 
 | |
|     ivAndCiphertext.set(nonce);
 | |
|     ivAndCiphertext.set(new Uint8Array(ciphertext), nonce.byteLength);
 | |
| 
 | |
|     return ivAndCiphertext;
 | |
|   }
 | |
| 
 | |
|   async function DecryptGCM(symmetricKey, ivAndCiphertext) {
 | |
|     const nonce = ivAndCiphertext.slice(0, NONCE_LENGTH);
 | |
|     const ciphertext = ivAndCiphertext.slice(NONCE_LENGTH);
 | |
| 
 | |
|     const key = await crypto.subtle.importKey(
 | |
|       'raw',
 | |
|       symmetricKey,
 | |
|       { name: 'AES-GCM' },
 | |
|       false,
 | |
|       ['decrypt']
 | |
|     );
 | |
| 
 | |
|     return crypto.subtle.decrypt(
 | |
|       { name: 'AES-GCM', iv: nonce },
 | |
|       key,
 | |
|       ciphertext
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   async function DHDecrypt(symmetricKey, ivAndCiphertext) {
 | |
|     const iv = ivAndCiphertext.slice(0, IV_LENGTH);
 | |
|     const ciphertext = ivAndCiphertext.slice(IV_LENGTH);
 | |
|     return libsignal.crypto.decrypt(symmetricKey, ciphertext, iv);
 | |
|   }
 | |
| 
 | |
|   const base32zIndex = Multibase.names.indexOf('base32z');
 | |
|   const base32zCode = Multibase.codes[base32zIndex];
 | |
| 
 | |
|   function decodeSnodeAddressToPubKey(snodeAddress) {
 | |
|     const snodeAddressClean = snodeAddress
 | |
|       .replace('.snode', '')
 | |
|       .replace('https://', '')
 | |
|       .replace('http://', '');
 | |
|     return Multibase.decode(`${base32zCode}${snodeAddressClean}`);
 | |
|   }
 | |
| 
 | |
|   async function generateEphemeralKeyPair() {
 | |
|     const keys = await libsignal.Curve.async.generateKeyPair();
 | |
|     // Signal protocol prepends with "0x05"
 | |
|     keys.pubKey = keys.pubKey.slice(1);
 | |
|     return keys;
 | |
|   }
 | |
| 
 | |
|   async function generateSignatureForPairing(secondaryPubKey, type) {
 | |
|     const pubKeyArrayBuffer = StringView.hexToArrayBuffer(secondaryPubKey);
 | |
|     // Make sure the signature includes the pairing action (pairing or unpairing)
 | |
|     const len = pubKeyArrayBuffer.byteLength;
 | |
|     const data = new Uint8Array(len + 1);
 | |
|     data.set(new Uint8Array(pubKeyArrayBuffer), 0);
 | |
|     data[len] = type;
 | |
| 
 | |
|     const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
 | |
|     if (!myKeyPair) {
 | |
|       throw new Error('Failed to get keypair for pairing signature generation');
 | |
|     }
 | |
|     const signature = await libsignal.Curve.async.calculateSignature(
 | |
|       myKeyPair.privKey,
 | |
|       data.buffer
 | |
|     );
 | |
|     return signature;
 | |
|   }
 | |
| 
 | |
|   async function verifyAuthorisation(authorisation) {
 | |
|     const {
 | |
|       primaryDevicePubKey,
 | |
|       secondaryDevicePubKey,
 | |
|       requestSignature,
 | |
|       grantSignature,
 | |
|     } = authorisation;
 | |
|     const isGrant = !!(grantSignature && grantSignature.length > 0);
 | |
|     if (!primaryDevicePubKey || !secondaryDevicePubKey) {
 | |
|       window.log.warn(
 | |
|         'Received a pairing request with missing pubkeys. Ignored.'
 | |
|       );
 | |
|       return false;
 | |
|     } else if (!requestSignature) {
 | |
|       window.log.warn(
 | |
|         'Received a pairing request with missing request signature. Ignored.'
 | |
|       );
 | |
|       return false;
 | |
|     }
 | |
|     const verify = async (signature, signatureType) => {
 | |
|       const encoding = typeof signature === 'string' ? 'base64' : undefined;
 | |
|       await this.verifyPairingSignature(
 | |
|         primaryDevicePubKey,
 | |
|         secondaryDevicePubKey,
 | |
|         dcodeIO.ByteBuffer.wrap(signature, encoding).toArrayBuffer(),
 | |
|         signatureType
 | |
|       );
 | |
|     };
 | |
|     try {
 | |
|       await verify(requestSignature, PairingType.REQUEST);
 | |
|     } catch (e) {
 | |
|       window.log.warn(
 | |
|         'Could not verify pairing request authorisation signature. Ignoring message.'
 | |
|       );
 | |
|       window.log.error(e);
 | |
|       return false;
 | |
|     }
 | |
|     // can't have grant without requestSignature?
 | |
|     if (isGrant) {
 | |
|       try {
 | |
|         await verify(grantSignature, PairingType.GRANT);
 | |
|       } catch (e) {
 | |
|         window.log.warn(
 | |
|           'Could not verify pairing grant authorisation signature. Ignoring message.'
 | |
|         );
 | |
|         window.log.error(e);
 | |
|         return false;
 | |
|       }
 | |
|     }
 | |
|     return true;
 | |
|   }
 | |
| 
 | |
|   // FIXME: rename to include the fact it's relative to YOUR device
 | |
|   async function validateAuthorisation(authorisation) {
 | |
|     const {
 | |
|       primaryDevicePubKey,
 | |
|       secondaryDevicePubKey,
 | |
|       grantSignature,
 | |
|     } = authorisation;
 | |
|     const alreadySecondaryDevice = !!window.storage.get('isSecondaryDevice');
 | |
|     const ourPubKey = textsecure.storage.user.getNumber();
 | |
|     const isRequest = !(grantSignature && grantSignature.length > 0);
 | |
|     if (isRequest && alreadySecondaryDevice) {
 | |
|       window.log.warn(
 | |
|         'Received a pairing request while being a secondary device. Ignored.'
 | |
|       );
 | |
|       return false;
 | |
|     } else if (isRequest && primaryDevicePubKey !== ourPubKey) {
 | |
|       window.log.warn(
 | |
|         'Received a pairing request addressed to another pubkey. Ignored.'
 | |
|       );
 | |
|       return false;
 | |
|     } else if (isRequest && secondaryDevicePubKey === ourPubKey) {
 | |
|       window.log.warn('Received a pairing request from ourselves. Ignored.');
 | |
|       return false;
 | |
|     }
 | |
|     return this.verifyAuthorisation(authorisation);
 | |
|   }
 | |
| 
 | |
|   async function verifyPairingSignature(
 | |
|     primaryDevicePubKey,
 | |
|     secondaryPubKey,
 | |
|     signature,
 | |
|     type
 | |
|   ) {
 | |
|     const secondaryPubKeyArrayBuffer = StringView.hexToArrayBuffer(
 | |
|       secondaryPubKey
 | |
|     );
 | |
|     const primaryDevicePubKeyArrayBuffer = StringView.hexToArrayBuffer(
 | |
|       primaryDevicePubKey
 | |
|     );
 | |
|     const len = secondaryPubKeyArrayBuffer.byteLength;
 | |
|     const data = new Uint8Array(len + 1);
 | |
|     // For REQUEST type message, the secondary device signs the primary device pubkey
 | |
|     // For GRANT type message, the primary device signs the secondary device pubkey
 | |
|     let issuer;
 | |
|     if (type === PairingType.GRANT) {
 | |
|       data.set(new Uint8Array(secondaryPubKeyArrayBuffer));
 | |
|       issuer = primaryDevicePubKeyArrayBuffer;
 | |
|     } else if (type === PairingType.REQUEST) {
 | |
|       data.set(new Uint8Array(primaryDevicePubKeyArrayBuffer));
 | |
|       issuer = secondaryPubKeyArrayBuffer;
 | |
|     }
 | |
|     data[len] = type;
 | |
|     // Throws for invalid signature
 | |
|     await libsignal.Curve.async.verifySignature(issuer, data.buffer, signature);
 | |
|   }
 | |
|   async function decryptToken({ cipherText64, serverPubKey64 }) {
 | |
|     const ivAndCiphertext = new Uint8Array(
 | |
|       dcodeIO.ByteBuffer.fromBase64(cipherText64).toArrayBuffer()
 | |
|     );
 | |
| 
 | |
|     const serverPubKey = new Uint8Array(
 | |
|       dcodeIO.ByteBuffer.fromBase64(serverPubKey64).toArrayBuffer()
 | |
|     );
 | |
|     const keyPair = await textsecure.storage.protocol.getIdentityKeyPair();
 | |
|     if (!keyPair) {
 | |
|       throw new Error('Failed to get keypair for token decryption');
 | |
|     }
 | |
|     const { privKey } = keyPair;
 | |
|     const symmetricKey = await libsignal.Curve.async.calculateAgreement(
 | |
|       serverPubKey,
 | |
|       privKey
 | |
|     );
 | |
| 
 | |
|     const token = await DHDecrypt(symmetricKey, ivAndCiphertext);
 | |
| 
 | |
|     const tokenString = dcodeIO.ByteBuffer.wrap(token).toString('utf8');
 | |
|     return tokenString;
 | |
|   }
 | |
| 
 | |
|   const sha512 = data => crypto.subtle.digest('SHA-512', data);
 | |
| 
 | |
|   const PairingType = Object.freeze({
 | |
|     REQUEST: 1,
 | |
|     GRANT: 2,
 | |
|   });
 | |
| 
 | |
|   window.libloki.crypto = {
 | |
|     DHEncrypt,
 | |
|     EncryptGCM, // AES-GCM
 | |
|     DHDecrypt,
 | |
|     DecryptGCM, // AES-GCM
 | |
|     decryptToken,
 | |
|     generateSignatureForPairing,
 | |
|     verifyPairingSignature,
 | |
|     verifyAuthorisation,
 | |
|     validateAuthorisation,
 | |
|     PairingType,
 | |
|     generateEphemeralKeyPair,
 | |
|     encryptForPubkey,
 | |
|     decryptForPubkey,
 | |
|     _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,
 | |
|     sha512,
 | |
|   };
 | |
| })();
 |