Skip to content

Commit

Permalink
Update to Argon2
Browse files Browse the repository at this point in the history
  • Loading branch information
AaronFeickert committed Jun 28, 2023
1 parent d28e823 commit e5defd0
Show file tree
Hide file tree
Showing 4 changed files with 128 additions and 26 deletions.
62 changes: 59 additions & 3 deletions lib/secure_storage.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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<int> 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) {
Expand All @@ -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';
Expand Down Expand Up @@ -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);
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -362,6 +399,25 @@ Future<Uint8List> _pbkdf2(Uint8List salt, Uint8List passphrase, int version) asy
return Uint8List.fromList(mainKeyBytes);
}

/// Argon2id
Future<Uint8List> _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<SecretBox> _xChaCha20Poly1305Encrypt(Uint8List key, Uint8List nonce, Uint8List data, Uint8List aad) async {
final Xchacha20 aead = Xchacha20.poly1305Aead();
Expand Down
38 changes: 38 additions & 0 deletions lib/stack_wallet_backup.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -84,6 +85,24 @@ List<VersionParameters> 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;
}

Expand Down Expand Up @@ -239,6 +258,25 @@ Future<Uint8List> _pbkdf2(Uint8List passphrase, Uint8List salt, MacAlgorithm mac
return Uint8List.fromList(derivedKeyBytes);
}

/// Argon2id
Future<Uint8List> _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
//
Expand Down
1 change: 1 addition & 0 deletions pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ environment:
flutter: ">=3.10.2"

dependencies:
argon2: ^1.0.1
collection: ^1.16.0
cryptography: ^2.0.5
flutter:
Expand Down
53 changes: 30 additions & 23 deletions test/secure_storage_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -174,7 +181,7 @@ void main() {
expect(() => StorageCryptoHandler.fromExisting(passphrase, evilKeyBlob, version), throwsA(const TypeMatcher<IncorrectPassphraseOrVersion>()));

// Evil version
for (int evilVersion in [1, 2]) {
for (int evilVersion in getVersions()) {
if (evilVersion == version) {
continue;
}
Expand Down

0 comments on commit e5defd0

Please sign in to comment.