signal-desktop/ts/test-electron/metadata/SecretSessionCipher_test.ts
2021-03-19 16:57:35 -04:00

454 lines
14 KiB
TypeScript

// Copyright 2018-2021 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
/* eslint-disable @typescript-eslint/no-explicit-any */
import { assert } from 'chai';
import {
ExplodedSenderCertificateType,
SecretSessionCipher,
createCertificateValidator,
_createSenderCertificateFromBuffer,
_createServerCertificateFromBuffer,
} from '../../metadata/SecretSessionCipher';
import {
bytesFromString,
stringFromBytes,
arrayBufferToBase64,
} from '../../Crypto';
import { KeyPairType } from '../../libsignal.d';
function toString(thing: string | ArrayBuffer): string {
if (typeof thing === 'string') {
return thing;
}
return arrayBufferToBase64(thing);
}
class InMemorySignalProtocolStore {
store: Record<string, any> = {};
Direction = {
SENDING: 1,
RECEIVING: 2,
};
getIdentityKeyPair(): Promise<{ privKey: ArrayBuffer; pubKey: ArrayBuffer }> {
return Promise.resolve(this.get('identityKey'));
}
getLocalRegistrationId(): Promise<string> {
return Promise.resolve(this.get('registrationId'));
}
put(key: string, value: any): void {
if (
key === undefined ||
value === undefined ||
key === null ||
value === null
) {
throw new Error('Tried to store undefined/null');
}
this.store[key] = value;
}
get(key: string, defaultValue?: any): any {
if (key === null || key === undefined) {
throw new Error('Tried to get value for undefined/null key');
}
if (key in this.store) {
return this.store[key];
}
return defaultValue;
}
remove(key: string): void {
if (key === null || key === undefined) {
throw new Error('Tried to remove value for undefined/null key');
}
delete this.store[key];
}
isTrustedIdentity(
identifier: string,
identityKey: ArrayBuffer
): Promise<boolean> {
if (identifier === null || identifier === undefined) {
throw new Error('tried to check identity key for undefined/null key');
}
if (!(identityKey instanceof ArrayBuffer)) {
throw new Error('Expected identityKey to be an ArrayBuffer');
}
const trusted = this.get(`identityKey${identifier}`);
if (trusted === undefined) {
return Promise.resolve(true);
}
return Promise.resolve(toString(identityKey) === toString(trusted));
}
loadIdentityKey(identifier: string): any {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to get identity key for undefined/null key');
}
return Promise.resolve(this.get(`identityKey${identifier}`));
}
saveIdentity(identifier: string, identityKey: ArrayBuffer): any {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key');
}
const address = window.libsignal.SignalProtocolAddress.fromString(
identifier
);
const existing = this.get(`identityKey${address.getName()}`);
this.put(`identityKey${address.getName()}`, identityKey);
if (existing && toString(identityKey) !== toString(existing)) {
return Promise.resolve(true);
}
return Promise.resolve(false);
}
/* Returns a prekeypair object or undefined */
loadPreKey(keyId: number): any {
let res = this.get(`25519KeypreKey${keyId}`);
if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey };
}
return Promise.resolve(res);
}
storePreKey(keyId: number, keyPair: any): Promise<void> {
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
}
removePreKey(keyId: number): Promise<void> {
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
}
/* Returns a signed keypair object or undefined */
loadSignedPreKey(keyId: number): any {
let res = this.get(`25519KeysignedKey${keyId}`);
if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey };
}
return Promise.resolve(res);
}
storeSignedPreKey(keyId: number, keyPair: any): Promise<void> {
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
}
removeSignedPreKey(keyId: number): Promise<void> {
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
}
loadSession(identifier: string): Promise<any> {
return Promise.resolve(this.get(`session${identifier}`));
}
storeSession(identifier: string, record: any): Promise<void> {
return Promise.resolve(this.put(`session${identifier}`, record));
}
removeSession(identifier: string): Promise<void> {
return Promise.resolve(this.remove(`session${identifier}`));
}
removeAllSessions(identifier: string): Promise<void> {
// eslint-disable-next-line no-restricted-syntax
for (const id in this.store) {
if (id.startsWith(`session${identifier}`)) {
delete this.store[id];
}
}
return Promise.resolve();
}
}
describe('SecretSessionCipher', () => {
it('successfully roundtrips', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('smert za smert')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
const decryptResult = await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
if (!decryptResult.content) {
throw new Error('decryptResult.content is null!');
}
if (!decryptResult.sender) {
throw new Error('decryptResult.sender is null!');
}
assert.strictEqual(
stringFromBytes(decryptResult.content),
'smert za smert'
);
assert.strictEqual(decryptResult.sender.toString(), '+14151111111.1');
});
it('fails when untrusted', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const falseTrustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
falseTrustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('и вот я')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
try {
await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
throw new Error('It did not fail!');
} catch (error) {
assert.strictEqual(error.message, 'Invalid signature');
}
});
it('fails when expired', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const aliceIdentityKey = await aliceStore.getIdentityKeyPair();
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('и вот я')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
try {
await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31338
);
throw new Error('It did not fail!');
} catch (error) {
assert.strictEqual(error.message, 'Certificate is expired');
}
});
it('fails when wrong identity', async function thisNeeded() {
this.timeout(4000);
const aliceStore = new InMemorySignalProtocolStore();
const bobStore = new InMemorySignalProtocolStore();
await _initializeSessions(aliceStore, bobStore);
const trustRoot = await window.libsignal.Curve.async.generateKeyPair();
const randomKeyPair = await window.libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
randomKeyPair.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore as any);
const ciphertext = await aliceCipher.encrypt(
new window.libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('smert za smert')
);
const bobCipher = new SecretSessionCipher(bobStore as any);
try {
await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
throw new Error('It did not fail!');
} catch (error) {
assert.strictEqual(
error.message,
"Sender's certificate key does not match key used in message"
);
}
});
// private SenderCertificate _createCertificateFor(
// ECKeyPair trustRoot
// String sender
// int deviceId
// ECPublicKey identityKey
// long expires
// )
async function _createSenderCertificateFor(
trustRoot: KeyPairType,
sender: string,
deviceId: number,
identityKey: ArrayBuffer,
expires: number
): Promise<ExplodedSenderCertificateType> {
const serverKey = await window.libsignal.Curve.async.generateKeyPair();
const serverCertificateCertificateProto = new window.textsecure.protobuf.ServerCertificate.Certificate();
serverCertificateCertificateProto.id = 1;
serverCertificateCertificateProto.key = serverKey.pubKey;
const serverCertificateCertificateBytes = serverCertificateCertificateProto.toArrayBuffer();
const serverCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
trustRoot.privKey,
serverCertificateCertificateBytes
);
const serverCertificateProto = new window.textsecure.protobuf.ServerCertificate();
serverCertificateProto.certificate = serverCertificateCertificateBytes;
serverCertificateProto.signature = serverCertificateSignature;
const serverCertificate = _createServerCertificateFromBuffer(
serverCertificateProto.toArrayBuffer()
);
const senderCertificateCertificateProto = new window.textsecure.protobuf.SenderCertificate.Certificate();
senderCertificateCertificateProto.sender = sender;
senderCertificateCertificateProto.senderDevice = deviceId;
senderCertificateCertificateProto.identityKey = identityKey;
senderCertificateCertificateProto.expires = expires;
senderCertificateCertificateProto.signer = window.textsecure.protobuf.ServerCertificate.decode(
serverCertificate.serialized
);
const senderCertificateBytes = senderCertificateCertificateProto.toArrayBuffer();
const senderCertificateSignature = await window.libsignal.Curve.async.calculateSignature(
serverKey.privKey,
senderCertificateBytes
);
const senderCertificateProto = new window.textsecure.protobuf.SenderCertificate();
senderCertificateProto.certificate = senderCertificateBytes;
senderCertificateProto.signature = senderCertificateSignature;
return _createSenderCertificateFromBuffer(
senderCertificateProto.toArrayBuffer()
);
}
// private void _initializeSessions(
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
async function _initializeSessions(
aliceStore: InMemorySignalProtocolStore,
bobStore: InMemorySignalProtocolStore
): Promise<void> {
const aliceAddress = new window.libsignal.SignalProtocolAddress(
'+14152222222',
1
);
await aliceStore.put(
'identityKey',
await window.libsignal.Curve.generateKeyPair()
);
await bobStore.put(
'identityKey',
await window.libsignal.Curve.generateKeyPair()
);
await aliceStore.put('registrationId', 57);
await bobStore.put('registrationId', 58);
const bobPreKey = await window.libsignal.Curve.async.generateKeyPair();
const bobIdentityKey = await bobStore.getIdentityKeyPair();
const bobSignedPreKey = await window.libsignal.KeyHelper.generateSignedPreKey(
bobIdentityKey,
2
);
const bobBundle = {
deviceId: 3,
identityKey: bobIdentityKey.pubKey,
registrationId: 1,
signedPreKey: {
keyId: 2,
publicKey: bobSignedPreKey.keyPair.pubKey,
signature: bobSignedPreKey.signature,
},
preKey: {
keyId: 1,
publicKey: bobPreKey.pubKey,
},
};
const aliceSessionBuilder = new window.libsignal.SessionBuilder(
aliceStore as any,
aliceAddress
);
await aliceSessionBuilder.processPreKey(bobBundle);
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
await bobStore.storePreKey(1, bobPreKey);
}
});