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.
		
		
		
		
		
			
		
			
				
	
	
		
			173 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			JavaScript
		
	
			
		
		
	
	
			173 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			JavaScript
		
	
| /* global
 | |
|   window,
 | |
|   libsignal,
 | |
|   textsecure,
 | |
|   StringView,
 | |
|   Multibase,
 | |
|   TextEncoder,
 | |
|   TextDecoder,
 | |
|   dcodeIO
 | |
| */
 | |
| 
 | |
| // eslint-disable-next-line func-names
 | |
| (function() {
 | |
|   window.libloki = window.libloki || {};
 | |
| 
 | |
|   class FallBackDecryptionError extends Error {}
 | |
| 
 | |
|   const IV_LENGTH = 16;
 | |
| 
 | |
|   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 DHDecrypt(symmetricKey, ivAndCiphertext) {
 | |
|     const iv = ivAndCiphertext.slice(0, IV_LENGTH);
 | |
|     const cipherText = ivAndCiphertext.slice(IV_LENGTH);
 | |
|     return libsignal.crypto.decrypt(symmetricKey, cipherText, iv);
 | |
|   }
 | |
| 
 | |
|   class FallBackSessionCipher {
 | |
|     constructor(address) {
 | |
|       this.identityKeyString = address.getName();
 | |
|       this.pubKey = StringView.hexToArrayBuffer(address.getName());
 | |
|     }
 | |
| 
 | |
|     async encrypt(plaintext) {
 | |
|       const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
 | |
|       const myPrivateKey = myKeyPair.privKey;
 | |
|       const symmetricKey = libsignal.Curve.calculateAgreement(
 | |
|         this.pubKey,
 | |
|         myPrivateKey
 | |
|       );
 | |
|       const ivAndCiphertext = await DHEncrypt(symmetricKey, plaintext);
 | |
|       return {
 | |
|         type: textsecure.protobuf.Envelope.Type.FRIEND_REQUEST,
 | |
|         body: ivAndCiphertext,
 | |
|         registrationId: null,
 | |
|       };
 | |
|     }
 | |
| 
 | |
|     async decrypt(ivAndCiphertext) {
 | |
|       const myKeyPair = await textsecure.storage.protocol.getIdentityKeyPair();
 | |
|       const myPrivateKey = myKeyPair.privKey;
 | |
|       const symmetricKey = libsignal.Curve.calculateAgreement(
 | |
|         this.pubKey,
 | |
|         myPrivateKey
 | |
|       );
 | |
|       try {
 | |
|         return await DHDecrypt(symmetricKey, ivAndCiphertext);
 | |
|       } catch (e) {
 | |
|         throw new FallBackDecryptionError(
 | |
|           `Could not decrypt message from ${
 | |
|             this.identityKeyString
 | |
|           } using FallBack encryption.`
 | |
|         );
 | |
|       }
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const base32zIndex = Multibase.names.indexOf('base32z');
 | |
|   const base32zCode = Multibase.codes[base32zIndex];
 | |
| 
 | |
|   function bufferToArrayBuffer(buf) {
 | |
|     const ab = new ArrayBuffer(buf.length);
 | |
|     const view = new Uint8Array(ab);
 | |
|     for (let i = 0; i < buf.length; i += 1) {
 | |
|       view[i] = buf[i];
 | |
|     }
 | |
|     return ab;
 | |
|   }
 | |
| 
 | |
|   function decodeSnodeAddressToPubKey(snodeAddress) {
 | |
|     const snodeAddressClean = snodeAddress
 | |
|       .replace('.snode', '')
 | |
|       .replace('http://', '');
 | |
|     return Multibase.decode(`${base32zCode}${snodeAddressClean}`);
 | |
|   }
 | |
| 
 | |
|   class LokiSnodeChannel {
 | |
|     constructor() {
 | |
|       this._ephemeralKeyPair = libsignal.Curve.generateKeyPair();
 | |
|       // Signal protocol prepends with "0x05"
 | |
|       this._ephemeralKeyPair.pubKey = this._ephemeralKeyPair.pubKey.slice(1);
 | |
|       this._ephemeralPubKeyHex = StringView.arrayBufferToHex(
 | |
|         this._ephemeralKeyPair.pubKey
 | |
|       );
 | |
|       this._cache = {};
 | |
|     }
 | |
| 
 | |
|     async _getSymmetricKey(snodeAddress) {
 | |
|       if (snodeAddress in this._cache) {
 | |
|         return this._cache[snodeAddress];
 | |
|       }
 | |
|       const ed25519PubKey = decodeSnodeAddressToPubKey(snodeAddress);
 | |
|       const sodium = await window.getSodium();
 | |
|       const curve25519PubKey = sodium.crypto_sign_ed25519_pk_to_curve25519(
 | |
|         ed25519PubKey
 | |
|       );
 | |
|       const snodePubKeyArrayBuffer = bufferToArrayBuffer(curve25519PubKey);
 | |
|       const symmetricKey = libsignal.Curve.calculateAgreement(
 | |
|         snodePubKeyArrayBuffer,
 | |
|         this._ephemeralKeyPair.privKey
 | |
|       );
 | |
|       this._cache[snodeAddress] = symmetricKey;
 | |
|       return symmetricKey;
 | |
|     }
 | |
| 
 | |
|     getChannelPublicKeyHex() {
 | |
|       return this._ephemeralPubKeyHex;
 | |
|     }
 | |
| 
 | |
|     async decrypt(snodeAddress, ivAndCipherTextBase64) {
 | |
|       const ivAndCipherText = dcodeIO.ByteBuffer.wrap(
 | |
|         ivAndCipherTextBase64,
 | |
|         'base64'
 | |
|       ).toArrayBuffer();
 | |
|       const symmetricKey = await this._getSymmetricKey(snodeAddress);
 | |
|       try {
 | |
|         const decrypted = await DHDecrypt(symmetricKey, ivAndCipherText);
 | |
|         const decoder = new TextDecoder();
 | |
|         return decoder.decode(decrypted);
 | |
|       } catch (e) {
 | |
|         return ivAndCipherText;
 | |
|       }
 | |
|     }
 | |
| 
 | |
|     async encrypt(snodeAddress, plainText) {
 | |
|       if (typeof plainText === 'string') {
 | |
|         const textEncoder = new TextEncoder();
 | |
|         // eslint-disable-next-line no-param-reassign
 | |
|         plainText = textEncoder.encode(plainText);
 | |
|       }
 | |
|       const symmetricKey = await this._getSymmetricKey(snodeAddress);
 | |
|       const cipherText = await DHEncrypt(symmetricKey, plainText);
 | |
|       return dcodeIO.ByteBuffer.wrap(cipherText).toString('base64');
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   const snodeCipher = new LokiSnodeChannel();
 | |
| 
 | |
|   window.libloki.crypto = {
 | |
|     DHEncrypt,
 | |
|     DHDecrypt,
 | |
|     FallBackSessionCipher,
 | |
|     FallBackDecryptionError,
 | |
|     snodeCipher,
 | |
|     // for testing
 | |
|     _LokiSnodeChannel: LokiSnodeChannel,
 | |
|     _decodeSnodeAddressToPubKey: decodeSnodeAddressToPubKey,
 | |
|   };
 | |
| })();
 |