add: adding encryption package

This commit is contained in:
Patrick Stewart 2024-12-15 20:12:57 -07:00
parent 3f3e9596e3
commit 273e0f5eaa
12 changed files with 484 additions and 1 deletions

View file

@ -1,5 +1,5 @@
/// Interface for encryption.
abstract class Encrypter {
abstract class EncrypterContract {
/// Encrypt the given value.
///
/// @throws EncryptException

7
packages/encryption/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
# https://dart.dev/guides/libraries/private-files
# Created by `dart pub`
.dart_tool/
# Avoid committing pubspec.lock for library packages; see
# https://dart.dev/guides/libraries/private-files#pubspeclock.
pubspec.lock

View file

@ -0,0 +1,3 @@
## 1.0.0
- Initial version.

View file

@ -0,0 +1,10 @@
The MIT License (MIT)
The Laravel Framework is Copyright (c) Taylor Otwell
The Fabric Framework is Copyright (c) Vieo, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -0,0 +1,105 @@
# Platform Encryption
A robust and flexible encryption package for the Protevus platform, implementing Laravel-inspired encryption functionality in Dart.
## Features
- Support for multiple cipher types (AES-128-CBC, AES-256-CBC, AES-128-GCM, AES-256-GCM)
- Encryption and decryption of both strings and objects
- Key management with support for previous keys
- Proper error handling with EncryptException and DecryptException
## Installation
Add this package to your `pubspec.yaml`:
```yaml
dependencies:
platform_encryption: ^1.0.0
```
Then run:
```
dart pub get
```
## Usage
Here's a basic example of how to use the Encrypter class:
```dart
import 'package:platform_encryption/platform_encryption.dart';
void main() {
// Generate a key for AES-256-CBC
final key = Encrypter.generateKey('aes-256-cbc');
// Create an Encrypter instance
final encrypter = Encrypter(key, cipher: 'aes-256-cbc');
// Encrypt a string
const originalString = 'Hello, Protevus Platform!';
final encryptedString = encrypter.encryptString(originalString);
// Decrypt the string
final decryptedString = encrypter.decryptString(encryptedString);
print('Original: $originalString');
print('Encrypted: $encryptedString');
print('Decrypted: $decryptedString');
}
```
### Encrypting and Decrypting Objects
You can also encrypt and decrypt objects:
```dart
final originalObject = {'username': 'john_doe', 'email': 'john@example.com'};
final encryptedObject = encrypter.encrypt(originalObject);
final decryptedObject = encrypter.decrypt(encryptedObject);
```
### Using Previous Keys
To support key rotation, you can provide previous keys when creating an Encrypter instance:
```dart
final oldKey = Encrypter.generateKey('aes-256-cbc');
final newKey = Encrypter.generateKey('aes-256-cbc');
final encrypter = Encrypter(newKey, cipher: 'aes-256-cbc', previousKeys: [oldKey]);
```
This allows the Encrypter to decrypt messages that were encrypted with the old key.
### Error Handling
The package throws `EncryptException` and `DecryptException` for encryption and decryption errors respectively:
```dart
try {
encrypter.encrypt(null);
} on EncryptException catch (e) {
print('Encryption failed: $e');
}
try {
encrypter.decrypt('invalid_payload');
} on DecryptException catch (e) {
print('Decryption failed: $e');
}
```
## API Reference
For detailed API documentation, please refer to the [API Reference](link-to-api-docs).
## Contributing
Contributions are welcome! Please read our [contributing guidelines](link-to-contributing-guidelines) before submitting pull requests.
## License
This project is licensed under the [MIT License](link-to-license).

View file

@ -0,0 +1,30 @@
# This file configures the static analysis results for your project (errors,
# warnings, and lints).
#
# This enables the 'recommended' set of lints from `package:lints`.
# This set helps identify many issues that may lead to problems when running
# or consuming Dart code, and enforces writing Dart using a single, idiomatic
# style and format.
#
# If you want a smaller set of lints you can change this to specify
# 'package:lints/core.yaml'. These are just the most critical lints
# (the recommended set includes the core lints).
# The core lints are also what is used by pub.dev for scoring packages.
include: package:lints/recommended.yaml
# Uncomment the following section to specify additional rules.
# linter:
# rules:
# - camel_case_types
# analyzer:
# exclude:
# - path/to/excluded/files/**
# For more information about the core and recommended set of lints, see
# https://dart.dev/go/core-lints
# For additional information about configuring this file, see
# https://dart.dev/guides/language/analysis-options

View file

@ -0,0 +1,67 @@
import 'package:platform_encryption/platform_encryption.dart';
import 'package:platform_contracts/contracts.dart';
void main() {
// Generate a key for AES-256-CBC
final key = Encrypter.generateKey('aes-256-cbc');
print('Generated key: $key');
// Create an Encrypter instance
final encrypter = Encrypter(key, cipher: 'aes-256-cbc');
// Example 1: Encrypting and decrypting a string
const originalString = 'Hello, Protevus Platform!';
print('\nExample 1: String encryption');
print('Original: $originalString');
final encryptedString = encrypter.encryptString(originalString);
print('Encrypted: $encryptedString');
final decryptedString = encrypter.decryptString(encryptedString);
print('Decrypted: $decryptedString');
// Example 2: Encrypting and decrypting an object
final originalObject = {
'username': 'john_doe',
'email': 'john@example.com',
'age': 30,
};
print('\nExample 2: Object encryption');
print('Original: $originalObject');
final encryptedObject = encrypter.encrypt(originalObject);
print('Encrypted: $encryptedObject');
final decryptedObject = encrypter.decrypt(encryptedObject);
print('Decrypted: $decryptedObject');
// Example 3: Handling encryption exceptions
print('\nExample 3: Handling exceptions');
try {
encrypter.encrypt(null);
} on EncryptException catch (e) {
print('Caught EncryptException: $e');
}
// Example 4: Handling decryption exceptions
try {
encrypter.decrypt('invalid_payload');
} on DecryptException catch (e) {
print('Caught DecryptException: $e');
}
// Example 5: Using previous keys
print('\nExample 5: Using previous keys');
final oldKey = Encrypter.generateKey('aes-256-cbc');
final newKey = Encrypter.generateKey('aes-256-cbc');
final oldEncrypter = Encrypter(oldKey, cipher: 'aes-256-cbc');
final encryptedWithOldKey = oldEncrypter.encryptString('Secret message');
print('Encrypted with old key: $encryptedWithOldKey');
final newEncrypter =
Encrypter(newKey, cipher: 'aes-256-cbc', previousKeys: [oldKey]);
final decryptedWithNewEncrypter =
newEncrypter.decryptString(encryptedWithOldKey);
print('Decrypted with new encrypter: $decryptedWithNewEncrypter');
}

View file

@ -0,0 +1,3 @@
library platform_encryption;
export 'src/encrypter.dart';

View file

@ -0,0 +1,154 @@
import 'dart:convert';
import 'dart:math';
import 'package:crypto/crypto.dart';
import 'package:encrypt/encrypt.dart' as encryption;
import 'package:platform_contracts/contracts.dart';
class Encrypter implements EncrypterContract, StringEncrypter {
final String _key;
final String _cipher;
final List<String> _previousKeys;
static const Map<String, Map<String, dynamic>> supportedCiphers = {
'aes-128-cbc': {'size': 16, 'aead': false},
'aes-256-cbc': {'size': 32, 'aead': false},
'aes-128-gcm': {'size': 16, 'aead': true},
'aes-256-gcm': {'size': 32, 'aead': true},
};
Encrypter(this._key,
{String cipher = 'aes-256-cbc', List<String> previousKeys = const []})
: _cipher = cipher,
_previousKeys = previousKeys {
if (!isSupported(_key, _cipher)) {
final ciphers = supportedCiphers.keys.join(', ');
throw ArgumentError(
'Unsupported cipher or incorrect key length. Supported ciphers are: $ciphers.');
}
}
static bool isSupported(String key, String cipher) {
final lowerCipher = cipher.toLowerCase();
if (!supportedCiphers.containsKey(lowerCipher)) {
return false;
}
return base64.decode(key).length == supportedCiphers[lowerCipher]!['size'];
}
static String generateKey(String cipher) {
final size = supportedCiphers[cipher.toLowerCase()]?['size'] ?? 32;
final random = Random.secure();
final bytes = List<int>.generate(size, (_) => random.nextInt(256));
return base64.encode(bytes);
}
@override
String encrypt(dynamic value, [bool serialize = true]) {
if (value == null) {
throw EncryptException();
}
try {
final iv = encryption.IV.fromSecureRandom(16);
final encrypter = _createEncrypter(_key);
final serializedValue = serialize ? jsonEncode(value) : value.toString();
final encrypted = encrypter.encrypt(serializedValue, iv: iv);
final payload = {
'iv': base64.encode(iv.bytes),
'value': encrypted.base64,
'mac': _createMac(iv.bytes, encrypted.bytes, _key),
'tag': '', // For AEAD ciphers, we'd include the tag here
};
return base64.encode(utf8.encode(jsonEncode(payload)));
} catch (e) {
throw EncryptException();
}
}
@override
String encryptString(String value) {
return encrypt(value, false);
}
@override
dynamic decrypt(String payload, [bool unserialize = true]) {
try {
final decodedPayload = _getJsonPayload(payload);
final iv = encryption.IV(base64.decode(decodedPayload['iv']));
for (final key in [_key, ..._previousKeys]) {
if (_validMac(decodedPayload, key)) {
final encrypter = _createEncrypter(key);
final decrypted =
encrypter.decrypt64(decodedPayload['value'], iv: iv);
return unserialize ? jsonDecode(decrypted) : decrypted;
}
}
throw DecryptException();
} catch (e) {
throw DecryptException();
}
}
@override
String decryptString(String payload) {
return decrypt(payload, false) as String;
}
@override
String getKey() => _key;
@override
List<String> getAllKeys() => [_key, ..._previousKeys];
@override
List<String> getPreviousKeys() => _previousKeys;
encryption.Encrypter _createEncrypter(String key) {
final keyBytes = base64.decode(key);
final encryptionKey = encryption.Key(keyBytes);
return encryption.Encrypter(
encryption.AES(encryptionKey, mode: encryption.AESMode.cbc));
}
String _createMac(List<int> iv, List<int> value, String key) {
final hmac = Hmac(sha256, base64.decode(key));
final digest = hmac.convert(iv + value);
return base64.encode(digest.bytes);
}
Map<String, dynamic> _getJsonPayload(String payload) {
try {
final decoded = jsonDecode(utf8.decode(base64.decode(payload)));
if (!_validPayload(decoded)) {
throw FormatException('The payload is invalid.');
}
return decoded;
} catch (e) {
throw DecryptException();
}
}
bool _validPayload(dynamic payload) {
if (payload is! Map<String, dynamic>) return false;
for (final item in ['iv', 'value', 'mac']) {
if (!payload.containsKey(item) || payload[item] is! String) {
return false;
}
}
return true;
}
bool _validMac(Map<String, dynamic> payload, String key) {
return _createMac(
base64.decode(payload['iv']),
base64.decode(payload['value']),
key,
) ==
payload['mac'];
}
}

View file

@ -0,0 +1,14 @@
/// Exception thrown when the application encryption key is missing.
class MissingAppKeyException implements Exception {
/// The error message.
final String message;
/// Creates a new [MissingAppKeyException] instance.
///
/// If no message is provided, a default message is used.
MissingAppKeyException(
[this.message = 'No application encryption key has been specified.']);
@override
String toString() => 'MissingAppKeyException: $message';
}

View file

@ -0,0 +1,18 @@
name: platform_encryption
description: The Encryption Package for the Protevus Platform
version: 0.0.1
homepage: https://protevus.com
documentation: https://docs.protevus.com
repository: https://github.com/protevus/platformo
environment:
sdk: ^3.4.2
# Add regular dependencies here.
dependencies:
encrypt: ^5.0.3
platform_contracts: ^8.0.0
dev_dependencies:
lints: ^3.0.0
test: ^1.24.0

View file

@ -0,0 +1,72 @@
import 'dart:convert';
import 'package:test/test.dart';
import 'package:platform_encryption/platform_encryption.dart';
import 'package:platform_contracts/contracts.dart';
void main() {
late Encrypter encrypter;
late String testKey;
setUp(() {
// Generate a 256-bit (32-byte) key for AES-256-CBC
final keyBytes = List<int>.generate(32, (i) => i);
testKey = base64.encode(keyBytes);
encrypter = Encrypter(testKey, cipher: 'aes-256-cbc');
});
test('Encrypter implements EncrypterContract and StringEncrypter', () {
expect(encrypter, isA<EncrypterContract>());
expect(encrypter, isA<StringEncrypter>());
});
test('Encrypt and decrypt a string', () {
const originalString = 'Hello, World!';
final encrypted = encrypter.encrypt(originalString);
final decrypted = encrypter.decrypt(encrypted);
expect(decrypted, equals(originalString));
});
test('Encrypt and decrypt an object', () {
final originalObject = {'key': 'value', 'number': 42};
final encrypted = encrypter.encrypt(originalObject);
final decrypted = encrypter.decrypt(encrypted);
expect(decrypted, equals(originalObject));
});
test('EncryptString and decryptString', () {
const originalString = 'Hello, World!';
final encrypted = encrypter.encryptString(originalString);
final decrypted = encrypter.decryptString(encrypted);
expect(decrypted, equals(originalString));
});
test('GetKey returns the correct key', () {
expect(encrypter.getKey(), equals(testKey));
});
test('GetAllKeys returns all keys', () {
final previousKeyBytes = List<int>.generate(32, (i) => i + 100);
final previousKey = base64.encode(previousKeyBytes);
final encrypterWithPreviousKey =
Encrypter(testKey, cipher: 'aes-256-cbc', previousKeys: [previousKey]);
expect(
encrypterWithPreviousKey.getAllKeys(), equals([testKey, previousKey]));
});
test('GetPreviousKeys returns previous keys', () {
final previousKeyBytes = List<int>.generate(32, (i) => i + 100);
final previousKey = base64.encode(previousKeyBytes);
final encrypterWithPreviousKey =
Encrypter(testKey, cipher: 'aes-256-cbc', previousKeys: [previousKey]);
expect(encrypterWithPreviousKey.getPreviousKeys(), equals([previousKey]));
});
test('Throws EncryptException on encryption failure', () {
expect(() => encrypter.encrypt(null), throwsA(isA<EncryptException>()));
});
test('Throws DecryptException on decryption failure', () {
expect(() => encrypter.decrypt('invalid_payload'),
throwsA(isA<DecryptException>()));
});
}