Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: at_client put method - introduce shouldEncrypt flag #1398

Merged
merged 18 commits into from
Sep 27, 2024
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 packages/at_client/lib/src/client/request_options.dart
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,10 @@ class PutRequestOptions extends RequestOptions {

/// Whether to send this update request directly to the remote atServer
bool useRemoteAtServer = false;

/// Except public keys, shared keys and self keys are encrypted by default.
/// If client prefers not to encrypt a shared key or self key/ use their own encryption scheme, set this flag to false.
bool shouldEncrypt = true;
}

/// Parameters that application code can optionally provide when calling
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,6 @@ class AtClientPreference {

/// Which version of the atProtocol this client will use.
/// Note that this is different from the version of the
/// Note that this is different from the version of the
/// atProtocol that the client supports, which is set in
/// [AtClientConfig]
/// This instance variable is experimental, for now
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,38 +37,58 @@ class PutRequestTransformer
AtClientUtil.fixAtSign(updateVerbBuilder.atKey.sharedBy);
// Setting updateVerbBuilder.value
updateVerbBuilder.value = tuple.two;

//Encrypt the data for non public keys
if (!updateVerbBuilder.atKey.metadata.isPublic) {
var encryptionService = AtKeyEncryptionManager(_atClient)
.get(updateVerbBuilder.atKey, _atClient.getCurrentAtSign()!);
try {
updateVerbBuilder.value = await encryptionService.encrypt(
updateVerbBuilder.atKey, updateVerbBuilder.value,
storeSharedKeyEncryptedWithData:
options.storeSharedKeyEncryptedMetadata);
} on AtException catch (e) {
e.stack(AtChainedException(Intent.shareData,
ExceptionScenario.encryptionFailed, 'Failed to encrypt the data'));
rethrow;
}
final atKey = updateVerbBuilder.atKey;
final metadata = atKey.metadata;
// Check if the data needs to be encrypted for non-public keys
if (!_isPublicKey(metadata) && options.shouldEncrypt) {
await _encryptData(updateVerbBuilder, options);
} else {
if (encryptionPrivateKey.isNull) {
throw AtPrivateKeyNotFoundException('Failed to sign the public data');
}
final atSigningInput = AtSigningInput(updateVerbBuilder.value)
..signingMode = AtSigningMode.data;
final signingResult = _atClient.atChops!.sign(atSigningInput);
updateVerbBuilder.atKey.metadata.dataSignature = signingResult.result;
// Encode the public data if it contains new line characters
if (updateVerbBuilder.value.contains('\n')) {
updateVerbBuilder.value =
AtEncoderImpl().encodeData(updateVerbBuilder.value, encodingType);
updateVerbBuilder.atKey.metadata.encoding =
encodingType.toShortString();
// Sign the data for public keys
if (_isPublicKey(metadata)) {
_signPublicData(updateVerbBuilder, encryptionPrivateKey);
}
// Encode the data if it contains new line characters
_encodeIfValueContainsNewLine(updateVerbBuilder);
}

return updateVerbBuilder;
}

Future<void> _encryptData(
UpdateVerbBuilder updateVerbBuilder, PutRequestOptions options) async {
var encryptionService = AtKeyEncryptionManager(_atClient)
.get(updateVerbBuilder.atKey, _atClient.getCurrentAtSign()!);
try {
updateVerbBuilder.value = await encryptionService.encrypt(
updateVerbBuilder.atKey, updateVerbBuilder.value,
storeSharedKeyEncryptedWithData:
options.storeSharedKeyEncryptedMetadata);
updateVerbBuilder.atKey.metadata.isEncrypted = true;
} on AtException catch (e) {
e.stack(AtChainedException(Intent.shareData,
ExceptionScenario.encryptionFailed, 'Failed to encrypt the data'));
rethrow;
}
}

void _signPublicData(
UpdateVerbBuilder updateVerbBuilder, String? encryptionPrivateKey) {
if (encryptionPrivateKey.isNull) {
throw AtPrivateKeyNotFoundException('Failed to sign the public data');
}
final atSigningInput = AtSigningInput(updateVerbBuilder.value)
..signingMode = AtSigningMode.data;
final signingResult = _atClient.atChops!.sign(atSigningInput);
updateVerbBuilder.atKey.metadata.dataSignature = signingResult.result;
}

void _encodeIfValueContainsNewLine(UpdateVerbBuilder updateVerbBuilder) {
if (updateVerbBuilder.value.contains('\n')) {
updateVerbBuilder.value =
AtEncoderImpl().encodeData(updateVerbBuilder.value, encodingType);
updateVerbBuilder.atKey.metadata.encoding = encodingType.toShortString();
}
}

bool _isPublicKey(Metadata metadata) => metadata.isPublic;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:at_client/src/client/at_client_spec.dart';
import 'package:at_base2e15/at_base2e15.dart';
import 'package:at_client/src/converters/decoder/at_decoder.dart';
import 'package:at_client/src/decryption_service/decryption.dart';
import 'package:at_client/src/decryption_service/decryption_manager.dart';
import 'package:at_client/src/response/default_response_parser.dart';
import 'package:at_client/src/response/json_utils.dart';
Expand All @@ -17,7 +18,7 @@ import 'package:at_commons/at_commons.dart';
class GetResponseTransformer
implements Transformer<Tuple<AtKey, String>, AtValue> {
late final AtClient _atClient;

AtKeyDecryptionManager? decryptionManager;
GetResponseTransformer(this._atClient);

@override
Expand All @@ -35,39 +36,67 @@ class GetResponseTransformer
atValue.metadata = metadata;
tuple.one.metadata = metadata!;
}

// For public and cached public keys, data is not encrypted.
if (_isKeyPublic(decodedResponse['key'])) {
return _handlePublicData(atValue, tuple);
}
decryptionManager ??= AtKeyDecryptionManager(_atClient);
var decryptionService =
decryptionManager!.get(tuple.one, _atClient.getCurrentAtSign()!);
// Decrypt the data, for other keys
if (!(decodedResponse['key'].startsWith('public:')) &&
!(decodedResponse['key'].startsWith('cached:public:'))) {
var decryptionService = AtKeyDecryptionManager(_atClient)
.get(tuple.one, _atClient.getCurrentAtSign()!);
// For new encrypted data after AtClient v3.2.1, isEncrypted will be true(default value for PutRequestOptions.shouldEncrypt) for self and shared keys
// isEncrypted will be false if client sets PutRequestOptions.shouldEncrypt to false
if (_shouldDecrypt(atValue.metadata)) {
atValue.value = await _decrypt(atValue, decryptionService, tuple.one);
} else {
// for old data, try decrypting the value. if decryption fails, set the original value.
try {
atValue.value =
await decryptionService.decrypt(tuple.one, atValue.value) as String;
} on AtException catch (e) {
e.stack(AtChainedException(Intent.fetchData,
ExceptionScenario.decryptionFailed, 'Failed to decrypt the data'));
rethrow;
atValue.value = await _decrypt(atValue, decryptionService, tuple.one);
gkc marked this conversation as resolved.
Show resolved Hide resolved
} on FormatException {
// trying to decrypt plain data will result in FormatException.
if (atValue.metadata!.encoding != null) {
atValue.value = AtDecoderImpl()
.decodeData(atValue.value, atValue.metadata!.encoding!);
}
}
}
// After decrypting the data, if data is binary, decode the data
// For cached keys, isBinary is not on server-side. Hence getting
// isBinary from AtKey.
if (tuple.one.metadata.isBinary) {
atValue.value = Base2e15.decode(atValue.value);
}
return atValue;
}

if (((decodedResponse['key'].startsWith('public:')) ||
(decodedResponse['key'].startsWith('cached:public:'))) &&
(atValue.metadata!.encoding.isNotNull)) {
AtValue _handlePublicData(AtValue atValue, Tuple<AtKey, String> tuple) {
if (atValue.metadata?.encoding != null) {
atValue.value = AtDecoderImpl()
.decodeData(atValue.value, atValue.metadata!.encoding!);
}

// After decrypting the data, if data is binary, decode the data
// For cached keys, isBinary is not on server-side. Hence getting
// isBinary from AtKey.
if (tuple.one.metadata.isBinary) {
atValue.value = Base2e15.decode(atValue.value);
}

return atValue;
}

Future<String> _decrypt(
AtValue atValue, AtKeyDecryption decryptionService, AtKey atKey) async {
try {
return await decryptionService.decrypt(atKey, atValue.value) as String;
} on AtException catch (e) {
e.stack(AtChainedException(Intent.fetchData,
ExceptionScenario.decryptionFailed, 'Failed to decrypt the data'));
rethrow;
}
}

bool _shouldDecrypt(Metadata? metadata) {
return metadata != null && metadata.isEncrypted;
}

/// Return true if key is a public key or a cached public key
/// Else returns false
bool _isKeyPublic(String key) {
Expand Down
12 changes: 12 additions & 0 deletions packages/at_client/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,18 @@ dependencies:
meta: ^1.8.0
version: ^3.0.2

dependency_overrides:
at_commons:
git:
url: https://github.com/atsign-foundation/at_libraries.git
ref: update_isencrypted_changes
path: packages/at_commons
at_lookup:
git:
url: https://github.com/atsign-foundation/at_libraries.git
ref: at_lookup_publish
path: packages/at_lookup

dev_dependencies:
lints: ^4.0.0
test: ^1.21.4
Expand Down
33 changes: 0 additions & 33 deletions packages/at_client/test/get_request_test.dart

This file was deleted.

Loading
Loading