twonly-app-dependencies/libsignal_protocol_dart/lib/src/provisioning_cipher.dart
2025-12-07 16:10:41 +01:00

109 lines
3.7 KiB
Dart

import 'dart:convert';
import 'dart:typed_data';
import 'package:crypto/crypto.dart';
import 'cbc.dart';
import 'ecc/curve.dart';
import 'ecc/ec_public_key.dart';
import 'eq.dart';
import 'invalid_mac_exception.dart';
import 'kdf/derived_root_secrets.dart';
import 'kdf/hkdfv3.dart';
import 'legacy_message_exception.dart';
import 'util/byte_util.dart';
const String provision = 'Mixin Provisioning Message';
class ProvisionEnvelope {
ProvisionEnvelope(this.publicKey, this.body);
ProvisionEnvelope.fromJson(Map<String, dynamic> json)
: publicKey = base64Decode(json['public_key'] as String),
body = base64Decode(json['body'] as String);
final Uint8List publicKey;
final Uint8List body;
Map<String, dynamic> toJson() => <String, dynamic>{
'public_key': base64Encode(publicKey),
'body': base64Encode(body),
};
}
Uint8List decrypt(String privateKey, String content) {
final ourPrivateKey = base64Decode(privateKey);
final envelopeDecode = base64Decode(content);
final map = jsonDecode(String.fromCharCodes(envelopeDecode));
final provisionEnvelope =
ProvisionEnvelope.fromJson(map as Map<String, dynamic>);
final publicKeyable = Curve.decodePoint(provisionEnvelope.publicKey, 0);
final message = provisionEnvelope.body;
if (message[0] != 1) {
throw LegacyMessageException('Invalid version');
}
final iv = Uint8List.fromList(message.getRange(1, 16 + 1).toList());
final mac = message.getRange(message.length - 32, message.length).toList();
final ivAndCiphertext =
Uint8List.fromList(message.getRange(0, message.length - 32).toList());
final cipherText = Uint8List.fromList(
message.getRange(16 + 1, message.length - 32).toList());
final sharedSecret = Curve.calculateAgreement(
publicKeyable, Curve.decodePrivatePoint(ourPrivateKey));
final derivedSecretBytes = HKDFv3().deriveSecrets(sharedSecret,
Uint8List.fromList(utf8.encode(provision)), DerivedRootSecrets.size);
final aesKey =
Uint8List.fromList(derivedSecretBytes.getRange(0, 32).toList());
final macKey = Uint8List.fromList(
derivedSecretBytes.getRange(32, derivedSecretBytes.length).toList());
if (!verifyMAC(macKey, ivAndCiphertext, mac)) {
throw InvalidMacException("MAC doesn't match!");
}
final plaintext = aesCbcDecrypt(aesKey, iv, cipherText);
return plaintext;
}
bool verifyMAC(Uint8List key, Uint8List input, List<int> mac) {
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(input);
return eq(digest.bytes, mac);
}
class ProvisioningCipher {
ProvisioningCipher(this._theirPublicKey);
final ECPublicKey _theirPublicKey;
Uint8List encrypt(Uint8List message) {
final ourKeyPair = Curve.generateKeyPair();
final sharedSecret =
Curve.calculateAgreement(_theirPublicKey, ourKeyPair.privateKey);
final derivedSecret = HKDFv3().deriveSecrets(
sharedSecret, Uint8List.fromList(utf8.encode(provision)), 64);
final parts = ByteUtil.splitTwo(derivedSecret, 32, 32);
final version = Uint8List.fromList([1]);
final ciphertext = getCiphertext(parts[0], message);
final mac = _getMac(parts[1], ByteUtil.combine([version, ciphertext]));
final body = ByteUtil.combine([version, ciphertext, mac]);
final envelope = ProvisionEnvelope(ourKeyPair.publicKey.serialize(), body);
final result = jsonEncode(envelope);
return Uint8List.fromList(utf8.encode(result));
}
Uint8List getCiphertext(Uint8List key, Uint8List message) {
final iv = Uint8List(16);
final m = aesCbcEncrypt(key, iv, message);
return Uint8List.fromList(iv + m);
}
Uint8List _getMac(Uint8List key, Uint8List message) {
final hmacSha256 = Hmac(sha256, key);
final digest = hmacSha256.convert(message);
return Uint8List.fromList(digest.bytes);
}
}