signal-desktop/test/metadata/SecretSessionCipher_test.js
Scott Nonnenberg 8d6cba1b43 Eliminate remaining Electron 8 deprecations
* Change systemPreferences.isDarkMode() to nativeTheme.shouldUseDarkColors

* Remove vibrancy parameter to BrowserWindow

* Update curve25519-n; removes context-aware deprecation warning

* Set app.allowRendererProcessReuse = true to remove warning

* Move from deprecated setters to direct property set

* Serialized sender certificates: Store less, store plain object

* isMenuBarAutoHide -> autoHideMenuBar

* UUID: Fix sealed sender indicator on message details screen

* Data._cleanData: Remove function keys, handle null in array

Also:
- run _cleanData when saving attachment download jobs
- remove job from jobs table when the send itself throws error

* _cleanData: Don't dig into strings, booleans, or numbers

* getPropsForMessageDetail: Make it clear what we're reducing

Co-authored-by: Ken Powers <ken@signal.org>
2020-03-24 17:03:01 -07:00

417 lines
12 KiB
JavaScript

/* global libsignal, textsecure */
'use strict';
const {
SecretSessionCipher,
createCertificateValidator,
_createSenderCertificateFromBuffer,
_createServerCertificateFromBuffer,
} = window.Signal.Metadata;
const {
bytesFromString,
stringFromBytes,
arrayBufferToBase64,
} = window.Signal.Crypto;
function InMemorySignalProtocolStore() {
this.store = {};
}
function toString(thing) {
if (typeof thing === 'string') {
return thing;
}
return arrayBufferToBase64(thing);
}
InMemorySignalProtocolStore.prototype = {
Direction: {
SENDING: 1,
RECEIVING: 2,
},
getIdentityKeyPair() {
return Promise.resolve(this.get('identityKey'));
},
getLocalRegistrationId() {
return Promise.resolve(this.get('registrationId'));
},
put(key, value) {
if (
key === undefined ||
value === undefined ||
key === null ||
value === null
)
throw new Error('Tried to store undefined/null');
this.store[key] = value;
},
get(key, defaultValue) {
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) {
if (key === null || key === undefined)
throw new Error('Tried to remove value for undefined/null key');
delete this.store[key];
},
isTrustedIdentity(identifier, identityKey) {
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) {
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, identityKey) {
if (identifier === null || identifier === undefined) {
throw new Error('Tried to put identity key for undefined/null key');
}
const address = 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) {
let res = this.get(`25519KeypreKey${keyId}`);
if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey };
}
return Promise.resolve(res);
},
storePreKey(keyId, keyPair) {
return Promise.resolve(this.put(`25519KeypreKey${keyId}`, keyPair));
},
removePreKey(keyId) {
return Promise.resolve(this.remove(`25519KeypreKey${keyId}`));
},
/* Returns a signed keypair object or undefined */
loadSignedPreKey(keyId) {
let res = this.get(`25519KeysignedKey${keyId}`);
if (res !== undefined) {
res = { pubKey: res.pubKey, privKey: res.privKey };
}
return Promise.resolve(res);
},
storeSignedPreKey(keyId, keyPair) {
return Promise.resolve(this.put(`25519KeysignedKey${keyId}`, keyPair));
},
removeSignedPreKey(keyId) {
return Promise.resolve(this.remove(`25519KeysignedKey${keyId}`));
},
loadSession(identifier) {
return Promise.resolve(this.get(`session${identifier}`));
},
storeSession(identifier, record) {
return Promise.resolve(this.put(`session${identifier}`, record));
},
removeSession(identifier) {
return Promise.resolve(this.remove(`session${identifier}`));
},
removeAllSessions(identifier) {
// 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 libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore);
const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('smert za smert')
);
const bobCipher = new SecretSessionCipher(bobStore);
const decryptResult = await bobCipher.decrypt(
createCertificateValidator(trustRoot.pubKey),
ciphertext,
31335
);
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 libsignal.Curve.async.generateKeyPair();
const falseTrustRoot = await libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
falseTrustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore);
const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('и вот я')
);
const bobCipher = new SecretSessionCipher(bobStore);
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 libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
aliceIdentityKey.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore);
const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('и вот я')
);
const bobCipher = new SecretSessionCipher(bobStore);
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 libsignal.Curve.async.generateKeyPair();
const randomKeyPair = await libsignal.Curve.async.generateKeyPair();
const senderCertificate = await _createSenderCertificateFor(
trustRoot,
'+14151111111',
1,
randomKeyPair.pubKey,
31337
);
const aliceCipher = new SecretSessionCipher(aliceStore);
const ciphertext = await aliceCipher.encrypt(
new libsignal.SignalProtocolAddress('+14152222222', 1),
{ serialized: senderCertificate.serialized },
bytesFromString('smert za smert')
);
const bobCipher = new SecretSessionCipher(bobStore);
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,
sender,
deviceId,
identityKey,
expires
) {
const serverKey = await libsignal.Curve.async.generateKeyPair();
const serverCertificateCertificateProto = new textsecure.protobuf.ServerCertificate.Certificate();
serverCertificateCertificateProto.id = 1;
serverCertificateCertificateProto.key = serverKey.pubKey;
const serverCertificateCertificateBytes = serverCertificateCertificateProto
.encode()
.toArrayBuffer();
const serverCertificateSignature = await libsignal.Curve.async.calculateSignature(
trustRoot.privKey,
serverCertificateCertificateBytes
);
const serverCertificateProto = new textsecure.protobuf.ServerCertificate();
serverCertificateProto.certificate = serverCertificateCertificateBytes;
serverCertificateProto.signature = serverCertificateSignature;
const serverCertificate = _createServerCertificateFromBuffer(
serverCertificateProto.encode().toArrayBuffer()
);
const senderCertificateCertificateProto = new textsecure.protobuf.SenderCertificate.Certificate();
senderCertificateCertificateProto.sender = sender;
senderCertificateCertificateProto.senderDevice = deviceId;
senderCertificateCertificateProto.identityKey = identityKey;
senderCertificateCertificateProto.expires = expires;
senderCertificateCertificateProto.signer = textsecure.protobuf.ServerCertificate.decode(
serverCertificate.serialized
);
const senderCertificateBytes = senderCertificateCertificateProto
.encode()
.toArrayBuffer();
const senderCertificateSignature = await libsignal.Curve.async.calculateSignature(
serverKey.privKey,
senderCertificateBytes
);
const senderCertificateProto = new textsecure.protobuf.SenderCertificate();
senderCertificateProto.certificate = senderCertificateBytes;
senderCertificateProto.signature = senderCertificateSignature;
return _createSenderCertificateFromBuffer(
senderCertificateProto.encode().toArrayBuffer()
);
}
// private void _initializeSessions(
// SignalProtocolStore aliceStore, SignalProtocolStore bobStore)
async function _initializeSessions(aliceStore, bobStore) {
const aliceAddress = new libsignal.SignalProtocolAddress('+14152222222', 1);
await aliceStore.put(
'identityKey',
await libsignal.Curve.generateKeyPair()
);
await bobStore.put('identityKey', await libsignal.Curve.generateKeyPair());
await aliceStore.put('registrationId', 57);
await bobStore.put('registrationId', 58);
const bobPreKey = await libsignal.Curve.async.generateKeyPair();
const bobIdentityKey = await bobStore.getIdentityKeyPair();
const bobSignedPreKey = await libsignal.KeyHelper.generateSignedPreKey(
bobIdentityKey,
2
);
const bobBundle = {
identityKey: bobIdentityKey.pubKey,
registrationId: 1,
preKey: {
keyId: 1,
publicKey: bobPreKey.pubKey,
},
signedPreKey: {
keyId: 2,
publicKey: bobSignedPreKey.keyPair.pubKey,
signature: bobSignedPreKey.signature,
},
};
const aliceSessionBuilder = new libsignal.SessionBuilder(
aliceStore,
aliceAddress
);
await aliceSessionBuilder.processPreKey(bobBundle);
await bobStore.storeSignedPreKey(2, bobSignedPreKey.keyPair);
await bobStore.storePreKey(1, bobPreKey);
}
});