libsignal-protocol-javascript/test/SessionCipherTest.js
2016-07-31 23:20:57 -07:00

336 lines
13 KiB
JavaScript

/*
* vim: ts=4:sw=4
*/
'use strict';
describe('SessionCipher', function() {
describe('getRemoteRegistrationId', function() {
var store = new SignalProtocolStore();
var registrationId = 1337;
var address = new libsignal.SignalProtocolAddress('foo', 1);
var sessionCipher = new libsignal.SessionCipher(store, address.toString());
describe('when a record exists', function() {
before(function(done) {
var record = new Internal.SessionRecord('identityKey', registrationId);
store.storeSession(address.toString(), record.serialize()).then(done);
});
it('returns a valid registrationId', function(done) {
sessionCipher.getRemoteRegistrationId().then(function(value) {
assert.strictEqual(value, registrationId);
}).then(done,done);
});
});
describe('when a record does not exist', function() {
it('returns undefined', function(done) {
var sessionCipher = new libsignal.SessionCipher(store, 'bar.1');
sessionCipher.getRemoteRegistrationId().then(function(value) {
assert.isUndefined(value);
}).then(done,done);
});
});
});
describe('hasOpenSession', function() {
var store = new SignalProtocolStore();
var address = new libsignal.SignalProtocolAddress('foo', 1);
var sessionCipher = new libsignal.SessionCipher(store, address.toString());
describe('registrationId is valid', function() {
before(function(done) {
var record = new Internal.SessionRecord('identityKey', 1);
store.storeSession(address.toString(), record.serialize()).then(done);
});
it('returns true for a session with a valid registrationId', function(done) {
sessionCipher.hasOpenSession(address.toString()).then(function(value) {
assert.isTrue(value);
}).then(done,done);
});
});
describe('registrationId is null', function() {
before(function(done) {
var record = new Internal.SessionRecord('identityKey');
store.storeSession(address.toString(), record.serialize()).then(done);
});
it('returns false for a session with a null registrationId', function(done) {
sessionCipher.hasOpenSession(address.toString()).then(function(value) {
assert.isFalse(value);
}).then(done,done);
});
});
describe('when there is no session', function() {
it('returns false', function(done) {
sessionCipher.hasOpenSession('bar').then(function(value) {
assert.isFalse(value);
}).then(done,done);
});
});
});
function setupReceiveStep(store, data, privKeyQueue) {
if (data.newEphemeralKey !== undefined) {
privKeyQueue.push(data.newEphemeralKey);
}
if (data.ourIdentityKey === undefined) {
return Promise.resolve();
}
return Internal.crypto.createKeyPair(data.ourIdentityKey).then(function(keyPair) {
store.put('identityKey', keyPair);
}).then(function() {
return Internal.crypto.createKeyPair(data.ourSignedPreKey);
}).then(function(signedKeyPair) {
store.storeSignedPreKey(data.signedPreKeyId, signedKeyPair);
}).then(function() {
if (data.ourPreKey !== undefined) {
return Internal.crypto.createKeyPair(data.ourPreKey).then(function(keyPair) {
store.storePreKey(data.preKeyId, keyPair);
});
}
});
}
function getPaddedMessageLength(messageLength) {
var messageLengthWithTerminator = messageLength + 1;
var messagePartCount = Math.floor(messageLengthWithTerminator / 160);
if (messageLengthWithTerminator % 160 !== 0) {
messagePartCount++;
}
return messagePartCount * 160;
}
function pad(plaintext) {
var paddedPlaintext = new Uint8Array(
getPaddedMessageLength(plaintext.byteLength + 1) - 1
);
paddedPlaintext.set(new Uint8Array(plaintext));
paddedPlaintext[plaintext.byteLength] = 0x80;
return paddedPlaintext.buffer;
}
function unpad(paddedPlaintext) {
paddedPlaintext = new Uint8Array(paddedPlaintext);
var plaintext;
for (var i = paddedPlaintext.length - 1; i >= 0; i--) {
if (paddedPlaintext[i] == 0x80) {
plaintext = new Uint8Array(i);
plaintext.set(paddedPlaintext.subarray(0, i));
plaintext = plaintext.buffer;
break;
} else if (paddedPlaintext[i] !== 0x00) {
throw new Error('Invalid padding');
}
}
return plaintext;
}
function doReceiveStep(store, data, privKeyQueue, address) {
return setupReceiveStep(store, data, privKeyQueue).then(function() {
var sessionCipher = new libsignal.SessionCipher(store, address);
if (data.type == textsecure.protobuf.IncomingPushMessageSignal.Type.CIPHERTEXT) {
return sessionCipher.decryptWhisperMessage(data.message).then(unpad);
}
else if (data.type == textsecure.protobuf.IncomingPushMessageSignal.Type.PREKEY_BUNDLE) {
return sessionCipher.decryptPreKeyWhisperMessage(data.message).then(unpad);
} else {
throw new Error("Unknown data type in test vector");
}
}).then(function checkResult(plaintext) {
var content = textsecure.protobuf.PushMessageContent.decode(plaintext);
if (data.expectTerminateSession) {
if (content.flags == textsecure.protobuf.PushMessageContent.Flags.END_SESSION) {
return true;
} else {
return false;
}
}
return content.body == data.expectedSmsText;
}).catch(function checkException(e) {
if (data.expectException) {
return true;
}
throw e;
});
}
function setupSendStep(store, data, privKeyQueue) {
if (data.registrationId !== undefined) {
store.put('registrationId', data.registrationId);
}
if (data.ourBaseKey !== undefined) {
privKeyQueue.push(data.ourBaseKey);
}
if (data.ourEphemeralKey !== undefined) {
privKeyQueue.push(data.ourEphemeralKey);
}
if (data.ourIdentityKey !== undefined) {
return Internal.crypto.createKeyPair(data.ourIdentityKey).then(function(keyPair) {
store.put('identityKey', keyPair);
});
}
return Promise.resolve();
}
function doSendStep(store, data, privKeyQueue, address) {
return setupSendStep(store, data, privKeyQueue).then(function() {
if (data.getKeys !== undefined) {
var deviceObject = {
encodedNumber : address.toString(),
identityKey : data.getKeys.identityKey,
preKey : data.getKeys.devices[0].preKey,
signedPreKey : data.getKeys.devices[0].signedPreKey,
registrationId : data.getKeys.devices[0].registrationId
};
var builder = new libsignal.SessionBuilder(store, address);
return builder.processPreKey(deviceObject);
}
}).then(function() {
var proto = new textsecure.protobuf.PushMessageContent();
if (data.endSession) {
proto.flags = textsecure.protobuf.PushMessageContent.Flags.END_SESSION;
} else {
proto.body = data.smsText;
}
var sessionCipher = new SessionCipher(store, address);
return sessionCipher.encrypt(pad(proto.toArrayBuffer())).then(function(msg) {
//XXX: This should be all we do: isEqual(data.expectedCiphertext, encryptedMsg, false);
if (msg.type == 1) {
return util.isEqual(data.expectedCiphertext, msg.body);
} else {
if (new Uint8Array(data.expectedCiphertext)[0] !== msg.body.charCodeAt(0)) {
throw new Error("Bad version byte");
}
var expected = Internal.protobuf.PreKeyWhisperMessage.decode(
data.expectedCiphertext.slice(1)
).encode();
if (!util.isEqual(expected, msg.body.substring(1))) {
throw new Error("Result does not match expected ciphertext");
}
return true;
}
}).then(function(res) {
if (data.endSession) {
return sessionCipher.closeOpenSessionForDevice().then(function() {
return res;
});
}
return res;
});
});
}
function getDescription(step) {
var direction = step[0];
var data = step[1];
if (direction === "receiveMessage") {
if (data.expectTerminateSession) {
return 'receive end session message';
} else if (data.type === 3) {
return 'receive prekey message ' + data.expectedSmsText;
} else {
return 'receive message ' + data.expectedSmsText;
}
} else if (direction === "sendMessage") {
if (data.endSession) {
return 'send end session message';
} else if (data.ourIdentityKey) {
return 'send prekey message ' + data.smsText;
} else {
return 'send message ' + data.smsText;
}
}
}
TestVectors.forEach(function(test) {
describe(test.name, function(done) {
this.timeout(20000);
var privKeyQueue = [];
var origCreateKeyPair = Internal.crypto.createKeyPair;
before(function() {
// Shim createKeyPair to return predetermined keys from
// privKeyQueue instead of random keys.
Internal.crypto.createKeyPair = function(privKey) {
if (privKey !== undefined) {
return origCreateKeyPair(privKey);
}
if (privKeyQueue.length == 0) {
throw new Error('Out of private keys');
} else {
var privKey = privKeyQueue.shift();
return Internal.crypto.createKeyPair(privKey).then(function(keyPair) {
var a = btoa(util.toString(keyPair.privKey));
var b = btoa(util.toString(privKey));
if (util.toString(keyPair.privKey) != util.toString(privKey))
throw new Error('Failed to rederive private key!');
else
return keyPair;
});
}
}
});
after(function() {
Internal.crypto.createKeyPair = origCreateKeyPair;
if (privKeyQueue.length != 0) {
throw new Error('Leftover private keys');
}
});
function describeStep(step) {
var direction = step[0];
var data = step[1];
if (direction === "receiveMessage") {
if (data.expectTerminateSession) {
return 'receive end session message';
} else if (data.type === 3) {
return 'receive prekey message ' + data.expectedSmsText;
} else {
return 'receive message ' + data.expectedSmsText;
}
} else if (direction === "sendMessage") {
if (data.endSession) {
return 'send end session message';
} else if (data.ourIdentityKey) {
return 'send prekey message ' + data.smsText;
} else {
return 'send message ' + data.smsText;
}
}
}
var store = new SignalProtocolStore();
var address = libsignal.SignalProtocolAddress.fromString("SNOWDEN.1");
test.vectors.forEach(function(step) {
it(getDescription(step), function(done) {
var doStep;
if (step[0] === "receiveMessage") {
doStep = doReceiveStep;
} else if (step[0] === "sendMessage") {
doStep = doSendStep;
} else {
throw new Error('Invalid test');
}
doStep(store, step[1], privKeyQueue, address)
.then(assert).then(done, done);
});
});
});
});
});