feat: Add a basic OMEMO session manager
This commit is contained in:
parent
713ea8e1b1
commit
27b1931629
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
0
lib/src/omemo/crypto.dart
Normal file
0
lib/src/omemo/crypto.dart
Normal file
84
lib/src/omemo/sessionmanager.dart
Normal file
84
lib/src/omemo/sessionmanager.dart
Normal 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,
|
||||
);
|
||||
}
|
||||
}
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user