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:cryptography/cryptography.dart';
|
||||||
import 'package:omemo_dart/src/keys.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();
|
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.
|
/// Flattens [inputs] and concatenates the elements.
|
||||||
List<int> concat(List<List<int>> inputs) {
|
List<int> concat(List<List<int>> inputs) {
|
||||||
final tmp = List<int>.empty(growable: true);
|
final tmp = List<int>.empty(growable: true);
|
||||||
@ -20,3 +22,15 @@ bool listsEqual(List<int> a, List<int> b) {
|
|||||||
|
|
||||||
return true;
|
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
|
cryptography: ^2.0.5
|
||||||
pinenacl: ^0.5.1
|
pinenacl: ^0.5.1
|
||||||
protobuf: ^2.1.0
|
protobuf: ^2.1.0
|
||||||
|
synchronized: ^3.0.0+2
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
lints: ^2.0.0
|
lints: ^2.0.0
|
||||||
|
Loading…
Reference in New Issue
Block a user