Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Update to Argon2 #14

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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