Skip to content

Commit

Permalink
Support encryption key in configuration (#920)
Browse files Browse the repository at this point in the history
* Implement Configuration encryption key
  • Loading branch information
desistefanova committed Oct 10, 2022
1 parent c47fe49 commit c501973
Show file tree
Hide file tree
Showing 6 changed files with 147 additions and 21 deletions.
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,
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
6 changes: 5 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,9 @@ class _RealmCore {
} else if (config is DisconnectedSyncConfiguration) {
_realmLib.realm_config_set_force_sync_history(configPtr, true);
}

if (config.encryptionKey != null) {
_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 @@ -530,7 +530,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 @@ -551,7 +551,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', () {
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 @@ -788,6 +788,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

0 comments on commit c501973

Please sign in to comment.