feat: Make any SPK survive exactly one rotation

This commit is contained in:
PapaTutuWawa 2022-08-08 15:48:09 +02:00
parent b8b6bbf800
commit 1bcbf27c83
3 changed files with 58 additions and 4 deletions

View File

@ -24,3 +24,9 @@ class NotEncryptedForDeviceException implements Exception {
class NoDecryptionKeyException implements Exception {
String errMsg() => 'No key available for decrypting the message';
}
/// Triggered by the Session Manager when the identifier of the used Signed Prekey
/// is neither the current SPK's identifier nor the old one's.
class UnknownSignedPrekeyException implements Exception {
String errMsg() => 'Unknown Signed Prekey used.';
}

View File

@ -156,27 +156,40 @@ class OmemoSessionManager {
return OmemoKeyExchange()
..pkId = kexResult.opkId
..spkId = 0
..spkId = bundle.spkId
..ik = await device.ik.pk.getBytes()
..ek = await kexResult.ek.pk.getBytes();
}
/// Build a new session with the user at [jid] with the device [deviceId] using data
/// from the key exchange [kex].
/// from the key exchange [kex]. In case [kex] contains an unknown Signed Prekey
/// identifier an UnknownSignedPrekeyException will be thrown.
Future<void> _addSessionFromKeyExchange(String jid, int deviceId, OmemoKeyExchange kex) async {
// Pick the correct SPK
final device = await getDevice();
OmemoKeyPair? spk;
if (kex.spkId == device.spkId) {
spk = device.spk;
} else if (kex.spkId == device.oldSpkId) {
spk = device.oldSpk;
} else {
throw UnknownSignedPrekeyException();
}
assert(spk != null, 'The used SPK must be found');
final kexResult = await x3dhFromInitialMessage(
X3DHMessage(
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
OmemoPublicKey.fromBytes(kex.ek!, KeyPairType.x25519),
kex.pkId!,
),
device.spk,
spk!,
device.opks.values.elementAt(kex.pkId!),
device.ik,
);
final ratchet = await OmemoDoubleRatchet.acceptNewSession(
device.spk,
spk,
OmemoPublicKey.fromBytes(kex.ik!, KeyPairType.ed25519),
kexResult.sk,
kexResult.ad,

View File

@ -249,4 +249,39 @@ void main() {
expect(newDevice!.oldSpkId, oldDevice.spkId);
expect(listsEqual(newDevice!.oldSpkSignature!, oldDevice.spkSignature), true);
});
test('Test accepting a session with an old SPK', () async {
const aliceJid = 'alice@server.example';
const bobJid = 'bob@other.server.example';
// Alice and Bob generate their sessions
final aliceSession = await OmemoSessionManager.generateNewIdentity(aliceJid, opkAmount: 1);
final bobSession = await OmemoSessionManager.generateNewIdentity(bobJid, opkAmount: 1);
// Alice encrypts a message for Bob
const messagePlaintext = 'Hello Bob!';
final aliceMessage = await aliceSession.encryptToJid(
bobJid,
messagePlaintext,
newSessions: [
await (await bobSession.getDevice()).toBundle(),
],
);
expect(aliceMessage.encryptedKeys.length, 1);
// Alice loses her Internet connection. Bob rotates his SPK.
await bobSession.rotateSignedPrekey();
// Alice regains her Internet connection and sends the message to Bob
// ...
// Bob decrypts it
final bobMessage = await bobSession.decryptMessage(
aliceMessage.ciphertext,
aliceJid,
(await aliceSession.getDevice()).id,
aliceMessage.encryptedKeys,
);
expect(messagePlaintext, bobMessage);
});
}