Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Break loop if error name is "MessageCounterError", etc... #4

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 34 additions & 25 deletions src/session_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const crypto = require('./crypto');
const curve = require('./curve');
const errors = require('./errors');
const queueJob = require('./queue_job');
const Util = require('./util');


class SessionBuilder {
Expand All @@ -24,12 +25,12 @@ class SessionBuilder {
throw new errors.UntrustedIdentityKeyError(this.addr.id, device.identityKey);
}
curve.verifySignature(device.identityKey, device.signedPreKey.publicKey,
device.signedPreKey.signature);
device.signedPreKey.signature);
const baseKey = curve.generateKeyPair();
const devicePreKey = device.preKey && device.preKey.publicKey;
const session = await this.initSession(true, baseKey, undefined, device.identityKey,
devicePreKey, device.signedPreKey.publicKey,
device.registrationId);
devicePreKey, device.signedPreKey.publicKey,
device.registrationId);
session.pendingPreKey = {
signedKeyId: device.signedPreKey.keyId,
baseKey: baseKey.pubKey
Expand All @@ -40,14 +41,14 @@ class SessionBuilder {
let record = await this.storage.loadSession(fqAddr);
if (!record) {
record = new SessionRecord();
} else {
const openSession = record.getOpenSession();
if (openSession) {
console.warn("Closing stale open session for new outgoing prekey bundle");
record.closeSession(openSession);
}
}
record.setSession(session);
const openSession = record.getOpenSession();
record.archiveCurrentState();
if (openSession && session && !Util.isEqual(openSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) {
console.warn("Deleting all sessions because identity has changed");
record.deleteAllSessions();
}
record.updateSessionState(session);
await this.storage.storeSession(fqAddr, record);
});
}
Expand All @@ -61,27 +62,35 @@ class SessionBuilder {
// This just means we haven't replied.
return;
}
const preKeyPair = await this.storage.loadPreKey(message.preKeyId);
if (message.preKeyId && !preKeyPair) {
throw new errors.PreKeyError('Invalid PreKey ID');
}
const signedPreKeyPair = await this.storage.loadSignedPreKey(message.signedPreKeyId);
if (!signedPreKeyPair) {
throw new errors.PreKeyError("Missing SignedPreKey");
}
const [preKeyPair, signedPreKeyPair] = await Promise.all([
this.storage.loadPreKey(message.preKeyId),
this.storage.loadSignedPreKey(message.signedPreKeyId)
]);
const existingOpenSession = record.getOpenSession();
if (!signedPreKeyPair) {
if (existingOpenSession && existingOpenSession.currentRatchet) return;
throw new errors.PreKeyError("Missing Signed PreKey for PreKeyWhisperMessage");
}
if (existingOpenSession) {
console.warn("Closing open session in favor of incoming prekey bundle");
record.closeSession(existingOpenSession);
record.archiveCurrentState();
}
if (message.preKeyId && !preKeyPair) {
throw new errors.PreKeyError("Invalid PreKey ID");
}
const session = await this.initSession(false, preKeyPair, signedPreKeyPair,
message.identityKey, message.baseKey,
undefined, message.registrationId);
if (existingOpenSession && session && !Util.isEqual(existingOpenSession.indexInfo.remoteIdentityKey, session.indexInfo.remoteIdentityKey)) {
console.warn("Deleting all sessions because identity has changed");
record.deleteAllSessions();
}
record.setSession(await this.initSession(false, preKeyPair, signedPreKeyPair,
message.identityKey, message.baseKey,
undefined, message.registrationId));
record.updateSessionState(session);
// this.storage.saveIdentity
return message.preKeyId;
}

async initSession(isInitiator, ourEphemeralKey, ourSignedKey, theirIdentityPubKey,
theirEphemeralPubKey, theirSignedPubKey, registrationId) {
theirEphemeralPubKey, theirSignedPubKey, registrationId) {
if (isInitiator) {
if (ourSignedKey) {
throw new Error("Invalid call to initSession");
Expand Down Expand Up @@ -119,7 +128,7 @@ class SessionBuilder {
sharedSecret.set(new Uint8Array(a4), 32 * 4);
}
const masterKey = crypto.deriveSecrets(Buffer.from(sharedSecret), Buffer.alloc(32),
Buffer.from("WhisperText"));
Buffer.from("WhisperText"));
const session = SessionRecord.createEntry();
session.registrationId = registrationId;
session.currentRatchet = {
Expand Down
115 changes: 69 additions & 46 deletions src/session_cipher.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const ChainType = require('./chain_type');
const ProtocolAddress = require('./protocol_address');
const SessionBuilder = require('./session_builder');
const SessionRecord = require('./session_record');
const Util = require('./util');
const crypto = require('./crypto');
const curve = require('./curve');
const errors = require('./errors');
Expand Down Expand Up @@ -45,6 +46,7 @@ class SessionCipher {
return `<SessionCipher(${this.addr.toString()})>`;
}

/** @returns {Promise<import('./session_record')>} */
async getRecord() {
const record = await this.storage.loadSession(this.addr.toString());
if (record && !(record instanceof SessionRecord)) {
Expand All @@ -64,51 +66,60 @@ class SessionCipher {

async encrypt(data) {
assertBuffer(data);
const ourIdentityKey = await this.storage.getOurIdentity();
return await this.queueJob(async () => {
const record = await this.getRecord();
const [ourIdentityKey, ourRegistrationId, record] = await Promise.all([
this.storage.getOurIdentity(),
this.storage.getOurRegistrationId(),
this.getRecord()
]);
if (!record) {
throw new errors.SessionError("No sessions");
}
const session = record.getOpenSession();
if (!session) {
throw new errors.SessionError("No open session");
}
const remoteIdentityKey = session.indexInfo.remoteIdentityKey;
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
}
const chain = session.getChain(session.currentRatchet.ephemeralKeyPair.pubKey);
if (chain.chainType === ChainType.RECEIVING) {
throw new Error("Tried to encrypt on a receiving chain");
}
this.fillMessageKeys(chain, chain.chainKey.counter + 1);
const keys = crypto.deriveSecrets(chain.messageKeys[chain.chainKey.counter],
Buffer.alloc(32), Buffer.from("WhisperMessageKeys"));
delete chain.messageKeys[chain.chainKey.counter];
const msg = protobufs.WhisperMessage.create();
msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey;
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
msg.ciphertext = crypto.encrypt(keys[0], data, keys[2].slice(0, 16));
const msgBuf = protobufs.WhisperMessage.encode(msg).finish();
const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1);
macInput.set(ourIdentityKey.pubKey);
macInput.set(session.indexInfo.remoteIdentityKey, 33);
macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION);
Buffer.alloc(32), Buffer.from("WhisperMessageKeys"));
delete chain.messageKeys[chain.chainKey.counter];
const msg = protobufs.WhisperMessage.create();
msg.ephemeralKey = session.currentRatchet.ephemeralKeyPair.pubKey;
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
msg.ciphertext = crypto.encrypt(keys[0], data, keys[2].slice(0, 16));
const msgBuf = protobufs.WhisperMessage.encode(msg).finish();
const macInput = Buffer.alloc(msgBuf.byteLength + (33 * 2) + 1);
macInput.set(ourIdentityKey.pubKey);
macInput.set(session.indexInfo.remoteIdentityKey, 33);
macInput[33 * 2] = this._encodeTupleByte(VERSION, VERSION); // 51
macInput.set(msgBuf, (33 * 2) + 1);
const mac = crypto.calculateMAC(keys[1], macInput);
const result = Buffer.alloc(msgBuf.byteLength + 9);
result[0] = this._encodeTupleByte(VERSION, VERSION);
result.set(msgBuf, 1);
result.set(mac.slice(0, 8), msgBuf.byteLength + 1);

const remoteIdentityKey = session.indexInfo.remoteIdentityKey;
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
}

// this.storage.saveIdentity(session.indexInfo.remoteIdentityKey)

record.updateSessionState(session);
await this.storeRecord(record);

let type, body;
if (session.pendingPreKey) {
type = 3; // prekey bundle
const preKeyMsg = protobufs.PreKeyWhisperMessage.create({
identityKey: ourIdentityKey.pubKey,
registrationId: await this.storage.getOurRegistrationId(),
registrationId: ourRegistrationId,
baseKey: session.pendingPreKey.baseKey,
signedPreKeyId: session.pendingPreKey.signedKeyId,
message: result
Expand All @@ -134,31 +145,26 @@ class SessionCipher {
});
}

async decryptWithSessions(data, sessions) {
async decryptWithSessions(data, sessions, errors = []) {
// Iterate through the sessions, attempting to decrypt using each one.
// Stop and return the result if we get a valid result.
if (!sessions.length) {
throw new errors.SessionError("No sessions available");
throw new errors.SessionError(errors[0] || "No sessions available");
}
const errs = [];
for (const session of sessions) {
let plaintext;
try {
plaintext = await this.doDecryptWhisperMessage(data, session);
session.indexInfo.used = Date.now();
return {
session,
plaintext
};
} catch(e) {
errs.push(e);
}
}
console.error("Failed to decrypt message with any known session...");
for (const e of errs) {
console.error("Session error:" + e, e.stack);
const session = sessions.pop();
try {
const plaintext = await this.doDecryptWhisperMessage(data, session);
session.indexInfo.used = Date.now();
return {
session,
plaintext
};
} catch (e) {
if (e.name === "MessageCounterError")
throw e;
errors.push(e);
return await this.decryptWithSessions(data, sessions, errors);
}
throw new errors.SessionError("No matching sessions found for message");
}

async decryptWhisperMessage(data) {
Expand All @@ -169,6 +175,11 @@ class SessionCipher {
throw new errors.SessionError("No session record");
}
const result = await this.decryptWithSessions(data, record.getSessions());
const session = (await this.getRecord()).getOpenSession();
if (result.session.indexInfo.baseKey != session.indexInfo.baseKey) {
record.archiveCurrentState();
record.openSession(result.session);
}
const remoteIdentityKey = result.session.indexInfo.remoteIdentityKey;
if (!await this.storage.isTrustedIdentity(this.addr.id, remoteIdentityKey)) {
throw new errors.UntrustedIdentityKeyError(this.addr.id, remoteIdentityKey);
Expand All @@ -181,6 +192,8 @@ class SessionCipher {
// a full SessionError response.
console.warn("Decrypted message with closed session.");
}
// this.storage.saveIdentity
record.updateSessionState(result.session);
await this.storeRecord(record);
return result.plaintext;
});
Expand All @@ -205,6 +218,16 @@ class SessionCipher {
const preKeyId = await builder.initIncoming(record, preKeyProto);
const session = record.getSession(preKeyProto.baseKey);
const plaintext = await this.doDecryptWhisperMessage(preKeyProto.message, session);
record.updateSessionState(session);

const openSession = record.getOpenSession();
if (session && openSession && !Util.isEqual(session.indexInfo.remoteIdentityKey, openSession.indexInfo.remoteIdentityKey)) {
console.warn("Promote the old session and update identity");
record.archiveCurrentState();
record.openSession(session);
// this.storage.saveIdentity
}

await this.storeRecord(record);
if (preKeyId) {
await this.storage.removePreKey(preKeyId);
Expand All @@ -216,15 +239,15 @@ class SessionCipher {
async doDecryptWhisperMessage(messageBuffer, session) {
assertBuffer(messageBuffer);
if (!session) {
throw new TypeError("session required");
throw new Error("No session found to decrypt message from " + this.addr.toString())
}
const versions = this._decodeTupleByte(messageBuffer[0]);
if (versions[1] > 3 || versions[0] < 3) { // min version > 3 or max version < 3
throw new Error("Incompatible version number on WhisperMessage");
}
const messageProto = messageBuffer.slice(1, -8);
const message = protobufs.WhisperMessage.decode(messageProto);
this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter);
await this.maybeStepRatchet(session, message.ephemeralKey, message.previousCounter);
const chain = session.getChain(message.ephemeralKey);
if (chain.chainType === ChainType.SENDING) {
throw new Error("Tried to decrypt on a sending chain");
Expand All @@ -233,7 +256,7 @@ class SessionCipher {
if (!chain.messageKeys.hasOwnProperty(message.counter)) {
// Most likely the message was already decrypted and we are trying to process
// twice. This can happen if the user restarts before the server gets an ACK.
throw new errors.MessageCounterError('Key used already or never filled');
throw new errors.MessageCounterError("Message key not found. The counter was repeated or the key was not filled.");
}
const messageKey = chain.messageKeys[message.counter];
delete chain.messageKeys[message.counter];
Expand All @@ -258,10 +281,10 @@ class SessionCipher {
return;
}
if (counter - chain.chainKey.counter > 2000) {
throw new errors.SessionError('Over 2000 messages into the future!');
throw new errors.SessionError("Over 2000 messages into the future!");
}
if (chain.chainKey.key === undefined) {
throw new errors.SessionError('Chain closed');
throw new errors.SessionError("Got invalid request to extend chain after it was already closed");
}
const key = chain.chainKey.key;
chain.messageKeys[chain.chainKey.counter + 1] = crypto.calculateMAC(key, Buffer.from([1]));
Expand All @@ -270,7 +293,7 @@ class SessionCipher {
return this.fillMessageKeys(chain, counter);
}

maybeStepRatchet(session, remoteKey, previousCounter) {
async maybeStepRatchet(session, remoteKey, previousCounter) {
if (session.getChain(remoteKey)) {
return;
}
Expand Down Expand Up @@ -325,7 +348,7 @@ class SessionCipher {
if (record) {
const openSession = record.getOpenSession();
if (openSession) {
record.closeSession(openSession);
record.archiveCurrentState();
await this.storeRecord(record);
}
}
Expand Down
Loading