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

Support encryption key in configuration #920

Merged
merged 16 commits into from
Oct 10, 2022
Merged
Show file tree
Hide file tree
Changes from 15 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
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,11 +42,11 @@
```
* Added support for realm list of nullable primitive types, ie. `RealmList<int?>`. ([#163](https://github.com/realm/realm-dart/issues/163))
* Allow null arguments on query. ([#871](https://github.com/realm/realm-dart/issues/871))

* Added support for API key authentication. (Issue [#432](https://github.com/realm/realm-dart/issues/432))
* Expose `User.apiKeys` client - this client can be used to create, fetch, and delete API keys.
* Expose `Credentials.apiKey` that enable authentication with API keys.
* Exposed `User.accessToken` and `User.refreshToken` - these tokens can be used to authenticate against the server when calling HTTP API outside of the Dart/Flutter SDK. For example, if you want to use the GraphQL. (PR [#919](https://github.com/realm/realm-dart/pull/919))
* Added support for `encryptionKey` to `Configuration.local`, `Configuration.flexibleSync` and `Configuration.disconnectedSync` so realm files can be encrypted and existing encrypted files from other Realm sources opened (assuming you have the key)([#920](https://github.com/realm/realm-dart/pull/920))

### Fixed
* Previously removeAt did not truncate length. ([#883](https://github.com/realm/realm-dart/issues/883))
Expand Down
59 changes: 42 additions & 17 deletions lib/src/configuration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,9 @@ abstract class Configuration implements Finalizable {
this.schemaObjects, {
String? path,
this.fifoFilesFallbackPath,
this.encryptionKey,
}) {
_validateEncryptionKey(encryptionKey);
this.path = path ?? _path.join(_path.dirname(_defaultPath), _path.basename(defaultRealmName));
}

Expand All @@ -112,12 +114,12 @@ abstract class Configuration implements Finalizable {
/// If omitted the [defaultPath] for the platform will be used.
late final String path;

//TODO: Config: Support encryption keys. https://github.com/realm/realm-dart/issues/88
// /// The key used to encrypt the entire [Realm].
// ///
// /// A full 64byte (512bit) key for AES-256 encryption.
// /// Once set, must be specified each time the file is used.
// final List<int>? encryptionKey;
/// The key used to encrypt the entire [Realm].
///
/// A full 64byte (512bit) key for AES-256 encryption.
/// Once set, must be specified each time the file is used.
/// If null encryption is not enabled.
final List<int>? encryptionKey;

/// Constructs a [LocalConfiguration]
static LocalConfiguration local(
Expand All @@ -126,22 +128,22 @@ abstract class Configuration implements Finalizable {
int schemaVersion = 0,
String? fifoFilesFallbackPath,
String? path,
List<int>? encryptionKey,
bool disableFormatUpgrade = false,
bool isReadOnly = false,
ShouldCompactCallback? shouldCompactCallback,
MigrationCallback? migrationCallback,
}) =>
LocalConfiguration._(
schemaObjects,
initialDataCallback: initialDataCallback,
schemaVersion: schemaVersion,
fifoFilesFallbackPath: fifoFilesFallbackPath,
path: path,
disableFormatUpgrade: disableFormatUpgrade,
isReadOnly: isReadOnly,
shouldCompactCallback: shouldCompactCallback,
migrationCallback: migrationCallback,
);
LocalConfiguration._(schemaObjects,
initialDataCallback: initialDataCallback,
schemaVersion: schemaVersion,
fifoFilesFallbackPath: fifoFilesFallbackPath,
path: path,
encryptionKey: encryptionKey,
desistefanova marked this conversation as resolved.
Show resolved Hide resolved
disableFormatUpgrade: disableFormatUpgrade,
isReadOnly: isReadOnly,
shouldCompactCallback: shouldCompactCallback,
migrationCallback: migrationCallback);

/// Constructs a [InMemoryConfiguration]
static InMemoryConfiguration inMemory(
Expand All @@ -161,6 +163,7 @@ abstract class Configuration implements Finalizable {
List<SchemaObject> schemaObjects, {
String? fifoFilesFallbackPath,
String? path,
List<int>? encryptionKey,
SyncErrorHandler syncErrorHandler = defaultSyncErrorHandler,
SyncClientResetErrorHandler syncClientResetErrorHandler = const ManualSyncClientResetHandler(_defaultSyncClientResetHandler),
}) =>
Expand All @@ -169,6 +172,7 @@ abstract class Configuration implements Finalizable {
schemaObjects,
fifoFilesFallbackPath: fifoFilesFallbackPath,
path: path,
encryptionKey: encryptionKey,
syncErrorHandler: syncErrorHandler,
syncClientResetErrorHandler: syncClientResetErrorHandler,
);
Expand All @@ -178,12 +182,30 @@ abstract class Configuration implements Finalizable {
List<SchemaObject> schemaObjects, {
String? fifoFilesFallbackPath,
String? path,
List<int>? encryptionKey,
}) =>
DisconnectedSyncConfiguration._(
schemaObjects,
fifoFilesFallbackPath: fifoFilesFallbackPath,
path: path,
encryptionKey: encryptionKey,
);

void _validateEncryptionKey(List<int>? key) {
if (key == null) {
return;
}

if (key.length != realmCore.encryptionKeySize) {
throw RealmException("Wrong encryption key size (must be ${realmCore.encryptionKeySize}, but was ${key.length})");
}

int notAByteElement = key.firstWhere((e) => e > 255, orElse: () => -1);
if (notAByteElement >= 0) {
throw RealmException('''Encryption key must be a list of bytes with allowed values form 0 to 255.
Invalid value $notAByteElement found at index ${key.indexOf(notAByteElement)}.''');
}
}
}

/// [LocalConfiguration] is used to open local [Realm] instances,
Expand All @@ -196,6 +218,7 @@ class LocalConfiguration extends Configuration {
this.schemaVersion = 0,
super.fifoFilesFallbackPath,
super.path,
super.encryptionKey,
this.disableFormatUpgrade = false,
this.isReadOnly = false,
this.shouldCompactCallback,
Expand Down Expand Up @@ -285,6 +308,7 @@ class FlexibleSyncConfiguration extends Configuration {
super.schemaObjects, {
super.fifoFilesFallbackPath,
super.path,
super.encryptionKey,
this.syncErrorHandler = defaultSyncErrorHandler,
this.syncClientResetErrorHandler = const ManualSyncClientResetHandler(_defaultSyncClientResetHandler),
}) : super._();
Expand Down Expand Up @@ -313,6 +337,7 @@ class DisconnectedSyncConfiguration extends Configuration {
super.schemaObjects, {
super.fifoFilesFallbackPath,
super.path,
super.encryptionKey,
}) : super._();
}

Expand Down
7 changes: 6 additions & 1 deletion lib/src/native/realm_core.dart
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ class _RealmCore {
// ignore: unused_field
static const int RLM_INVALID_OBJECT_KEY = -1;

final int encryptionKeySize = 64;

static Object noopUserdata = Object();

// Hide the RealmCore class and make it a singleton
Expand Down Expand Up @@ -225,7 +227,10 @@ class _RealmCore {
} else if (config is DisconnectedSyncConfiguration) {
_realmLib.realm_config_set_force_sync_history(configPtr, true);
}

if (config.encryptionKey != null) {
assert(config is! InMemoryConfiguration, "Encryption keys are not allowed for InMemoryConfiguration");
desistefanova marked this conversation as resolved.
Show resolved Hide resolved
_realmLib.realm_config_set_encryption_key(configPtr, config.encryptionKey!.toUint8Ptr(arena), encryptionKeySize);
}
return configHandle;
});
}
Expand Down
37 changes: 35 additions & 2 deletions test/configuration_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -506,7 +506,7 @@ Future<void> main([List<String>? args]) async {
path.basename('my-custom-realm-name.realm'),
);
final config = Configuration.flexibleSync(user, [Event.schema], path: customPath);
var realm = Realm(config);
var realm = getRealm(config);
});

baasTest('Configuration.disconnectedSync', (appConfig) async {
Expand All @@ -527,7 +527,40 @@ Future<void> main([List<String>? args]) async {
realm.close();

final disconnectedSyncConfig = Configuration.disconnectedSync(schema, path: realmPath);
final disconnectedRealm = Realm(disconnectedSyncConfig);
final disconnectedRealm = getRealm(disconnectedSyncConfig);
expect(disconnectedRealm.find<Task>(oid), isNotNull);
});

test('Configuration set short encryption key', () {
desistefanova marked this conversation as resolved.
Show resolved Hide resolved
List<int> key = [1, 2, 3];
expect(
() => Configuration.local([Car.schema], encryptionKey: key),
throws<RealmException>("Wrong encryption key size"),
);
});

test('Configuration set byte exceeding encryption key', () {
List<int> byteExceedingKey = List<int>.generate(encryptionKeySize, (i) => random.nextInt(4294967296));
expect(
() => Configuration.local([Car.schema], encryptionKey: byteExceedingKey),
throws<RealmException>("Encryption key must be a list of bytes with allowed values form 0 to 255"),
);
});

test('Configuration set a correct encryption key', () {
List<int> key = List<int>.generate(encryptionKeySize, (i) => random.nextInt(256));
Configuration.local([Car.schema], encryptionKey: key);
});

baasTest('FlexibleSyncConfiguration set long encryption key', (appConfiguration) async {
final app = App(appConfiguration);
final credentials = Credentials.anonymous();
final user = await app.logIn(credentials);

List<int> key = List<int>.generate(encryptionKeySize + 10, (i) => random.nextInt(256));
expect(
() => Configuration.flexibleSync(user, [Task.schema], encryptionKey: key),
throws<RealmException>("Wrong encryption key size"),
);
});
}
63 changes: 63 additions & 0 deletions test/realm_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -824,6 +824,69 @@ Future<void> main([List<String>? args]) async {
expect(stored.location, now.location);
expect(stored.location.name, 'Europe/Copenhagen');
});

test('Realm - open local not encrypted realm with encryption key', () {
openEncryptedRealm(null, generateValidKey());
});

test('Realm - open local encrypted realm with an empty encryption key', () {
openEncryptedRealm(generateValidKey(), null);
});

test('Realm - open local encrypted realm with an invalid encryption key', () {
openEncryptedRealm(generateValidKey(), generateValidKey());
});

test('Realm - open local encrypted realm with the correct encryption key', () {
List<int> key = generateValidKey();
openEncryptedRealm(key, key);
});

test('Realm - open closed local encrypted realm with the correct encryption key', () {
List<int> key = generateValidKey();
openEncryptedRealm(key, key, afterEncrypt: (realm) => realm.close());
});

test('Realm - open closed local encrypted realm with an invalid encryption key', () {
openEncryptedRealm(generateValidKey(), generateValidKey(), afterEncrypt: (realm) => realm.close());
});

baasTest('Realm - open remote encrypted realm with encryption key', (appConfiguration) async {
final app = App(appConfiguration);
final credentials = Credentials.anonymous();
final user = await app.logIn(credentials);
List<int> key = List<int>.generate(encryptionKeySize, (i) => random.nextInt(256));
final configuration = Configuration.flexibleSync(user, [Task.schema], encryptionKey: key);

final realm = getRealm(configuration);
expect(realm.isClosed, false);
expect(
() => getRealm(Configuration.flexibleSync(user, [Task.schema])),
throws<RealmException>("already opened with a different encryption key"),
);
});
}

List<int> generateValidKey() {
return List<int>.generate(encryptionKeySize, (i) => random.nextInt(256));
}

void openEncryptedRealm(List<int>? encryptionKey, List<int>? decryptionKey, {void Function(Realm)? afterEncrypt}) {
final config1 = Configuration.local([Car.schema], encryptionKey: encryptionKey);
final config2 = Configuration.local([Car.schema], encryptionKey: decryptionKey);
final realm = getRealm(config1);
if (afterEncrypt != null) {
afterEncrypt(realm);
}
if (encryptionKey == decryptionKey) {
final decriptedRealm = getRealm(config2);
expect(decriptedRealm.isClosed, false);
} else {
expect(
() => getRealm(config2),
throws<RealmException>(realm.isClosed ? "Realm file decryption failed" : "already opened with a different encryption key"),
);
}
}

extension on When {
Expand Down
1 change: 1 addition & 0 deletions test/test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@ O8BM8KOSx9wGyoGs4+OusvRkJizhPaIwa3FInLs4r+xZW9Bp6RndsmVECtvXRv5d
87ztpg6o3DZJRmTp2lAnkNLmxXlFkOSNIwiT3qqyRZOh4DuxPOpfg9K+vtFmRdEJ
RwIDAQAB
-----END PUBLIC KEY-----''';
final int encryptionKeySize = 64;

enum AppNames {
flexible,
Expand Down