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:
parent
3d3cbb45b7
commit
284cf5be3a
2 changed files with 452 additions and 316 deletions
|
@ -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
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
})();
|
||||||
|
|
||||||
})();
|
})();
|
Loading…
Reference in a new issue