Update libsignal-protocol v0.6.0

Adds session accessors on SessionCipher and an internal session lock to
replace the same implemented in protocol_wrapper.js

// FREEBIE
This commit is contained in:
lilia 2016-05-13 19:33:24 -07:00
parent 3d3cbb45b7
commit 284cf5be3a
2 changed files with 452 additions and 316 deletions

View file

@ -34864,48 +34864,50 @@ function SessionBuilder(storage, remoteAddress) {
SessionBuilder.prototype = { SessionBuilder.prototype = {
processPreKey: function(device) { processPreKey: function(device) {
return this.storage.isTrustedIdentity( return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
this.remoteAddress.getName(), device.identityKey return this.storage.isTrustedIdentity(
).then(function(trusted) { this.remoteAddress.getName(), device.identityKey
if (!trusted) { ).then(function(trusted) {
throw new Error('Identity key changed'); if (!trusted) {
} throw new Error('Identity key changed');
return Internal.crypto.Ed25519Verify(
device.identityKey,
device.signedPreKey.publicKey,
device.signedPreKey.signature
);
}).then(function() {
return Internal.crypto.createKeyPair();
}).then(function(baseKey) {
var devicePreKey = (device.preKey.publicKey);
return this.initSession(true, baseKey, undefined, device.identityKey,
devicePreKey, device.signedPreKey.publicKey
).then(function(session) {
session.pendingPreKey = {
preKeyId : device.preKey.keyId,
signedKeyId : device.signedPreKey.keyId,
baseKey : baseKey.pubKey
};
return session;
});
}.bind(this)).then(function(session) {
var address = this.remoteAddress.toString();
return this.storage.loadSession(address).then(function(serialized) {
var record;
if (serialized !== undefined) {
record = Internal.SessionRecord.deserialize(serialized);
} else {
record = new Internal.SessionRecord(device.identityKey, device.registrationId);
} }
record.archiveCurrentState(); return Internal.crypto.Ed25519Verify(
record.updateSessionState(session, device.registrationId); device.identityKey,
return Promise.all([ device.signedPreKey.publicKey,
this.storage.storeSession(address, record.serialize()), device.signedPreKey.signature
this.storage.putIdentityKey(this.remoteAddress.getName(), record.identityKey) );
]); }).then(function() {
return Internal.crypto.createKeyPair();
}).then(function(baseKey) {
var devicePreKey = (device.preKey.publicKey);
return this.initSession(true, baseKey, undefined, device.identityKey,
devicePreKey, device.signedPreKey.publicKey
).then(function(session) {
session.pendingPreKey = {
preKeyId : device.preKey.keyId,
signedKeyId : device.signedPreKey.keyId,
baseKey : baseKey.pubKey
};
return session;
});
}.bind(this)).then(function(session) {
var address = this.remoteAddress.toString();
return this.storage.loadSession(address).then(function(serialized) {
var record;
if (serialized !== undefined) {
record = Internal.SessionRecord.deserialize(serialized);
} else {
record = new Internal.SessionRecord(device.identityKey, device.registrationId);
}
record.archiveCurrentState();
record.updateSessionState(session, device.registrationId);
return Promise.all([
this.storage.storeSession(address, record.serialize()),
this.storage.putIdentityKey(this.remoteAddress.getName(), record.identityKey)
]);
}.bind(this));
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -35070,93 +35072,94 @@ SessionCipher.prototype = {
}); });
}, },
encrypt: function(plaintext) { encrypt: function(plaintext) {
if (!(plaintext instanceof ArrayBuffer)) { return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
throw new Error("Expected plaintext to be an ArrayBuffer"); if (!(plaintext instanceof ArrayBuffer)) {
} throw new Error("Expected plaintext to be an ArrayBuffer");
}
var address = this.remoteAddress.toString(); var address = this.remoteAddress.toString();
var ourIdentityKey, myRegistrationId, record, session; var ourIdentityKey, myRegistrationId, record, session, chain;
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);
}
var msg = new Internal.protobuf.WhisperMessage(); var msg = new Internal.protobuf.WhisperMessage();
var paddedPlaintext = new Uint8Array( var paddedPlaintext = new Uint8Array(
this.getPaddedMessageLength(plaintext.byteLength + 1) - 1 this.getPaddedMessageLength(plaintext.byteLength + 1) - 1
); );
paddedPlaintext.set(new Uint8Array(plaintext)); paddedPlaintext.set(new Uint8Array(plaintext));
paddedPlaintext[plaintext.byteLength] = 0x80; paddedPlaintext[plaintext.byteLength] = 0x80;
msg.ephemeralKey = util.toArrayBuffer( return Promise.all([
session.currentRatchet.ephemeralKeyPair.pubKey this.storage.getIdentityKeyPair(),
); this.storage.getLocalRegistrationId(),
var chain = session[util.toString(msg.ephemeralKey)]; 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);
}
return this.fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() { msg.ephemeralKey = util.toArrayBuffer(
return Internal.HKDF(util.toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), session.currentRatchet.ephemeralKeyPair.pubKey
new ArrayBuffer(32), "WhisperMessageKeys" );
).then(function(keys) { chain = session[util.toString(msg.ephemeralKey)];
delete chain.messageKeys[chain.chainKey.counter];
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
return Internal.crypto.encrypt( return this.fillMessageKeys(chain, chain.chainKey.counter + 1);
keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16) }.bind(this)).then(function() {
).then(function(ciphertext) { return Internal.HKDF(
msg.ciphertext = ciphertext; util.toArrayBuffer(chain.messageKeys[chain.chainKey.counter]),
var encodedMsg = util.toArrayBuffer(msg.encode()); new ArrayBuffer(32), "WhisperMessageKeys");
}).then(function(keys) {
delete chain.messageKeys[chain.chainKey.counter];
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1); return Internal.crypto.encrypt(
macInput.set(new Uint8Array(util.toArrayBuffer(ourIdentityKey.pubKey))); keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)
macInput.set(new Uint8Array(util.toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33); ).then(function(ciphertext) {
macInput[33*2] = (3 << 4) | 3; msg.ciphertext = ciphertext;
macInput.set(new Uint8Array(encodedMsg), 33*2 + 1); var encodedMsg = msg.toArrayBuffer();
return Internal.crypto.sign( var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
keys[1], macInput.buffer macInput.set(new Uint8Array(util.toArrayBuffer(ourIdentityKey.pubKey)));
).then(function(mac) { macInput.set(new Uint8Array(util.toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
var result = new Uint8Array(encodedMsg.byteLength + 9); macInput[33*2] = (3 << 4) | 3;
result[0] = (3 << 4) | 3; macInput.set(new Uint8Array(encodedMsg), 33*2 + 1);
result.set(new Uint8Array(encodedMsg), 1);
result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
record.updateSessionState(session); return Internal.crypto.sign(keys[1], macInput.buffer).then(function(mac) {
return this.storage.storeSession(address, record.serialize()).then(function() { var result = new Uint8Array(encodedMsg.byteLength + 9);
return result; result[0] = (3 << 4) | 3;
}); result.set(new Uint8Array(encodedMsg), 1);
}.bind(this)); result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
}.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); record.updateSessionState(session);
preKeyMsg.preKeyId = session.pendingPreKey.preKeyId; return this.storage.storeSession(address, record.serialize()).then(function() {
preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId; return result;
});
}.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.message = message; preKeyMsg.baseKey = util.toArrayBuffer(session.pendingPreKey.baseKey);
var result = String.fromCharCode((3 << 4) | 3) + util.toString(preKeyMsg.encode()); preKeyMsg.preKeyId = session.pendingPreKey.preKeyId;
return {type: 3, body: result}; preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId;
} else {
return {type: 1, body: util.toString(message)}; preKeyMsg.message = message;
} var result = String.fromCharCode((3 << 4) | 3) + util.toString(preKeyMsg.encode());
}); return {type: 3, body: result};
} else {
return {type: 1, body: util.toString(message)};
}
});
}.bind(this));
}, },
getPaddedMessageLength: function(messageLength) { getPaddedMessageLength: function(messageLength) {
var messageLengthWithTerminator = messageLength + 1; var messageLengthWithTerminator = messageLength + 1;
@ -35169,50 +35172,54 @@ SessionCipher.prototype = {
return messagePartCount * 160; return messagePartCount * 160;
}, },
decryptWhisperMessage: function(messageBytes) { decryptWhisperMessage: function(messageBytes) {
var address = this.remoteAddress.toString(); return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(address).then(function(record) { var address = this.remoteAddress.toString();
if (!record) { return this.getRecord(address).then(function(record) {
throw new Error("No record for device " + address); if (!record) {
} throw new Error("No record for device " + address);
var messageProto = messageBytes.slice(1, messageBytes.byteLength- 8); }
var message = Internal.protobuf.WhisperMessage.decode(messageProto); var messageProto = messageBytes.slice(1, messageBytes.byteLength- 8);
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer(); var message = Internal.protobuf.WhisperMessage.decode(messageProto);
var session = record.getSessionByRemoteEphemeralKey(remoteEphemeralKey); var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
return this.doDecryptWhisperMessage(util.toArrayBuffer(messageBytes), session).then(function(plaintext) { var session = record.getSessionByRemoteEphemeralKey(remoteEphemeralKey);
record.updateSessionState(session); return this.doDecryptWhisperMessage(util.toArrayBuffer(messageBytes), session).then(function(plaintext) {
return this.storage.storeSession(address, record.serialize()).then(function() { record.updateSessionState(session);
return [plaintext] return this.storage.storeSession(address, record.serialize()).then(function() {
}); return [plaintext]
}.bind(this)); });
}.bind(this));
}.bind(this));
}.bind(this)); }.bind(this));
}, },
decryptPreKeyWhisperMessage: function(encodedMessage, encoding) { decryptPreKeyWhisperMessage: function(encodedMessage, encoding) {
var address = this.remoteAddress.toString(); return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(address).then(function(record) { var address = this.remoteAddress.toString();
var preKeyProto = Internal.protobuf.PreKeyWhisperMessage.decode(encodedMessage, encoding); return this.getRecord(address).then(function(record) {
if (!record) { var preKeyProto = Internal.protobuf.PreKeyWhisperMessage.decode(encodedMessage, encoding);
if (preKeyProto.registrationId === undefined) { if (!record) {
throw new Error("No registrationId"); if (preKeyProto.registrationId === undefined) {
throw new Error("No registrationId");
}
record = new Internal.SessionRecord(
util.toString(preKeyProto.identityKey),
preKeyProto.registrationId
);
} }
record = new Internal.SessionRecord( var builder = new SessionBuilder(this.storage, this.remoteAddress);
util.toString(preKeyProto.identityKey), return builder.processV3(record, preKeyProto).then(function(preKeyId) {
preKeyProto.registrationId var session = record.getSessionOrIdentityKeyByBaseKey(preKeyProto.baseKey);
); return this.doDecryptWhisperMessage(
} preKeyProto.message.toArrayBuffer(), session
var builder = new SessionBuilder(this.storage, this.remoteAddress); ).then(function(plaintext) {
return builder.processV3(record, preKeyProto).then(function(preKeyId) { record.updateSessionState(session);
var session = record.getSessionOrIdentityKeyByBaseKey(preKeyProto.baseKey); return this.storage.storeSession(address, record.serialize()).then(function() {
return this.doDecryptWhisperMessage( if (preKeyId !== undefined) {
preKeyProto.message.toArrayBuffer(), session return this.storage.removePreKey(preKeyId);
).then(function(plaintext) { }
record.updateSessionState(session); }.bind(this)).then(function() {
return this.storage.storeSession(address, record.serialize()).then(function() { return [plaintext]
if (preKeyId !== undefined) { });
return this.storage.removePreKey(preKeyId); }.bind(this));
}
}.bind(this)).then(function() {
return [plaintext]
});
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
@ -35377,13 +35384,46 @@ SessionCipher.prototype = {
throw new Error("Bad MAC"); throw new Error("Bad MAC");
} }
}); });
},
getRemoteRegistrationId: function() {
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
if (record === undefined) {
return undefined;
}
return record.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));
} }
}; };
libsignal.SessionCipher = function(storage, remoteAddress) { libsignal.SessionCipher = function(storage, remoteAddress) {
var cipher = new SessionCipher(storage, remoteAddress); var cipher = new SessionCipher(storage, remoteAddress);
// return Promise that resolves // returns a Promise that resolves to a ciphertext array buffer
this.encrypt = cipher.encrypt.bind(cipher); this.encrypt = cipher.encrypt.bind(cipher);
// returns a Promise that inits a session if necessary and resolves // returns a Promise that inits a session if necessary and resolves
@ -35392,8 +35432,36 @@ libsignal.SessionCipher = function(storage, remoteAddress) {
// returns a Promise that resolves to decrypted plaintext array buffer // returns a Promise that resolves to decrypted plaintext array buffer
this.decryptWhisperMessage = cipher.decryptWhisperMessage.bind(cipher); this.decryptWhisperMessage = cipher.decryptWhisperMessage.bind(cipher);
this.getRemoteRegistrationId = cipher.getRemoteRegistrationId.bind(cipher);
this.hasOpenSession = cipher.hasOpenSession.bind(cipher);
this.closeOpenSessionForDevice = cipher.closeOpenSessionForDevice.bind(cipher);
}; };
/*
* jobQueue manages multiple queues indexed by device to serialize
* session io ops on the database.
*/
;(function() {
'use strict';
Internal.SessionLock = {};
var jobQueue = {};
Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) {
var runPrevious = jobQueue[number] || Promise.resolve();
var runCurrent = jobQueue[number] = runPrevious.then(runJob, runJob);
runCurrent.then(function() {
if (jobQueue[number] === runCurrent) {
delete jobQueue[number];
}
});
return runCurrent;
}
})();
})(); })();
/* /*
* vim: ts=4:sw=4:expandtab * vim: ts=4:sw=4:expandtab

View file

@ -34750,48 +34750,50 @@ function SessionBuilder(storage, remoteAddress) {
SessionBuilder.prototype = { SessionBuilder.prototype = {
processPreKey: function(device) { processPreKey: function(device) {
return this.storage.isTrustedIdentity( return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
this.remoteAddress.getName(), device.identityKey return this.storage.isTrustedIdentity(
).then(function(trusted) { this.remoteAddress.getName(), device.identityKey
if (!trusted) { ).then(function(trusted) {
throw new Error('Identity key changed'); if (!trusted) {
} throw new Error('Identity key changed');
return Internal.crypto.Ed25519Verify(
device.identityKey,
device.signedPreKey.publicKey,
device.signedPreKey.signature
);
}).then(function() {
return Internal.crypto.createKeyPair();
}).then(function(baseKey) {
var devicePreKey = (device.preKey.publicKey);
return this.initSession(true, baseKey, undefined, device.identityKey,
devicePreKey, device.signedPreKey.publicKey
).then(function(session) {
session.pendingPreKey = {
preKeyId : device.preKey.keyId,
signedKeyId : device.signedPreKey.keyId,
baseKey : baseKey.pubKey
};
return session;
});
}.bind(this)).then(function(session) {
var address = this.remoteAddress.toString();
return this.storage.loadSession(address).then(function(serialized) {
var record;
if (serialized !== undefined) {
record = Internal.SessionRecord.deserialize(serialized);
} else {
record = new Internal.SessionRecord(device.identityKey, device.registrationId);
} }
record.archiveCurrentState(); return Internal.crypto.Ed25519Verify(
record.updateSessionState(session, device.registrationId); device.identityKey,
return Promise.all([ device.signedPreKey.publicKey,
this.storage.storeSession(address, record.serialize()), device.signedPreKey.signature
this.storage.putIdentityKey(this.remoteAddress.getName(), record.identityKey) );
]); }).then(function() {
return Internal.crypto.createKeyPair();
}).then(function(baseKey) {
var devicePreKey = (device.preKey.publicKey);
return this.initSession(true, baseKey, undefined, device.identityKey,
devicePreKey, device.signedPreKey.publicKey
).then(function(session) {
session.pendingPreKey = {
preKeyId : device.preKey.keyId,
signedKeyId : device.signedPreKey.keyId,
baseKey : baseKey.pubKey
};
return session;
});
}.bind(this)).then(function(session) {
var address = this.remoteAddress.toString();
return this.storage.loadSession(address).then(function(serialized) {
var record;
if (serialized !== undefined) {
record = Internal.SessionRecord.deserialize(serialized);
} else {
record = new Internal.SessionRecord(device.identityKey, device.registrationId);
}
record.archiveCurrentState();
record.updateSessionState(session, device.registrationId);
return Promise.all([
this.storage.storeSession(address, record.serialize()),
this.storage.putIdentityKey(this.remoteAddress.getName(), record.identityKey)
]);
}.bind(this));
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}, },
@ -34956,93 +34958,94 @@ SessionCipher.prototype = {
}); });
}, },
encrypt: function(plaintext) { encrypt: function(plaintext) {
if (!(plaintext instanceof ArrayBuffer)) { return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
throw new Error("Expected plaintext to be an ArrayBuffer"); if (!(plaintext instanceof ArrayBuffer)) {
} throw new Error("Expected plaintext to be an ArrayBuffer");
}
var address = this.remoteAddress.toString(); var address = this.remoteAddress.toString();
var ourIdentityKey, myRegistrationId, record, session; var ourIdentityKey, myRegistrationId, record, session, chain;
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);
}
var msg = new Internal.protobuf.WhisperMessage(); var msg = new Internal.protobuf.WhisperMessage();
var paddedPlaintext = new Uint8Array( var paddedPlaintext = new Uint8Array(
this.getPaddedMessageLength(plaintext.byteLength + 1) - 1 this.getPaddedMessageLength(plaintext.byteLength + 1) - 1
); );
paddedPlaintext.set(new Uint8Array(plaintext)); paddedPlaintext.set(new Uint8Array(plaintext));
paddedPlaintext[plaintext.byteLength] = 0x80; paddedPlaintext[plaintext.byteLength] = 0x80;
msg.ephemeralKey = util.toArrayBuffer( return Promise.all([
session.currentRatchet.ephemeralKeyPair.pubKey this.storage.getIdentityKeyPair(),
); this.storage.getLocalRegistrationId(),
var chain = session[util.toString(msg.ephemeralKey)]; 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);
}
return this.fillMessageKeys(chain, chain.chainKey.counter + 1).then(function() { msg.ephemeralKey = util.toArrayBuffer(
return Internal.HKDF(util.toArrayBuffer(chain.messageKeys[chain.chainKey.counter]), session.currentRatchet.ephemeralKeyPair.pubKey
new ArrayBuffer(32), "WhisperMessageKeys" );
).then(function(keys) { chain = session[util.toString(msg.ephemeralKey)];
delete chain.messageKeys[chain.chainKey.counter];
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
return Internal.crypto.encrypt( return this.fillMessageKeys(chain, chain.chainKey.counter + 1);
keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16) }.bind(this)).then(function() {
).then(function(ciphertext) { return Internal.HKDF(
msg.ciphertext = ciphertext; util.toArrayBuffer(chain.messageKeys[chain.chainKey.counter]),
var encodedMsg = util.toArrayBuffer(msg.encode()); new ArrayBuffer(32), "WhisperMessageKeys");
}).then(function(keys) {
delete chain.messageKeys[chain.chainKey.counter];
msg.counter = chain.chainKey.counter;
msg.previousCounter = session.currentRatchet.previousCounter;
var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1); return Internal.crypto.encrypt(
macInput.set(new Uint8Array(util.toArrayBuffer(ourIdentityKey.pubKey))); keys[0], paddedPlaintext.buffer, keys[2].slice(0, 16)
macInput.set(new Uint8Array(util.toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33); ).then(function(ciphertext) {
macInput[33*2] = (3 << 4) | 3; msg.ciphertext = ciphertext;
macInput.set(new Uint8Array(encodedMsg), 33*2 + 1); var encodedMsg = msg.toArrayBuffer();
return Internal.crypto.sign( var macInput = new Uint8Array(encodedMsg.byteLength + 33*2 + 1);
keys[1], macInput.buffer macInput.set(new Uint8Array(util.toArrayBuffer(ourIdentityKey.pubKey)));
).then(function(mac) { macInput.set(new Uint8Array(util.toArrayBuffer(session.indexInfo.remoteIdentityKey)), 33);
var result = new Uint8Array(encodedMsg.byteLength + 9); macInput[33*2] = (3 << 4) | 3;
result[0] = (3 << 4) | 3; macInput.set(new Uint8Array(encodedMsg), 33*2 + 1);
result.set(new Uint8Array(encodedMsg), 1);
result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
record.updateSessionState(session); return Internal.crypto.sign(keys[1], macInput.buffer).then(function(mac) {
return this.storage.storeSession(address, record.serialize()).then(function() { var result = new Uint8Array(encodedMsg.byteLength + 9);
return result; result[0] = (3 << 4) | 3;
}); result.set(new Uint8Array(encodedMsg), 1);
}.bind(this)); result.set(new Uint8Array(mac, 0, 8), encodedMsg.byteLength + 1);
}.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); record.updateSessionState(session);
preKeyMsg.preKeyId = session.pendingPreKey.preKeyId; return this.storage.storeSession(address, record.serialize()).then(function() {
preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId; return result;
});
}.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.message = message; preKeyMsg.baseKey = util.toArrayBuffer(session.pendingPreKey.baseKey);
var result = String.fromCharCode((3 << 4) | 3) + util.toString(preKeyMsg.encode()); preKeyMsg.preKeyId = session.pendingPreKey.preKeyId;
return {type: 3, body: result}; preKeyMsg.signedPreKeyId = session.pendingPreKey.signedKeyId;
} else {
return {type: 1, body: util.toString(message)}; preKeyMsg.message = message;
} var result = String.fromCharCode((3 << 4) | 3) + util.toString(preKeyMsg.encode());
}); return {type: 3, body: result};
} else {
return {type: 1, body: util.toString(message)};
}
});
}.bind(this));
}, },
getPaddedMessageLength: function(messageLength) { getPaddedMessageLength: function(messageLength) {
var messageLengthWithTerminator = messageLength + 1; var messageLengthWithTerminator = messageLength + 1;
@ -35055,50 +35058,54 @@ SessionCipher.prototype = {
return messagePartCount * 160; return messagePartCount * 160;
}, },
decryptWhisperMessage: function(messageBytes) { decryptWhisperMessage: function(messageBytes) {
var address = this.remoteAddress.toString(); return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(address).then(function(record) { var address = this.remoteAddress.toString();
if (!record) { return this.getRecord(address).then(function(record) {
throw new Error("No record for device " + address); if (!record) {
} throw new Error("No record for device " + address);
var messageProto = messageBytes.slice(1, messageBytes.byteLength- 8); }
var message = Internal.protobuf.WhisperMessage.decode(messageProto); var messageProto = messageBytes.slice(1, messageBytes.byteLength- 8);
var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer(); var message = Internal.protobuf.WhisperMessage.decode(messageProto);
var session = record.getSessionByRemoteEphemeralKey(remoteEphemeralKey); var remoteEphemeralKey = message.ephemeralKey.toArrayBuffer();
return this.doDecryptWhisperMessage(util.toArrayBuffer(messageBytes), session).then(function(plaintext) { var session = record.getSessionByRemoteEphemeralKey(remoteEphemeralKey);
record.updateSessionState(session); return this.doDecryptWhisperMessage(util.toArrayBuffer(messageBytes), session).then(function(plaintext) {
return this.storage.storeSession(address, record.serialize()).then(function() { record.updateSessionState(session);
return [plaintext] return this.storage.storeSession(address, record.serialize()).then(function() {
}); return [plaintext]
}.bind(this)); });
}.bind(this));
}.bind(this));
}.bind(this)); }.bind(this));
}, },
decryptPreKeyWhisperMessage: function(encodedMessage, encoding) { decryptPreKeyWhisperMessage: function(encodedMessage, encoding) {
var address = this.remoteAddress.toString(); return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(address).then(function(record) { var address = this.remoteAddress.toString();
var preKeyProto = Internal.protobuf.PreKeyWhisperMessage.decode(encodedMessage, encoding); return this.getRecord(address).then(function(record) {
if (!record) { var preKeyProto = Internal.protobuf.PreKeyWhisperMessage.decode(encodedMessage, encoding);
if (preKeyProto.registrationId === undefined) { if (!record) {
throw new Error("No registrationId"); if (preKeyProto.registrationId === undefined) {
throw new Error("No registrationId");
}
record = new Internal.SessionRecord(
util.toString(preKeyProto.identityKey),
preKeyProto.registrationId
);
} }
record = new Internal.SessionRecord( var builder = new SessionBuilder(this.storage, this.remoteAddress);
util.toString(preKeyProto.identityKey), return builder.processV3(record, preKeyProto).then(function(preKeyId) {
preKeyProto.registrationId var session = record.getSessionOrIdentityKeyByBaseKey(preKeyProto.baseKey);
); return this.doDecryptWhisperMessage(
} preKeyProto.message.toArrayBuffer(), session
var builder = new SessionBuilder(this.storage, this.remoteAddress); ).then(function(plaintext) {
return builder.processV3(record, preKeyProto).then(function(preKeyId) { record.updateSessionState(session);
var session = record.getSessionOrIdentityKeyByBaseKey(preKeyProto.baseKey); return this.storage.storeSession(address, record.serialize()).then(function() {
return this.doDecryptWhisperMessage( if (preKeyId !== undefined) {
preKeyProto.message.toArrayBuffer(), session return this.storage.removePreKey(preKeyId);
).then(function(plaintext) { }
record.updateSessionState(session); }.bind(this)).then(function() {
return this.storage.storeSession(address, record.serialize()).then(function() { return [plaintext]
if (preKeyId !== undefined) { });
return this.storage.removePreKey(preKeyId); }.bind(this));
}
}.bind(this)).then(function() {
return [plaintext]
});
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
}.bind(this)); }.bind(this));
@ -35263,13 +35270,46 @@ SessionCipher.prototype = {
throw new Error("Bad MAC"); throw new Error("Bad MAC");
} }
}); });
},
getRemoteRegistrationId: function() {
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.getRecord(this.remoteAddress.toString()).then(function(record) {
if (record === undefined) {
return undefined;
}
return record.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));
} }
}; };
libsignal.SessionCipher = function(storage, remoteAddress) { libsignal.SessionCipher = function(storage, remoteAddress) {
var cipher = new SessionCipher(storage, remoteAddress); var cipher = new SessionCipher(storage, remoteAddress);
// return Promise that resolves // returns a Promise that resolves to a ciphertext array buffer
this.encrypt = cipher.encrypt.bind(cipher); this.encrypt = cipher.encrypt.bind(cipher);
// returns a Promise that inits a session if necessary and resolves // returns a Promise that inits a session if necessary and resolves
@ -35278,6 +35318,34 @@ libsignal.SessionCipher = function(storage, remoteAddress) {
// returns a Promise that resolves to decrypted plaintext array buffer // returns a Promise that resolves to decrypted plaintext array buffer
this.decryptWhisperMessage = cipher.decryptWhisperMessage.bind(cipher); this.decryptWhisperMessage = cipher.decryptWhisperMessage.bind(cipher);
this.getRemoteRegistrationId = cipher.getRemoteRegistrationId.bind(cipher);
this.hasOpenSession = cipher.hasOpenSession.bind(cipher);
this.closeOpenSessionForDevice = cipher.closeOpenSessionForDevice.bind(cipher);
}; };
/*
* jobQueue manages multiple queues indexed by device to serialize
* session io ops on the database.
*/
;(function() {
'use strict';
Internal.SessionLock = {};
var jobQueue = {};
Internal.SessionLock.queueJobForNumber = function queueJobForNumber(number, runJob) {
var runPrevious = jobQueue[number] || Promise.resolve();
var runCurrent = jobQueue[number] = runPrevious.then(runJob, runJob);
runCurrent.then(function() {
if (jobQueue[number] === runCurrent) {
delete jobQueue[number];
}
});
return runCurrent;
}
})();
})(); })();