This was mostly used internally by SessionRecord to flag the lack of an open session, but that can be replaced with an actual check for an open session.
369 lines
15 KiB
JavaScript
369 lines
15 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 an open record exists', function() {
|
|
before(function(done) {
|
|
var record = new Internal.SessionRecord(registrationId);
|
|
var session = {
|
|
registrationId: registrationId,
|
|
currentRatchet: {
|
|
rootKey : new ArrayBuffer(32),
|
|
lastRemoteEphemeralKey : new ArrayBuffer(32),
|
|
previousCounter : 0
|
|
},
|
|
indexInfo: {
|
|
baseKey : new ArrayBuffer(32),
|
|
baseKeyType : Internal.BaseKeyType.OURS,
|
|
remoteIdentityKey : new ArrayBuffer(32),
|
|
closed : -1
|
|
},
|
|
oldRatchetList: []
|
|
};
|
|
record.updateSessionState(session);
|
|
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('open session exists', function() {
|
|
before(function(done) {
|
|
var record = new Internal.SessionRecord();
|
|
var session = {
|
|
registrationId: 1337,
|
|
currentRatchet: {
|
|
rootKey : new ArrayBuffer(32),
|
|
lastRemoteEphemeralKey : new ArrayBuffer(32),
|
|
previousCounter : 0
|
|
},
|
|
indexInfo: {
|
|
baseKey : new ArrayBuffer(32),
|
|
baseKeyType : Internal.BaseKeyType.OURS,
|
|
remoteIdentityKey : new ArrayBuffer(32),
|
|
closed : -1
|
|
},
|
|
oldRatchetList: []
|
|
};
|
|
record.updateSessionState(session);
|
|
store.storeSession(address.toString(), record.serialize()).then(done);
|
|
});
|
|
it('returns true', function(done) {
|
|
sessionCipher.hasOpenSession(address.toString()).then(function(value) {
|
|
assert.isTrue(value);
|
|
}).then(done,done);
|
|
});
|
|
});
|
|
describe('no open session exists', function() {
|
|
before(function(done) {
|
|
var record = new Internal.SessionRecord();
|
|
store.storeSession(address.toString(), record.serialize()).then(done);
|
|
});
|
|
it('returns false', 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);
|
|
});
|
|
});
|
|
});
|
|
});
|
|
});
|