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.
		
		
		
		
		
			
		
			
				
	
	
		
			281 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			281 lines
		
	
	
		
			8.9 KiB
		
	
	
	
		
			TypeScript
		
	
import chai, { expect } from 'chai';
 | 
						|
import * as crypto from 'crypto';
 | 
						|
import * as sinon from 'sinon';
 | 
						|
import { concatUInt8Array, getSodium, MessageEncrypter } from '../../../../session/crypto';
 | 
						|
import { EncryptionType } from '../../../../session/types/EncryptionType';
 | 
						|
import { Stubs, TestUtils } from '../../../test-utils';
 | 
						|
import { SignalService } from '../../../../protobuf';
 | 
						|
 | 
						|
import { StringUtils, UserUtils } from '../../../../session/utils';
 | 
						|
 | 
						|
import chaiBytes from 'chai-bytes';
 | 
						|
import { PubKey } from '../../../../session/types';
 | 
						|
import { fromHex, toHex } from '../../../../session/utils/String';
 | 
						|
import { addMessagePadding } from '../../../../session/crypto/BufferPadding';
 | 
						|
 | 
						|
chai.use(chaiBytes);
 | 
						|
 | 
						|
// tslint:disable-next-line: max-func-body-length
 | 
						|
describe('MessageEncrypter', () => {
 | 
						|
  const sandbox = sinon.createSandbox();
 | 
						|
  const ourNumber = '0123456789abcdef';
 | 
						|
  const ourUserEd25516Keypair = {
 | 
						|
    pubKey: '37e1631b002de498caf7c5c1712718bde7f257c6dadeed0c21abf5e939e6c309',
 | 
						|
    privKey:
 | 
						|
      'be1d11154ff9b6de77873f0b6b0bcc460000000000000000000000000000000037e1631b002de498caf7c5c1712718bde7f257c6dadeed0c21abf5e939e6c309',
 | 
						|
  };
 | 
						|
 | 
						|
  const ourIdentityKeypair = {
 | 
						|
    pubKey: new Uint8Array([
 | 
						|
      5,
 | 
						|
      44,
 | 
						|
      2,
 | 
						|
      168,
 | 
						|
      162,
 | 
						|
      203,
 | 
						|
      50,
 | 
						|
      66,
 | 
						|
      136,
 | 
						|
      81,
 | 
						|
      30,
 | 
						|
      221,
 | 
						|
      57,
 | 
						|
      245,
 | 
						|
      1,
 | 
						|
      148,
 | 
						|
      162,
 | 
						|
      194,
 | 
						|
      255,
 | 
						|
      47,
 | 
						|
      134,
 | 
						|
      104,
 | 
						|
      180,
 | 
						|
      207,
 | 
						|
      188,
 | 
						|
      18,
 | 
						|
      71,
 | 
						|
      62,
 | 
						|
      58,
 | 
						|
      107,
 | 
						|
      23,
 | 
						|
      92,
 | 
						|
      97,
 | 
						|
    ]),
 | 
						|
    privKey: new Uint8Array([
 | 
						|
      200,
 | 
						|
      45,
 | 
						|
      226,
 | 
						|
      75,
 | 
						|
      253,
 | 
						|
      235,
 | 
						|
      213,
 | 
						|
      108,
 | 
						|
      187,
 | 
						|
      188,
 | 
						|
      217,
 | 
						|
      9,
 | 
						|
      51,
 | 
						|
      105,
 | 
						|
      65,
 | 
						|
      15,
 | 
						|
      97,
 | 
						|
      36,
 | 
						|
      233,
 | 
						|
      33,
 | 
						|
      21,
 | 
						|
      31,
 | 
						|
      7,
 | 
						|
      90,
 | 
						|
      145,
 | 
						|
      30,
 | 
						|
      52,
 | 
						|
      254,
 | 
						|
      47,
 | 
						|
      162,
 | 
						|
      192,
 | 
						|
      105,
 | 
						|
    ]),
 | 
						|
  };
 | 
						|
 | 
						|
  beforeEach(() => {
 | 
						|
    TestUtils.stubWindow('textsecure', {
 | 
						|
      storage: {
 | 
						|
        protocol: sandbox.stub(),
 | 
						|
      },
 | 
						|
    });
 | 
						|
 | 
						|
    sandbox.stub(UserUtils, 'getOurPubKeyStrFromCache').returns(ourNumber);
 | 
						|
    sandbox.stub(UserUtils, 'getUserED25519KeyPair').resolves(ourUserEd25516Keypair);
 | 
						|
  });
 | 
						|
 | 
						|
  afterEach(() => {
 | 
						|
    sandbox.restore();
 | 
						|
    TestUtils.restoreStubs();
 | 
						|
  });
 | 
						|
 | 
						|
  describe('EncryptionType', () => {
 | 
						|
    describe('ClosedGroup', () => {
 | 
						|
      it('should return a CLOSED_GROUP_MESSAGE envelope type for ClosedGroup', async () => {
 | 
						|
        const hexKeyPair = {
 | 
						|
          publicHex: `05${ourUserEd25516Keypair.pubKey}`,
 | 
						|
          privateHex: '0123456789abcdef',
 | 
						|
        };
 | 
						|
 | 
						|
        TestUtils.stubData('getLatestClosedGroupEncryptionKeyPair').resolves(hexKeyPair);
 | 
						|
 | 
						|
        const data = crypto.randomBytes(10);
 | 
						|
 | 
						|
        const result = await MessageEncrypter.encrypt(
 | 
						|
          TestUtils.generateFakePubKey(),
 | 
						|
          data,
 | 
						|
          EncryptionType.ClosedGroup
 | 
						|
        );
 | 
						|
        chai
 | 
						|
          .expect(result.envelopeType)
 | 
						|
          .to.deep.equal(SignalService.Envelope.Type.CLOSED_GROUP_MESSAGE);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should return a SESSION_MESSAGE envelope type for Fallback', async () => {
 | 
						|
        const data = crypto.randomBytes(10);
 | 
						|
 | 
						|
        const result = await MessageEncrypter.encrypt(
 | 
						|
          TestUtils.generateFakePubKey(),
 | 
						|
          data,
 | 
						|
          EncryptionType.Fallback
 | 
						|
        );
 | 
						|
        chai.expect(result.envelopeType).to.deep.equal(SignalService.Envelope.Type.SESSION_MESSAGE);
 | 
						|
      });
 | 
						|
 | 
						|
      it('should throw an error for anything else than Fallback or ClosedGroup', () => {
 | 
						|
        const data = crypto.randomBytes(10);
 | 
						|
        return MessageEncrypter.encrypt(
 | 
						|
          TestUtils.generateFakePubKey(),
 | 
						|
          data,
 | 
						|
          EncryptionType.Signal
 | 
						|
        ).should.eventually.be.rejectedWith(Error);
 | 
						|
      });
 | 
						|
    });
 | 
						|
  });
 | 
						|
 | 
						|
  // tslint:disable-next-line: max-func-body-length
 | 
						|
  describe('Session Protocol', () => {
 | 
						|
    let sandboxSessionProtocol: sinon.SinonSandbox;
 | 
						|
 | 
						|
    beforeEach(() => {
 | 
						|
      sandboxSessionProtocol = sinon.createSandbox();
 | 
						|
 | 
						|
      sandboxSessionProtocol.stub(UserUtils, 'getIdentityKeyPair').resolves(ourIdentityKeypair);
 | 
						|
    });
 | 
						|
 | 
						|
    afterEach(() => {
 | 
						|
      sandboxSessionProtocol.restore();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should pass the padded message body to encrypt', async () => {
 | 
						|
      const data = crypto.randomBytes(10);
 | 
						|
      const spy = sinon.spy(MessageEncrypter, 'encryptUsingSessionProtocol');
 | 
						|
      await MessageEncrypter.encrypt(TestUtils.generateFakePubKey(), data, EncryptionType.Fallback);
 | 
						|
      chai.expect(spy.callCount).to.be.equal(1);
 | 
						|
      const paddedData = addMessagePadding(data);
 | 
						|
      const firstArgument = new Uint8Array(spy.args[0][1]);
 | 
						|
      chai.expect(firstArgument).to.deep.equal(paddedData);
 | 
						|
      spy.restore();
 | 
						|
    });
 | 
						|
 | 
						|
    it('should pass the correct data for sodium crypto_sign', async () => {
 | 
						|
      const keypair = await UserUtils.getUserED25519KeyPair();
 | 
						|
      const recipient = TestUtils.generateFakePubKey();
 | 
						|
      const sodium = await getSodium();
 | 
						|
      const cryptoSignDetachedSpy = sandboxSessionProtocol.spy(sodium, 'crypto_sign_detached');
 | 
						|
      const plainText = '123456';
 | 
						|
      const plainTextBytes = new Uint8Array(StringUtils.encode(plainText, 'utf8'));
 | 
						|
      const userED25519PubKeyBytes = new Uint8Array(
 | 
						|
        // tslint:disable: no-non-null-assertion
 | 
						|
        StringUtils.fromHex(keypair!.pubKey)
 | 
						|
      );
 | 
						|
      const recipientX25519PublicKeyWithoutPrefix = PubKey.remove05PrefixIfNeeded(recipient.key);
 | 
						|
 | 
						|
      const recipientX25519PublicKey = new Uint8Array(
 | 
						|
        StringUtils.fromHex(recipientX25519PublicKeyWithoutPrefix)
 | 
						|
      );
 | 
						|
      await MessageEncrypter.encryptUsingSessionProtocol(recipient, plainTextBytes);
 | 
						|
      const [dataForSign, userED25519SecretKeyBytes] = cryptoSignDetachedSpy.args[0];
 | 
						|
      const userEdPrivkeyBytes = new Uint8Array(StringUtils.fromHex(keypair!.privKey));
 | 
						|
      expect(userED25519SecretKeyBytes).to.equalBytes(userEdPrivkeyBytes);
 | 
						|
      // dataForSign must be plaintext | userED25519PubKeyBytes | recipientX25519PublicKey
 | 
						|
      expect((dataForSign as Uint8Array).subarray(0, plainTextBytes.length)).to.equalBytes(
 | 
						|
        plainTextBytes
 | 
						|
      );
 | 
						|
      expect(
 | 
						|
        (dataForSign as Uint8Array).subarray(
 | 
						|
          plainTextBytes.length,
 | 
						|
          plainTextBytes.length + userED25519PubKeyBytes.length
 | 
						|
        )
 | 
						|
      ).to.equalBytes(userED25519PubKeyBytes);
 | 
						|
 | 
						|
      // the recipient pubkey must have its 05 prefix removed
 | 
						|
      expect(
 | 
						|
        (dataForSign as Uint8Array).subarray(plainTextBytes.length + userED25519PubKeyBytes.length)
 | 
						|
      ).to.equalBytes(recipientX25519PublicKey);
 | 
						|
    });
 | 
						|
 | 
						|
    it('should return valid decodable ciphertext', async () => {
 | 
						|
      // for testing, we encode a message to ourself
 | 
						|
      const userX25519KeyPair = await UserUtils.getIdentityKeyPair();
 | 
						|
      const userEd25519KeyPair = await UserUtils.getUserED25519KeyPair();
 | 
						|
 | 
						|
      const plainTextBytes = new Uint8Array(StringUtils.encode('123456789', 'utf8'));
 | 
						|
 | 
						|
      const sodium = await getSodium();
 | 
						|
 | 
						|
      const recipientX25519PrivateKey = userX25519KeyPair!.privKey;
 | 
						|
      const recipientX25519PublicKeyHex = toHex(userX25519KeyPair!.pubKey);
 | 
						|
      const recipientX25519PublicKeyWithoutPrefix = PubKey.remove05PrefixIfNeeded(
 | 
						|
        recipientX25519PublicKeyHex
 | 
						|
      );
 | 
						|
      const recipientX25519PublicKey = new PubKey(recipientX25519PublicKeyWithoutPrefix);
 | 
						|
      const ciphertext = await MessageEncrypter.encryptUsingSessionProtocol(
 | 
						|
        recipientX25519PublicKey,
 | 
						|
        plainTextBytes
 | 
						|
      );
 | 
						|
 | 
						|
      // decrypt content
 | 
						|
      const plaintextWithMetadata = sodium.crypto_box_seal_open(
 | 
						|
        ciphertext,
 | 
						|
        new Uint8Array(fromHex(recipientX25519PublicKey.key)),
 | 
						|
        new Uint8Array(recipientX25519PrivateKey)
 | 
						|
      );
 | 
						|
 | 
						|
      // get message parts
 | 
						|
      const signatureSize = sodium.crypto_sign_BYTES;
 | 
						|
      const ed25519PublicKeySize = sodium.crypto_sign_PUBLICKEYBYTES;
 | 
						|
      const signatureStart = plaintextWithMetadata.byteLength - signatureSize;
 | 
						|
      const signature = plaintextWithMetadata.subarray(signatureStart);
 | 
						|
      const pubkeyStart = plaintextWithMetadata.byteLength - (signatureSize + ed25519PublicKeySize);
 | 
						|
      const pubkeyEnd = plaintextWithMetadata.byteLength - signatureSize;
 | 
						|
      // this should be ours ed25519 pubkey
 | 
						|
      const senderED25519PublicKey = plaintextWithMetadata.subarray(pubkeyStart, pubkeyEnd);
 | 
						|
 | 
						|
      const plainTextEnd =
 | 
						|
        plaintextWithMetadata.byteLength - (signatureSize + ed25519PublicKeySize);
 | 
						|
      const plaintextDecoded = plaintextWithMetadata.subarray(0, plainTextEnd);
 | 
						|
 | 
						|
      expect(plaintextDecoded).to.equalBytes(plainTextBytes);
 | 
						|
      expect(senderED25519PublicKey).to.equalBytes(userEd25519KeyPair!.pubKey);
 | 
						|
 | 
						|
      // verify the signature is valid
 | 
						|
      const dataForVerify = concatUInt8Array(
 | 
						|
        plaintextDecoded,
 | 
						|
        senderED25519PublicKey,
 | 
						|
        new Uint8Array(fromHex(recipientX25519PublicKey.key))
 | 
						|
      );
 | 
						|
      const isValid = sodium.crypto_sign_verify_detached(
 | 
						|
        signature,
 | 
						|
        dataForVerify,
 | 
						|
        senderED25519PublicKey
 | 
						|
      );
 | 
						|
      expect(isValid).to.be.equal(true, 'the signature cannot be verified');
 | 
						|
    });
 | 
						|
  });
 | 
						|
});
 |