From e5defd059f1d82f3f92fe304ea31c7ea172b1a03 Mon Sep 17 00:00:00 2001 From: Aaron Feickert <66188213+AaronFeickert@users.noreply.github.com> Date: Sun, 29 Jan 2023 13:48:17 -0600 Subject: [PATCH] Update to `Argon2` --- lib/secure_storage.dart | 62 +++++++++++++++++++++++++++++++++-- lib/stack_wallet_backup.dart | 38 +++++++++++++++++++++ pubspec.yaml | 1 + test/secure_storage_test.dart | 53 +++++++++++++++++------------- 4 files changed, 128 insertions(+), 26 deletions(-) diff --git a/lib/secure_storage.dart b/lib/secure_storage.dart index 8d89edc..a21a4c7 100644 --- a/lib/secure_storage.dart +++ b/lib/secure_storage.dart @@ -63,8 +63,14 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; +import 'package:argon2/argon2.dart'; import 'package:cryptography/cryptography.dart'; +/// Return a list of all known versions +List getVersions() { + return [1, 2, 3]; +} + /// Get the PBKDF iterations for this version /// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2 int getPbkdfIterations(int version) { @@ -78,6 +84,17 @@ int getPbkdfIterations(int version) { } } +/// Get the Argon2id memory parameter for this version +/// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id +int getArgon2Memory(int version) { + switch (version) { + case 3: + return 47104; + default: + throw VersionError(); + } +} + /// Constants that should not be changed without good reason const int saltLength = 16; // in bytes const String dataKeyDomain = 'STACK_WALLET_DATA_KEY'; @@ -135,7 +152,14 @@ class StorageCryptoHandler { final salt = _randomBytes(saltLength); // Use the passphrase and salt to derive the main key with the PBKDF - final mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version); + Uint8List mainKey = Uint8List(Xchacha20.poly1305Aead().secretKeyLength); + if (version == 1 || version == 2) { + mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version); + } else if (version == 3) { + mainKey = await _argon2id(salt, _stringToBytes(passphrase), version); + } else { + throw VersionError(); + } // Generate a random data key final dataKey = _randomBytes(Xchacha20.poly1305Aead().secretKeyLength); @@ -156,7 +180,14 @@ class StorageCryptoHandler { Uint8List encryptedDataKey = keyBlobBytes.sublist(saltLength); // Derive the candidate main key - final Uint8List mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version); + Uint8List mainKey = Uint8List(Xchacha20.poly1305Aead().secretKeyLength); + if (version == 1 || version == 2) { + mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version); + } else if (version == 3) { + mainKey = await _argon2id(salt, _stringToBytes(passphrase), version); + } else { + throw VersionError(); + } // Determine if the main key is valid against the encrypted data key try { @@ -188,7 +219,13 @@ class StorageCryptoHandler { _salt = _randomBytes(saltLength); // Use the passphrase and salt to derive the main key with the PBKDF - _mainKey = await _pbkdf2(_salt, _stringToBytes(passphrase), version); + if (version == 1 || version == 2) { + _mainKey = await _pbkdf2(_salt, _stringToBytes(passphrase), version); + } else if (version == 3) { + _mainKey = await _argon2id(_salt, _stringToBytes(passphrase), version); + } else { + throw VersionError(); + } } /// Get the key blob, which is safe to store @@ -362,6 +399,25 @@ Future _pbkdf2(Uint8List salt, Uint8List passphrase, int version) asy return Uint8List.fromList(mainKeyBytes); } +/// Argon2id +Future _argon2id(Uint8List salt, Uint8List passphrase, int version) async { + final parameters = Argon2Parameters( + Argon2Parameters.ARGON2_id, + salt, + version: Argon2Parameters.ARGON2_VERSION_13, + iterations: 1, + lanes: 1, + memory: getArgon2Memory(version), + ); + final Argon2BytesGenerator argon2 = Argon2BytesGenerator(); + argon2.init(parameters); + + var derivedKeyBytes = Uint8List(Xchacha20.poly1305Aead().secretKeyLength); + argon2.generateBytes(passphrase, derivedKeyBytes); + + return derivedKeyBytes; +} + /// XChaCha20-Poly1305 encryption Future _xChaCha20Poly1305Encrypt(Uint8List key, Uint8List nonce, Uint8List data, Uint8List aad) async { final Xchacha20 aead = Xchacha20.poly1305Aead(); diff --git a/lib/stack_wallet_backup.dart b/lib/stack_wallet_backup.dart index 6b8bc57..d28780f 100644 --- a/lib/stack_wallet_backup.dart +++ b/lib/stack_wallet_backup.dart @@ -29,6 +29,7 @@ import 'dart:convert'; import 'dart:math'; import 'dart:typed_data'; +import 'package:argon2/argon2.dart'; import 'package:collection/collection.dart'; import 'package:cryptography/cryptography.dart'; import 'package:tuple/tuple.dart'; @@ -84,6 +85,24 @@ List getAllVersions() { Blake2b().hashLengthInBytes )); + // Version 3 uses Argon2id, XChaCha20-Poly1305, and Blake2b + version = 3; + aad = protocol + version.toString(); + const int owaspRecommendedArgon2idMemoryVersion3 = 47104; // OWASP recommendation: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id + const int argon2idSaltLength = 16; // Take that, rainbow tables! + versions.add(VersionParameters( + version, + (passphrase) => _argon2id(passphrase, Uint8List.fromList(utf8.encode(aad)), owaspRecommendedArgon2idMemoryVersion3, Xchacha20.poly1305Aead().secretKeyLength), + (adk, salt) => _argon2id(adk, salt, owaspRecommendedArgon2idMemoryVersion3, Xchacha20.poly1305Aead().secretKeyLength), + (key, nonce, plaintext) => _xChaCha20Poly1305Encrypt(key, nonce, plaintext, aad), + (key, blob) => _xChaCha20Poly1305Decrypt(key, blob, aad), + (data) => _blake2b(data, aad), + argon2idSaltLength, + Xchacha20.poly1305Aead().nonceLength, + Poly1305().macLength, + Blake2b().hashLengthInBytes + )); + return versions; } @@ -239,6 +258,25 @@ Future _pbkdf2(Uint8List passphrase, Uint8List salt, MacAlgorithm mac return Uint8List.fromList(derivedKeyBytes); } +/// Argon2id +Future _argon2id(Uint8List passphrase, Uint8List salt, int memory, int derivedKeyLength) async { + final parameters = Argon2Parameters( + Argon2Parameters.ARGON2_id, + salt, + version: Argon2Parameters.ARGON2_VERSION_13, + iterations: 1, + lanes: 1, + memory: memory, + ); + final Argon2BytesGenerator argon2 = Argon2BytesGenerator(); + argon2.init(parameters); + + var derivedKeyBytes = Uint8List(derivedKeyLength); + argon2.generateBytes(passphrase, derivedKeyBytes); + + return derivedKeyBytes; +} + // // AEAD functions // diff --git a/pubspec.yaml b/pubspec.yaml index f0b4ec0..0005c11 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: flutter: ">=3.10.2" dependencies: + argon2: ^1.0.1 collection: ^1.16.0 cryptography: ^2.0.5 flutter: diff --git a/test/secure_storage_test.dart b/test/secure_storage_test.dart index 691f82d..5f75a8f 100644 --- a/test/secure_storage_test.dart +++ b/test/secure_storage_test.dart @@ -29,30 +29,37 @@ const int saltLength = 16; // must match the library's value, which is private void main() { /// Version-independent operations - test ('upgrade, version 1 to 2', () async { - // Create a storage handler with version 1 - const String passphrase = 'test'; - StorageCryptoHandler handler = await StorageCryptoHandler.fromNewPassphrase(passphrase, 1); - - // Encrypt some data - const String name = 'secret_data_that_should_not_be_padded'; - const value = 'the secret data not to pad'; - final String encryptedValue = await handler.encryptValue(name, value); - - // Upgrade to version 2 (in this case, using the same passphrase) and get the new key blob - await handler.resetPassphrase(passphrase, 2); - final String keyBlob = await handler.getKeyBlob(); - - // Now we can recover the handler with the new passphrase - handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob, 2); - - // Confirm that decryption works as expected - final String decryptedValue = await handler.decryptValue(name, encryptedValue); - expect(decryptedValue, value); - }); + for (int oldVersion in getVersions()) { + for (int newVersion in getVersions()) { + if (oldVersion >= newVersion) { + continue; + } + test ('upgrade, version $oldVersion to $newVersion', () async { + // Create a storage handler with the old version + const String passphrase = 'test'; + StorageCryptoHandler handler = await StorageCryptoHandler.fromNewPassphrase(passphrase, oldVersion); + + // Encrypt some data + const String name = 'secret_data_that_should_not_be_padded'; + const value = 'the secret data not to pad'; + final String encryptedValue = await handler.encryptValue(name, value); + + // Upgrade to the new version (in this case, using the same passphrase) and get the new key blob + await handler.resetPassphrase(passphrase, newVersion); + final String keyBlob = await handler.getKeyBlob(); + + // Now we can recover the handler with the new passphrase + handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob, newVersion); + + // Confirm that decryption works as expected + final String decryptedValue = await handler.decryptValue(name, encryptedValue); + expect(decryptedValue, value); + }); + } + } /// Run with each known version - for (int version in [1, 2]) { + for (int version in getVersions()) { /// Version-specific operations test('examples, version $version', () async { // Create a storage handler from a new passphrase @@ -174,7 +181,7 @@ void main() { expect(() => StorageCryptoHandler.fromExisting(passphrase, evilKeyBlob, version), throwsA(const TypeMatcher())); // Evil version - for (int evilVersion in [1, 2]) { + for (int evilVersion in getVersions()) { if (evilVersion == version) { continue; }