Skip to content

Commit

Permalink
Merge branch '112-generator-replace-crypto' into 'main'
Browse files Browse the repository at this point in the history
Generator: replace crypto with pointycastle, allows newer js versions

See merge request objectbox/objectbox-dart!93
  • Loading branch information
greenrobot-team committed Oct 8, 2024
2 parents f2c08cf + 3ad2361 commit 6006737
Show file tree
Hide file tree
Showing 4 changed files with 90 additions and 42 deletions.
59 changes: 43 additions & 16 deletions generator/lib/src/analysis/analysis.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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) {
Expand All @@ -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<String> 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
Expand Down
2 changes: 1 addition & 1 deletion generator/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
69 changes: 44 additions & 25 deletions generator/test/analysis_test.dart
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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);

Expand All @@ -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"),
Expand Down Expand Up @@ -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<int>
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<ObfuscatedToken> 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;
}
2 changes: 2 additions & 0 deletions objectbox/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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).
Expand Down

0 comments on commit 6006737

Please sign in to comment.