Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/src/v2/models/destination.dart
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ class Destination {

DestinationType? type;
String? zkId;
String? encryptedDek;

Destination({
required this.value,
Expand All @@ -19,6 +20,7 @@ class Destination {
this.isEncrypted = false,
this.type,
this.zkId,
this.encryptedDek,
});

factory Destination.fromJson(Map<String, dynamic> json) => Destination(
Expand All @@ -27,6 +29,7 @@ class Destination {
isZk: json['zk'] as bool? ?? false,
type: DestinationType.fromJson(json['type'] as String?),
zkId: json['zk_id'] as String?,
encryptedDek: json['encrypted_dek'] as String?,
);

Map<String, dynamic> toJson() => {
Expand All @@ -35,5 +38,6 @@ class Destination {
'zk': isZk,
if (type != null) 'type': type!.jsonValue,
if (zkId != null) 'zk_id': zkId,
if (encryptedDek != null) 'encrypted_dek': encryptedDek,
};
}
4 changes: 4 additions & 0 deletions lib/src/v2/models/payment.dart
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ class Payment {
Platform? parentPlatform;
String? btcPayServerPluginVersion;

/// Runtime-only flag set by [BrantaService] after metadata decryption.
/// Not serialized to/from JSON.
bool isMetadataDecrypted = false;

Payment({
this.description,
required this.destinations,
Expand Down
45 changes: 38 additions & 7 deletions lib/src/v2/services/branta_service.dart
Original file line number Diff line number Diff line change
Expand Up @@ -69,9 +69,9 @@ class BrantaService implements IBrantaService {

final keys = <String, String>{};
for (final payment in payments) {
await _decryptDestinations(payment.destinations, lookupValue, encryptionKey, null, keys);
await _decryptDestinations(payment, lookupValue, encryptionKey, null, keys);
for (final value in additionalHashValues) {
await _decryptHashZkDestinations(payment.destinations, value, keys);
await _decryptHashZkDestinations(payment, value, keys);
}
}

Expand All @@ -82,21 +82,22 @@ class BrantaService implements IBrantaService {
}

Future<void> _decryptHashZkDestinations(
List<Destination> destinations,
Payment payment,
String plainValue,
Map<String, String> keys,
) async {
final hashZkType = plainValue.getHashZkType();
if (hashZkType == null) return;

final key = plainValue.toNormalizedHash();
for (final destination in destinations) {
for (final destination in payment.destinations) {
if (!destination.isZk || destination.type != hashZkType) continue;
try {
destination.value = await _aesEncryption.decrypt(destination.value, key);
destination.isEncrypted = false;
final zkId = destination.zkId;
if (zkId != null) keys.putIfAbsent(zkId, () => key);
await _tryDecryptMetadata(payment, destination, key);
} catch (_) {
// Key didn't match this destination — leave it encrypted.
}
Expand Down Expand Up @@ -136,7 +137,7 @@ class BrantaService implements IBrantaService {

final keys = <String, String>{};
for (final payment in payments) {
await _decryptDestinations(payment.destinations, normalizedDestination, destinationEncryptionKey, hashZkType, keys);
await _decryptDestinations(payment, normalizedDestination, destinationEncryptionKey, hashZkType, keys);
}

return PaymentsResult(
Expand All @@ -146,13 +147,13 @@ class BrantaService implements IBrantaService {
}

Future<void> _decryptDestinations(
List<Destination> destinations,
Payment payment,
String destinationValue,
String? encryptionKey,
DestinationType? hashZkType,
Map<String, String> keys,
) async {
for (final destination in destinations) {
for (final destination in payment.destinations) {
destination.isEncrypted = destination.isZk;
if (!destination.isZk) continue;

Expand All @@ -163,6 +164,7 @@ class BrantaService implements IBrantaService {
destination.isEncrypted = false;
final zkId = destination.zkId;
if (zkId != null) keys.putIfAbsent(zkId, () => encryptionKey);
await _tryDecryptMetadata(payment, destination, encryptionKey);
} catch (_) {
// Key didn't match — leave it encrypted.
}
Expand All @@ -173,13 +175,30 @@ class BrantaService implements IBrantaService {
destination.isEncrypted = false;
final zkId = destination.zkId;
if (zkId != null) keys.putIfAbsent(zkId, () => key);
await _tryDecryptMetadata(payment, destination, key);
} catch (_) {
// Key didn't match — leave it encrypted.
}
}
}
}

Future<void> _tryDecryptMetadata(
Payment payment,
Destination destination,
String keyUsed,
) async {
final encryptedDek = destination.encryptedDek;
if (encryptedDek == null || payment.metadata == null || payment.isMetadataDecrypted) return;
try {
final dek = await _aesEncryption.decrypt(encryptedDek, keyUsed);
payment.metadata = await _aesEncryption.decrypt(payment.metadata!, dek);
payment.isMetadataDecrypted = true;
} catch (_) {
// DEK decryption failed — leave metadata as-is.
}
}

@override
Future<AddPaymentResult> addPaymentAsync(Payment payment, {BrantaClientOptions? options}) async {
final privacy = _resolvePrivacy(options);
Expand All @@ -189,6 +208,12 @@ class BrantaService implements IBrantaService {
);
}

String? dek;
if (payment.metadata != null && payment.destinations.any((d) => d.isZk)) {
dek = _secretGenerator.generate();
payment.metadata = await _aesEncryption.encrypt(payment.metadata!, dek, deterministicNonce: false);
}

final secret = _secretGenerator.generate();
final encryptedToKey = <String, String>{};

Expand All @@ -202,6 +227,9 @@ class BrantaService implements IBrantaService {
deterministicNonce: _secretGenerator.deterministicNonce,
);
encryptedToKey[destination.value] = secret;
if (dek != null) {
destination.encryptedDek = await _aesEncryption.encrypt(dek, secret, deterministicNonce: false);
}
} else {
final hashZkType = destination.value.getHashZkType();
if (hashZkType == null) {
Expand All @@ -213,6 +241,9 @@ class BrantaService implements IBrantaService {
final key = normalizedValue.toNormalizedHash();
destination.value = await _aesEncryption.encrypt(normalizedValue, key, deterministicNonce: true);
encryptedToKey[destination.value] = key;
if (dek != null) {
destination.encryptedDek = await _aesEncryption.encrypt(dek, key, deterministicNonce: false);
}
}
}

Expand Down
Loading
Loading