libsignal-protocol-javascript/src/SessionCipher.js
Scott Nonnenberg d08705329a
Throw error if asked to jump forward in chain by over 2000 (#43)
As libsignal-protocol-java has had for more than three years. This check
was present when it was first pulled out into its own repo in late 2014:

60800e1556/src/main/java/org/whispersystems/libaxolotl/SessionCipher.java (L383)
2018-02-14 11:35:16 -08:00

440 lines
19 KiB
JavaScript

function SessionCipher(storage, remoteAddress, options) {
options = options || {};
if (typeof options.messageKeysLimit === 'undefined') {
options.messageKeysLimit = 1000;
}
this.messageKeysLimit = options.messageKeysLimit;
this.remoteAddress = remoteAddress;
this.storage = storage;
}
SessionCipher.prototype = {
getRecord: function(encodedNumber) {
return this.storage.loadSession(encodedNumber).then(function(serialized) {
if (serialized === undefined) {
return undefined;
}
return Internal.SessionRecord.deserialize(serialized);
});
},
encrypt: function(buffer, encoding) {
buffer = dcodeIO.ByteBuffer.wrap(buffer, encoding).toArrayBuffer();
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
if (!(buffer instanceof ArrayBuffer)) {
throw new Error("Expected buffer to be an ArrayBuffer");
}
var address = this.remoteAddress.toString();
var ourIdentityKey, myRegistrationId, record, session, chain;
var msg = new Internal.protobuf.WhisperMessage();
return Promise.all([
this.storage.getIdentityKeyPair(),
this.storage.getLocalRegistrationId(),
this.getRecord(address)
]).then(function(results) {
ourIdentityKey = results[0];
myRegistrationId = results[1];
record = results[2];
if (!record) {
throw new Error("No record for " + address);
}
session = record.getOpenSession();
if (!session) {
throw new Error("No session to encrypt message for " + address);
}
msg.ephemeralKey = util.toArrayBuffer(
session.currentRatchet.ephemeralKeyPair.pubKey
);
chain = session[util.toString(msg.ephemeralKey)];
if (chain.chainType === Internal.ChainType.RECEIVING) {
throw new Error("Tried to encrypt on a receiving chain");
}
return this.fillMessageKeys(chain, chain.chainKey.counter + 1);
}.bind(this)).then(function() {
return Internal.HKDF(
util.toArrayBuffer(chain.messageKeys[chain.chainKey.counter]),
new ArrayBuffer(32), "WhisperMessageKeys");
}).then(function(keys) {
delete chain.messageKeys[chain.chainKey.counter];
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
return Internal.crypto.encrypt(
keys[0], buffer, keys[2].slice(0, 16)
).then(function(ciphertext) {
msg.ciphertext = ciphertext;
var encodedMsg = msg.toArrayBuffer();
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
macInput.set(new Uint8Array(util.toArrayBuffer(ourIdentityKey.pubKey)));
macInput.set(new Uint8Array(util.toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(encodedMsg), 33*2 + 1);
return Internal.crypto.sign(keys[1], macInput.buffer).then(function(mac) {
var result = new Uint8Array(encodedMsg.byteLength + 9);
result[0] = (3 << 4) | 3;
result.set(new Uint8Array(encodedMsg), 1);
result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
return this.storage.isTrustedIdentity(
this.remoteAddress.getName(), util.toArrayBuffer(session.indexInfo.remoteIdentityKey), this.storage.Direction.SENDING
).then(function(trusted) {
if (!trusted) {
throw new Error('Identity key changed');
}
}).then(function() {
return this.storage.saveIdentity(this.remoteAddress.toString(), session.indexInfo.remoteIdentityKey);
}.bind(this)).then(function() {
record.updateSessionState(session);
return this.storage.storeSession(address, record.serialize()).then(function() {
return result;
});
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this)).then(function(message) {
if (session.pendingPreKey !== undefined) {
var preKeyMsg = new Internal.protobuf.PreKeyWhisperMessage();
preKeyMsg.identityKey = util.toArrayBuffer(ourIdentityKey.pubKey);
preKeyMsg.registrationId = myRegistrationId;
preKeyMsg.baseKey = util.toArrayBuffer(session.pendingPreKey.baseKey);
if (session.pendingPreKey.preKeyId) {
preKeyMsg.preKeyId = session.pendingPreKey.preKeyId;
}
preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId;
preKeyMsg.message = message;
var result = String.fromCharCode((3 << 4) | 3) + util.toString(preKeyMsg.encode());
return {
type : 3,
body : result,
registrationId : session.registrationId
};
} else {
return {
type : 1,
body : util.toString(message),
registrationId : session.registrationId
};
}
});
}.bind(this));
},
decryptWithSessionList: function(buffer, sessionList, errors) {
// Iterate recursively through the list, attempting to decrypt
// using each one at a time. Stop and return the result if we get
// a valid result
if (sessionList.length === 0) {
return Promise.reject(errors[0]);
}
var session = sessionList.pop();
return this.doDecryptWhisperMessage(buffer, session).then(function(plaintext) {
return { plaintext: plaintext, session: session };
}).catch(function(e) {
if (e.name === 'MessageCounterError') {
return Promise.reject(e);
}
errors.push(e);
return this.decryptWithSessionList(buffer, sessionList, errors);
}.bind(this));
},
decryptWhisperMessage: function(buffer, encoding) {
buffer = dcodeIO.ByteBuffer.wrap(buffer, encoding).toArrayBuffer();
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
var address = this.remoteAddress.toString();
return this.getRecord(address).then(function(record) {
if (!record) {
throw new Error("No record for device " + address);
}
var errors = [];
return this.decryptWithSessionList(buffer, record.getSessions(), errors).then(function(result) {
return this.getRecord(address).then(function(record) {
if (result.session.indexInfo.baseKey !== record.getOpenSession().indexInfo.baseKey) {
record.archiveCurrentState();
record.promoteState(result.session);
}
return this.storage.isTrustedIdentity(
this.remoteAddress.getName(), util.toArrayBuffer(result.session.indexInfo.remoteIdentityKey), this.storage.Direction.RECEIVING
).then(function(trusted) {
if (!trusted) {
throw new Error('Identity key changed');
}
}).then(function() {
return this.storage.saveIdentity(this.remoteAddress.toString(), result.session.indexInfo.remoteIdentityKey);
}.bind(this)).then(function() {
record.updateSessionState(result.session);
return this.storage.storeSession(address, record.serialize()).then(function() {
return result.plaintext;
});
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
},
decryptPreKeyWhisperMessage: function(buffer, encoding) {
buffer = dcodeIO.ByteBuffer.wrap(buffer, encoding);
var version = buffer.readUint8();
if ((version & 0xF) > 3 || (version >> 4) < 3) { // min version > 3 or max version < 3
throw new Error("Incompatible version number on PreKeyWhisperMessage");
}
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
var address = this.remoteAddress.toString();
return this.getRecord(address).then(function(record) {
var preKeyProto = Internal.protobuf.PreKeyWhisperMessage.decode(buffer);
if (!record) {
if (preKeyProto.registrationId === undefined) {
throw new Error("No registrationId");
}
record = new Internal.SessionRecord(
preKeyProto.registrationId
);
}
var builder = new SessionBuilder(this.storage, this.remoteAddress);
// isTrustedIdentity is called within processV3, no need to call it here
return builder.processV3(record, preKeyProto).then(function(preKeyId) {
var session = record.getSessionByBaseKey(preKeyProto.baseKey);
return this.doDecryptWhisperMessage(
preKeyProto.message.toArrayBuffer(), session
).then(function(plaintext) {
record.updateSessionState(session);
return this.storage.storeSession(address, record.serialize()).then(function() {
if (preKeyId !== undefined && preKeyId !== null) {
return this.storage.removePreKey(preKeyId);
}
}.bind(this)).then(function() {
return plaintext;
});
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
},
doDecryptWhisperMessage: function(messageBytes, session) {
if (!(messageBytes instanceof ArrayBuffer)) {
throw new Error("Expected messageBytes to be an ArrayBuffer");
}
var version = (new Uint8Array(messageBytes))[0];
if ((version & 0xF) > 3 || (version >> 4) < 3) { // min version > 3 or max version < 3
throw new Error("Incompatible version number on WhisperMessage");
}
var messageProto = messageBytes.slice(1, messageBytes.byteLength- 8);
var mac = messageBytes.slice(messageBytes.byteLength - 8, messageBytes.byteLength);
var message = Internal.protobuf.WhisperMessage.decode(messageProto);
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
if (session === undefined) {
return Promise.reject(new Error("No session found to decrypt message from " + this.remoteAddress.toString()));
}
if (session.indexInfo.closed != -1) {
console.log('decrypting message for closed session');
}
return this.maybeStepRatchet(session, remoteEphemeralKey, message.previousCounter).then(function() {
var chain = session[util.toString(message.ephemeralKey)];
if (chain.chainType === Internal.ChainType.SENDING) {
throw new Error("Tried to decrypt on a sending chain");
}
return this.fillMessageKeys(chain, message.counter).then(function() {
var messageKey = chain.messageKeys[message.counter];
if (messageKey === undefined) {
var e = new Error("Message key not found. The counter was repeated or the key was not filled.");
e.name = 'MessageCounterError';
throw e;
}
delete chain.messageKeys[message.counter];
return Internal.HKDF(util.toArrayBuffer(messageKey), new ArrayBuffer(32), "WhisperMessageKeys");
});
}.bind(this)).then(function(keys) {
return this.storage.getIdentityKeyPair().then(function(ourIdentityKey) {
var macInput = new Uint8Array(messageProto.byteLength + 33*2 + 1);
macInput.set(new Uint8Array(util.toArrayBuffer(session.indexInfo.remoteIdentityKey)));
macInput.set(new Uint8Array(util.toArrayBuffer(ourIdentityKey.pubKey)), 33);
macInput[33*2] = (3 << 4) | 3;
macInput.set(new Uint8Array(messageProto), 33*2 + 1);
return Internal.verifyMAC(macInput.buffer, keys[1], mac, 8);
}.bind(this)).then(function() {
return Internal.crypto.decrypt(keys[0], message.ciphertext.toArrayBuffer(), keys[2].slice(0, 16));
});
}.bind(this)).then(function(plaintext) {
delete session.pendingPreKey;
return plaintext;
});
},
fillMessageKeys: function(chain, counter) {
if (this.messageKeysLimit && Object.keys(chain.messageKeys).length >= this.messageKeysLimit) {
console.log("Too many message keys for chain");
return Promise.resolve(); // Stalker, much?
}
if (chain.chainKey.counter >= counter) {
return Promise.resolve(); // Already calculated
}
if (counter - chain.chainKey.counter > 2000) {
throw new Error('Over 2000 messages into the future!');
}
if (chain.chainKey.key === undefined) {
throw new Error("Got invalid request to extend chain after it was already closed");
}
var key = util.toArrayBuffer(chain.chainKey.key);
var byteArray = new Uint8Array(1);
byteArray[0] = 1;
return Internal.crypto.sign(key, byteArray.buffer).then(function(mac) {
byteArray[0] = 2;
return Internal.crypto.sign(key, byteArray.buffer).then(function(key) {
chain.messageKeys[chain.chainKey.counter + 1] = mac;
chain.chainKey.key = key;
chain.chainKey.counter += 1;
return this.fillMessageKeys(chain, counter);
}.bind(this));
}.bind(this));
},
maybeStepRatchet: function(session, remoteKey, previousCounter) {
if (session[util.toString(remoteKey)] !== undefined) {
return Promise.resolve();
}
console.log('New remote ephemeral key');
var ratchet = session.currentRatchet;
return Promise.resolve().then(function() {
var previousRatchet = session[util.toString(ratchet.lastRemoteEphemeralKey)];
if (previousRatchet !== undefined) {
return this.fillMessageKeys(previousRatchet, previousCounter).then(function() {
delete previousRatchet.chainKey.key;
session.oldRatchetList[session.oldRatchetList.length] = {
added : Date.now(),
ephemeralKey : ratchet.lastRemoteEphemeralKey
};
});
}
}.bind(this)).then(function() {
return this.calculateRatchet(session, remoteKey, false).then(function() {
// Now swap the ephemeral key and calculate the new sending chain
var previousRatchet = util.toString(ratchet.ephemeralKeyPair.pubKey);
if (session[previousRatchet] !== undefined) {
ratchet.previousCounter = session[previousRatchet].chainKey.counter;
delete session[previousRatchet];
}
return Internal.crypto.createKeyPair().then(function(keyPair) {
ratchet.ephemeralKeyPair = keyPair;
return this.calculateRatchet(session, remoteKey, true).then(function() {
ratchet.lastRemoteEphemeralKey = remoteKey;
}.bind(this));
}.bind(this));
}.bind(this));
}.bind(this));
},
calculateRatchet: function(session, remoteKey, sending) {
var ratchet = session.currentRatchet;
return Internal.crypto.ECDHE(remoteKey, util.toArrayBuffer(ratchet.ephemeralKeyPair.privKey)).then(function(sharedSecret) {
return Internal.HKDF(sharedSecret, util.toArrayBuffer(ratchet.rootKey), "WhisperRatchet").then(function(masterKey) {
var ephemeralPublicKey;
if (sending) {
ephemeralPublicKey = ratchet.ephemeralKeyPair.pubKey;
}
else {
ephemeralPublicKey = remoteKey;
}
session[util.toString(ephemeralPublicKey)] = {
messageKeys: {},
chainKey: { counter: -1, key: masterKey[1] },
chainType: sending ? Internal.ChainType.SENDING : Internal.ChainType.RECEIVING
};
ratchet.rootKey = masterKey[0];
});
});
},
getRemoteRegistrationId: function() {
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
if (record === undefined) {
return undefined;
}
var openSession = record.getOpenSession();
if (openSession === undefined) {
return null;
}
return openSession.registrationId;
});
}.bind(this));
},
hasOpenSession: function() {
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
if (record === undefined) {
return false;
}
return record.haveOpenSession();
});
}.bind(this));
},
closeOpenSessionForDevice: function() {
var address = this.remoteAddress.toString();
return Internal.SessionLock.queueJobForNumber(address, function() {
return this.getRecord(address).then(function(record) {
if (record === undefined || record.getOpenSession() === undefined) {
return;
}
record.archiveCurrentState();
return this.storage.storeSession(address, record.serialize());
}.bind(this));
}.bind(this));
},
deleteAllSessionsForDevice: function() {
// Used in session reset scenarios, where we really need to delete
var address = this.remoteAddress.toString();
return Internal.SessionLock.queueJobForNumber(address, function() {
return this.getRecord(address).then(function(record) {
if (record === undefined) {
return;
}
record.deleteAllSessions();
return this.storage.storeSession(address, record.serialize());
}.bind(this));
}.bind(this));
}
};
libsignal.SessionCipher = function(storage, remoteAddress, options) {
var cipher = new SessionCipher(storage, remoteAddress, options);
// returns a Promise that resolves to a ciphertext object
this.encrypt = cipher.encrypt.bind(cipher);
// returns a Promise that inits a session if necessary and resolves
// to a decrypted plaintext array buffer
this.decryptPreKeyWhisperMessage = cipher.decryptPreKeyWhisperMessage.bind(cipher);
// returns a Promise that resolves to decrypted plaintext array buffer
this.decryptWhisperMessage = cipher.decryptWhisperMessage.bind(cipher);
this.getRemoteRegistrationId = cipher.getRemoteRegistrationId.bind(cipher);
this.hasOpenSession = cipher.hasOpenSession.bind(cipher);
this.closeOpenSessionForDevice = cipher.closeOpenSessionForDevice.bind(cipher);
this.deleteAllSessionsForDevice = cipher.deleteAllSessionsForDevice.bind(cipher);
};