420 lines
12 KiB
JavaScript
420 lines
12 KiB
JavaScript
// Copyright 2018-2020 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
/* 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);
|
|
}
|
|
});
|