diff --git a/generator/lib/src/analysis/analysis.dart b/generator/lib/src/analysis/analysis.dart index e6c41ca4..3c571538 100644 --- a/generator/lib/src/analysis/analysis.dart +++ b/generator/lib/src/analysis/analysis.dart @@ -1,9 +1,13 @@ import 'dart:convert'; import 'dart:io'; import 'dart:isolate'; +import 'dart:typed_data'; -import 'package:cryptography/cryptography.dart'; import 'package:http/http.dart' as http; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/macs/poly1305.dart'; +import 'package:pointycastle/stream/chacha20poly1305.dart'; +import 'package:pointycastle/stream/chacha7539.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import '../version.dart'; @@ -130,7 +134,7 @@ class ObjectBoxAnalysis { if (await file.exists()) { final lines = await file.readAsLines(); if (lines.length >= 2) { - return decryptToken(lines[0], lines[1]); + return decryptAndVerifyToken(lines[0], lines[1]); } } } catch (e) { @@ -140,23 +144,46 @@ class ObjectBoxAnalysis { return null; } - /// Takes a Base64 encoded secret key and secret text (which is a [SecretBox] - /// concatenation) and returns the decrypted text. - Future decryptToken( - String secretKeyBase64, String secretTextBase64) async { - final algorithm = Chacha20.poly1305Aead(); - var secretKeyBytes = base64Decode(secretKeyBase64); - final secretKey = SecretKeyData(secretKeyBytes); + /// Takes a Base64 encoded key and concatenation of nonce, encrypted token and + /// MAC and returns the decrypted token. + String decryptAndVerifyToken( + String keyBase64, String nonceEncryptedTokenAndMacBase64) { + final key = base64Decode(keyBase64); + // Create copies of nonce and encrypted text with MAC to operate on + final nonceEncryptedAndMac = base64Decode(nonceEncryptedTokenAndMacBase64); + final nonce = Uint8List.fromList(Uint8List.view( + nonceEncryptedAndMac.buffer, + nonceEncryptedAndMac.offsetInBytes, + ObfuscatedToken.nonceLengthBytes, + )); + final encryptedAndMac = Uint8List.fromList(Uint8List.view( + nonceEncryptedAndMac.buffer, + nonceEncryptedAndMac.offsetInBytes + ObfuscatedToken.nonceLengthBytes, + nonceEncryptedAndMac.length - ObfuscatedToken.nonceLengthBytes)); + + final algorithm = ChaCha20Poly1305(ChaCha7539Engine(), Poly1305()); + var params = AEADParameters( + KeyParameter(key), ObfuscatedToken.macLengthBits, nonce, Uint8List(0)); + algorithm.init(false /* decrypt */, params); + + final decrypted = + Uint8List(algorithm.getOutputSize(encryptedAndMac.length)); + final outLen = algorithm.processBytes( + encryptedAndMac, 0, encryptedAndMac.length, decrypted, 0); + algorithm.doFinal(decrypted, outLen); + + return utf8.decode(decrypted); + } +} - final secretBox = SecretBox.fromConcatenation( - base64Decode(secretTextBase64), - nonceLength: algorithm.nonceLength, - macLength: algorithm.macAlgorithm.macLength); +class ObfuscatedToken { + static const int nonceLengthBytes = 12; + static const int macLengthBits = 16 * 8 /* 16 bytes */; - final clearText = await algorithm.decrypt(secretBox, secretKey: secretKey); + final String dataBase64; + final String keyBase64; - return utf8.decode(clearText); - } + ObfuscatedToken(this.dataBase64, this.keyBase64); } /// Wrapper for data to be sent for analysis. Use [toJson] to return a diff --git a/generator/pubspec.yaml b/generator/pubspec.yaml index f4c96665..83a6edc3 100644 --- a/generator/pubspec.yaml +++ b/generator/pubspec.yaml @@ -20,7 +20,7 @@ dependencies: pubspec_parse: ^1.0.0 yaml: ^3.0.0 http: '>=0.13.5 <2.0.0' - cryptography: ^2.0.5 + pointycastle: ^3.7.3 # 3.7.4 requires Dart 3, but still supporting 2.18 dev_dependencies: test: ^1.16.5 diff --git a/generator/test/analysis_test.dart b/generator/test/analysis_test.dart index 8e211f65..6b434836 100644 --- a/generator/test/analysis_test.dart +++ b/generator/test/analysis_test.dart @@ -1,9 +1,14 @@ import 'dart:convert'; import 'dart:io'; +import 'dart:math'; +import 'dart:typed_data'; -import 'package:cryptography/cryptography.dart'; import 'package:objectbox_generator/src/analysis/analysis.dart'; import 'package:objectbox_generator/src/analysis/build_properties.dart'; +import 'package:pointycastle/api.dart'; +import 'package:pointycastle/macs/poly1305.dart'; +import 'package:pointycastle/stream/chacha20poly1305.dart'; +import 'package:pointycastle/stream/chacha7539.dart'; import 'package:pub_semver/pub_semver.dart'; import 'package:pubspec_parse/pubspec_parse.dart'; import 'package:test/expect.dart'; @@ -16,14 +21,14 @@ void main() { test("obfuscate token", () async { final token = "REPLACE_WITH_TOKEN"; - var obfuscatedToken = await obfuscateToken(token); - final secretKeyBase64 = obfuscatedToken.secretKeyBase64; - final secretTextBase64 = obfuscatedToken.secretTextBase64; + var obfuscatedToken = _obfuscateToken(token); + final keyBase64 = obfuscatedToken.keyBase64; + final dataBase64 = obfuscatedToken.dataBase64; print("Store this in generator/lib/${ObjectBoxAnalysis.tokenFilePath}:"); - print("$secretKeyBase64\n$secretTextBase64"); + print("$keyBase64\n$dataBase64"); - final decryptedToken = await ObjectBoxAnalysis() - .decryptToken(secretKeyBase64, secretTextBase64); + final decryptedToken = + ObjectBoxAnalysis().decryptAndVerifyToken(keyBase64, dataBase64); expect(decryptedToken, equals(token)); }, skip: true); @@ -35,10 +40,10 @@ void main() { markTestSkipped("DART_ANALYSIS_TOKEN not set"); return; } - var obfuscatedToken = await obfuscateToken(token); + var obfuscatedToken = _obfuscateToken(token); final tokenFile = File("lib/${ObjectBoxAnalysis.tokenFilePath}"); await tokenFile.writeAsString( - "${obfuscatedToken.secretKeyBase64}\n${obfuscatedToken.secretTextBase64}"); + "${obfuscatedToken.keyBase64}\n${obfuscatedToken.dataBase64}"); final testPubspec = Pubspec("test", dependencies: { "flutter": SdkDependency("flutter"), @@ -117,22 +122,36 @@ void main() { }); } -class ObfuscatedToken { - final String secretTextBase64; - final String secretKeyBase64; - - ObfuscatedToken(this.secretTextBase64, this.secretKeyBase64); +/// Encrypt to obfuscate token and use MAC to ensure token did not get damaged. +/// This is explicitly not used for security purposes. +ObfuscatedToken _obfuscateToken(String token) { + // Note: support Dart before 3.2 where encode returns List + final message = Uint8List.fromList(utf8.encode(token)); + final key = _generateRandomBytes(32); + final nonce = _generateRandomBytes(ObfuscatedToken.nonceLengthBytes); + + final algorithm = ChaCha20Poly1305(ChaCha7539Engine(), Poly1305()); + var params = AEADParameters( + KeyParameter(key), ObfuscatedToken.macLengthBits, nonce, Uint8List(0)); + algorithm.init(true /* encrypt */, params); + + final encrypted = Uint8List(algorithm.getOutputSize(message.length)); + final outLen = + algorithm.processBytes(message, 0, message.length, encrypted, 0); + algorithm.doFinal(encrypted, outLen); + + // Store nonce together with encrypted text (which includes the MAC at the end) + final dataBase64 = base64Encode(nonce + encrypted); + final keyBase64 = base64Encode(key); + + return ObfuscatedToken(dataBase64, keyBase64); } -Future obfuscateToken(String token) async { - final algorithm = Chacha20.poly1305Aead(); - var secretKey = await algorithm.newSecretKey(); - - final message = utf8.encode(token); - final secretBox = await algorithm.encrypt(message, secretKey: secretKey); - - final secretTextBase64 = base64Encode(secretBox.concatenation()); - final secretKeyBase64 = base64Encode(await secretKey.extractBytes()); - - return ObfuscatedToken(secretTextBase64, secretKeyBase64); +Uint8List _generateRandomBytes(int length) { + final random = Random.secure(); + final bytes = Uint8List(length); + for (int i = 0; i < length; i++) { + bytes[i] = random.nextInt(256); + } + return bytes; } diff --git a/objectbox/CHANGELOG.md b/objectbox/CHANGELOG.md index 753ba623..0af50f75 100644 --- a/objectbox/CHANGELOG.md +++ b/objectbox/CHANGELOG.md @@ -1,5 +1,7 @@ ## latest +* Generator: replace cryptography library, allows to use newer versions of the transitive `js` dependency. [#638](https://github.com/objectbox/objectbox-dart/issues/638) + ## 4.0.2 (2024-08-14) * Sync: support option to enable [shared global IDs](https://sync.objectbox.io/advanced/object-ids#shared-global-ids).