feat: Add a basic OMEMO session manager

This commit is contained in:
PapaTutuWawa 2022-08-04 12:50:19 +02:00
parent 713ea8e1b1
commit 27b1931629
5 changed files with 154 additions and 0 deletions

View File

@ -1,3 +1,4 @@
import 'dart:convert';
import 'package:cryptography/cryptography.dart';
import 'package:omemo_dart/src/keys.dart';
@ -22,3 +23,57 @@ Future<List<int>> omemoDH(OmemoKeyPair kp, OmemoPublicKey pk, int identityKey) a
return shared.extractBytes();
}
class HkdfKeyResult {
const HkdfKeyResult(this.encryptionKey, this.authenticationKey, this.iv);
final List<int> encryptionKey;
final List<int> authenticationKey;
final List<int> iv;
}
/// OMEMO 0.8.3 often derives the three keys for encryption, authentication and the IV from
/// some input using HKDF-SHA-256. As such, this is a helper function that already provides
/// those three keys from [input] and the info string [info].
Future<HkdfKeyResult> deriveEncryptionKeys(List<int> input, String info) async {
final algorithm = Hkdf(
hmac: Hmac(Sha256()),
outputLength: 80,
);
final result = await algorithm.deriveKey(
secretKey: SecretKey(input),
nonce: List<int>.filled(32, 0x0),
info: utf8.encode(info),
);
final bytes = await result.extractBytes();
return HkdfKeyResult(bytes.sublist(0, 32), bytes.sublist(32, 64), bytes.sublist(64, 80));
}
/// A small helper function to make AES-256-CBC easier. Encrypt [plaintext] using [key] as
/// the encryption key and [iv] as the IV. Returns the ciphertext.
Future<List<int>> aes256CbcEncrypt(List<int> plaintext, List<int> key, List<int> iv) async {
final algorithm = AesCbc.with256bits(
macAlgorithm: MacAlgorithm.empty,
);
final result = await algorithm.encrypt(
plaintext,
secretKey: SecretKey(key),
nonce: iv,
);
return result.cipherText;
}
/// OMEMO often uses the output of a HMAC-SHA-256 truncated to its first 16 bytes.
/// Calculate the HMAC-SHA-256 of [input] using the authentication key [key] and
/// truncate the output to 16 bytes.
Future<List<int>> truncatedHmac(List<int> input, List<int> key) async {
final algorithm = Hmac.sha256();
final result = await algorithm.calculateMac(
input,
secretKey: SecretKey(key),
);
return result.bytes.sublist(0, 16);
}

View File

@ -1,3 +1,5 @@
import 'dart:math';
/// Flattens [inputs] and concatenates the elements.
List<int> concat(List<List<int>> inputs) {
final tmp = List<int>.empty(growable: true);
@ -20,3 +22,15 @@ bool listsEqual(List<int> a, List<int> b) {
return true;
}
/// Use Dart's cryptographically secure random number generator at Random.secure()
/// to generate [length] random numbers between 0 and 256 exclusive.
List<int> generateRandomBytes(int length) {
final bytes = List<int>.empty(growable: true);
final r = Random.secure();
for (var i = 0; i < length; i++) {
bytes.add(r.nextInt(256));
}
return bytes;
}

View File

View File

@ -0,0 +1,84 @@
import 'dart:convert';
import 'package:omemo_dart/src/crypto.dart';
import 'package:omemo_dart/src/double_ratchet/double_ratchet.dart';
import 'package:omemo_dart/src/helpers.dart';
import 'package:synchronized/synchronized.dart';
/// The info used for when encrypting the AES key for the actual payload.
const omemoPayloadInfoString = 'OMEMO Payload';
class EncryptionResult {
const EncryptionResult(this.ciphertext, this.encryptedKeys);
/// The actual message that was encrypted
final List<int> ciphertext;
/// Mapping of the device Id to the key for decrypting ciphertext, encrypted
/// for the ratchet with said device Id
final Map<String, List<int>> encryptedKeys;
}
class OmemoSessionManager {
OmemoSessionManager() : _ratchetMap = {}, _deviceMap = {}, _lock = Lock();
/// Lock for _ratchetMap and _bundleMap
final Lock _lock;
/// Mapping of the Device Id to its OMEMO session
final Map<String, OmemoDoubleRatchet> _ratchetMap;
/// Mapping of a bare Jid to its Device Ids
final Map<String, List<String>> _deviceMap;
/// Add a session [ratchet] with the [deviceId] to the internal tracking state.
Future<void> addSession(String jid, String deviceId, OmemoDoubleRatchet ratchet) async {
await _lock.synchronized(() async {
// Add the bundle Id
if (!_deviceMap.containsKey(jid)) {
_deviceMap[jid] = [deviceId];
} else {
_deviceMap[jid]!.add(deviceId);
}
// Add the ratchet session
if (!_ratchetMap.containsKey(deviceId)) {
_ratchetMap[deviceId] = ratchet;
} else {
// TODO(PapaTutuWawa): What do we do now?
throw Exception();
}
});
}
/// Encrypt the key [plaintext] for all known bundles of [jid]. Returns a map that
/// maps the Bundle Id to the ciphertext of [plaintext].
Future<EncryptionResult> encryptToJid(String jid, String plaintext) async {
final encryptedKeys = <String, List<int>>{};
// Generate the key and encrypt the plaintext
final key = generateRandomBytes(32);
final keys = await deriveEncryptionKeys(key, omemoPayloadInfoString);
final ciphertext = await aes256CbcEncrypt(
utf8.encode(plaintext),
keys.encryptionKey,
keys.iv,
);
final hmac = await truncatedHmac(ciphertext, keys.authenticationKey);
final concatKey = concat([keys.encryptionKey, hmac]);
await _lock.synchronized(() async {
// We assume that the user already checked if the session exists
for (final deviceId in _deviceMap[jid]!) {
final ratchet = _ratchetMap[deviceId]!;
encryptedKeys[deviceId] = (await ratchet.ratchetEncrypt(concatKey)).ciphertext;
}
});
return EncryptionResult(
ciphertext,
encryptedKeys,
);
}
}

View File

@ -9,6 +9,7 @@ dependencies:
cryptography: ^2.0.5
pinenacl: ^0.5.1
protobuf: ^2.1.0
synchronized: ^3.0.0+2
dev_dependencies:
lints: ^2.0.0