libsignal-protocol-javascript/src/SessionBuilder.js
lilia 538c7aecce Fix crashes when no preKey is present
Adds a test identical to the 'basic prekey v3' test with the addition of
`delete preKeyBundle.preKey` where appropriate.
2017-02-09 10:53:53 -08:00

228 lines
9.1 KiB
JavaScript

function SessionBuilder(storage, remoteAddress) {
this.remoteAddress = remoteAddress;
this.storage = storage;
}
SessionBuilder.prototype = {
processPreKey: function(device) {
return Internal.SessionLock.queueJobForNumber(this.remoteAddress.toString(), function() {
return this.storage.isTrustedIdentity(
this.remoteAddress.getName(), device.identityKey
).then(function(trusted) {
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;
if (device.preKey) {
devicePreKey = device.preKey.publicKey;
}
return this.initSession(true, baseKey, undefined, device.identityKey,
devicePreKey, device.signedPreKey.publicKey, device.registrationId
).then(function(session) {
session.pendingPreKey = {
signedKeyId : device.signedPreKey.keyId,
baseKey : baseKey.pubKey
};
if (device.preKey) {
session.pendingPreKey.preKeyId = device.preKey.keyId;
}
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();
}
record.archiveCurrentState();
record.updateSessionState(session, device.registrationId);
return Promise.all([
this.storage.storeSession(address, record.serialize()),
this.storage.saveIdentity(this.remoteAddress.getName(), session.indexInfo.remoteIdentityKey)
]);
}.bind(this));
}.bind(this));
}.bind(this));
},
processV3: function(record, message) {
var preKeyPair, signedPreKeyPair, session;
return this.storage.isTrustedIdentity(
this.remoteAddress.getName(), message.identityKey.toArrayBuffer()
).then(function(trusted) {
if (!trusted) {
var e = new Error('Unknown identity key');
e.identityKey = message.identityKey.toArrayBuffer();
throw e;
}
return Promise.all([
this.storage.loadPreKey(message.preKeyId),
this.storage.loadSignedPreKey(message.signedPreKeyId),
]).then(function(results) {
preKeyPair = results[0];
signedPreKeyPair = results[1];
});
}.bind(this)).then(function() {
session = record.getSessionByBaseKey(message.baseKey);
if (session) {
console.log("Duplicate PreKeyMessage for session");
return;
}
session = record.getOpenSession();
if (signedPreKeyPair === undefined) {
// Session may or may not be the right one, but if its not, we
// can't do anything about it ...fall through and let
// decryptWhisperMessage handle that case
if (session !== undefined && session.currentRatchet !== undefined) {
return;
} else {
throw new Error("Missing Signed PreKey for PreKeyWhisperMessage");
}
}
if (session !== undefined) {
record.archiveCurrentState();
}
if (message.preKeyId && !preKeyPair) {
console.log('Invalid prekey id', message.preKeyId);
}
return this.initSession(false, preKeyPair, signedPreKeyPair,
message.identityKey.toArrayBuffer(),
message.baseKey.toArrayBuffer(), undefined, message.registrationId
).then(function(new_session) {
// Note that the session is not actually saved until the very
// end of decryptWhisperMessage ... to ensure that the sender
// actually holds the private keys for all reported pubkeys
record.updateSessionState(new_session, message.registrationId);
return this.storage.saveIdentity(this.remoteAddress.getName(), message.identityKey.toArrayBuffer()).then(function() {
return message.preKeyId;
});
}.bind(this));
}.bind(this));
},
initSession: function(isInitiator, ourEphemeralKey, ourSignedKey,
theirIdentityPubKey, theirEphemeralPubKey,
theirSignedPubKey, registrationId) {
return this.storage.getIdentityKeyPair().then(function(ourIdentityKey) {
if (isInitiator) {
if (ourSignedKey !== undefined) {
throw new Error("Invalid call to initSession");
}
ourSignedKey = ourEphemeralKey;
} else {
if (theirSignedPubKey !== undefined) {
throw new Error("Invalid call to initSession");
}
theirSignedPubKey = theirEphemeralPubKey;
}
var sharedSecret;
if (ourEphemeralKey === undefined || theirEphemeralPubKey === undefined) {
sharedSecret = new Uint8Array(32 * 4);
} else {
sharedSecret = new Uint8Array(32 * 5);
}
for (var i = 0; i < 32; i++) {
sharedSecret[i] = 0xff;
}
return Promise.all([
Internal.crypto.ECDHE(theirSignedPubKey, ourIdentityKey.privKey),
Internal.crypto.ECDHE(theirIdentityPubKey, ourSignedKey.privKey),
Internal.crypto.ECDHE(theirSignedPubKey, ourSignedKey.privKey)
]).then(function(ecRes) {
if (isInitiator) {
sharedSecret.set(new Uint8Array(ecRes[0]), 32);
sharedSecret.set(new Uint8Array(ecRes[1]), 32 * 2);
} else {
sharedSecret.set(new Uint8Array(ecRes[0]), 32 * 2);
sharedSecret.set(new Uint8Array(ecRes[1]), 32);
}
sharedSecret.set(new Uint8Array(ecRes[2]), 32 * 3);
if (ourEphemeralKey !== undefined && theirEphemeralPubKey !== undefined) {
return Internal.crypto.ECDHE(
theirEphemeralPubKey, ourEphemeralKey.privKey
).then(function(ecRes4) {
sharedSecret.set(new Uint8Array(ecRes4), 32 * 4);
});
}
}).then(function() {
return Internal.HKDF(sharedSecret.buffer, new ArrayBuffer(32), "WhisperText");
}).then(function(masterKey) {
var session = {
registrationId: registrationId,
currentRatchet: {
rootKey : masterKey[0],
lastRemoteEphemeralKey : theirSignedPubKey,
previousCounter : 0
},
indexInfo: {
remoteIdentityKey : theirIdentityPubKey,
closed : -1
},
oldRatchetList: []
};
// If we're initiating we go ahead and set our first sending ephemeral key now,
// otherwise we figure it out when we first maybeStepRatchet with the remote's ephemeral key
if (isInitiator) {
session.indexInfo.baseKey = ourEphemeralKey.pubKey;
session.indexInfo.baseKeyType = Internal.BaseKeyType.OURS;
return Internal.crypto.createKeyPair().then(function(ourSendingEphemeralKey) {
session.currentRatchet.ephemeralKeyPair = ourSendingEphemeralKey;
return this.calculateSendingRatchet(session, theirSignedPubKey).then(function() {
return session;
});
}.bind(this));
} else {
session.indexInfo.baseKey = theirEphemeralPubKey;
session.indexInfo.baseKeyType = Internal.BaseKeyType.THEIRS;
session.currentRatchet.ephemeralKeyPair = ourSignedKey;
return session;
}
}.bind(this));
}.bind(this));
},
calculateSendingRatchet: function(session, remoteKey) {
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) {
session[util.toString(ratchet.ephemeralKeyPair.pubKey)] = {
messageKeys : {},
chainKey : { counter : -1, key : masterKey[1] },
chainType : Internal.ChainType.SENDING
};
ratchet.rootKey = masterKey[0];
});
}
};
libsignal.SessionBuilder = function (storage, remoteAddress) {
var builder = new SessionBuilder(storage, remoteAddress);
this.processPreKey = builder.processPreKey.bind(builder);
this.processV3 = builder.processV3.bind(builder);
};