Skip to content

Commit e5defd0

Browse files
committed
Update to Argon2
1 parent d28e823 commit e5defd0

File tree

4 files changed

+128
-26
lines changed

4 files changed

+128
-26
lines changed

lib/secure_storage.dart

Lines changed: 59 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,8 +63,14 @@
6363
import 'dart:convert';
6464
import 'dart:math';
6565
import 'dart:typed_data';
66+
import 'package:argon2/argon2.dart';
6667
import 'package:cryptography/cryptography.dart';
6768

69+
/// Return a list of all known versions
70+
List<int> getVersions() {
71+
return [1, 2, 3];
72+
}
73+
6874
/// Get the PBKDF iterations for this version
6975
/// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2
7076
int getPbkdfIterations(int version) {
@@ -78,6 +84,17 @@ int getPbkdfIterations(int version) {
7884
}
7985
}
8086

87+
/// Get the Argon2id memory parameter for this version
88+
/// https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
89+
int getArgon2Memory(int version) {
90+
switch (version) {
91+
case 3:
92+
return 47104;
93+
default:
94+
throw VersionError();
95+
}
96+
}
97+
8198
/// Constants that should not be changed without good reason
8299
const int saltLength = 16; // in bytes
83100
const String dataKeyDomain = 'STACK_WALLET_DATA_KEY';
@@ -135,7 +152,14 @@ class StorageCryptoHandler {
135152
final salt = _randomBytes(saltLength);
136153

137154
// Use the passphrase and salt to derive the main key with the PBKDF
138-
final mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
155+
Uint8List mainKey = Uint8List(Xchacha20.poly1305Aead().secretKeyLength);
156+
if (version == 1 || version == 2) {
157+
mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
158+
} else if (version == 3) {
159+
mainKey = await _argon2id(salt, _stringToBytes(passphrase), version);
160+
} else {
161+
throw VersionError();
162+
}
139163

140164
// Generate a random data key
141165
final dataKey = _randomBytes(Xchacha20.poly1305Aead().secretKeyLength);
@@ -156,7 +180,14 @@ class StorageCryptoHandler {
156180
Uint8List encryptedDataKey = keyBlobBytes.sublist(saltLength);
157181

158182
// Derive the candidate main key
159-
final Uint8List mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
183+
Uint8List mainKey = Uint8List(Xchacha20.poly1305Aead().secretKeyLength);
184+
if (version == 1 || version == 2) {
185+
mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
186+
} else if (version == 3) {
187+
mainKey = await _argon2id(salt, _stringToBytes(passphrase), version);
188+
} else {
189+
throw VersionError();
190+
}
160191

161192
// Determine if the main key is valid against the encrypted data key
162193
try {
@@ -188,7 +219,13 @@ class StorageCryptoHandler {
188219
_salt = _randomBytes(saltLength);
189220

190221
// Use the passphrase and salt to derive the main key with the PBKDF
191-
_mainKey = await _pbkdf2(_salt, _stringToBytes(passphrase), version);
222+
if (version == 1 || version == 2) {
223+
_mainKey = await _pbkdf2(_salt, _stringToBytes(passphrase), version);
224+
} else if (version == 3) {
225+
_mainKey = await _argon2id(_salt, _stringToBytes(passphrase), version);
226+
} else {
227+
throw VersionError();
228+
}
192229
}
193230

194231
/// Get the key blob, which is safe to store
@@ -362,6 +399,25 @@ Future<Uint8List> _pbkdf2(Uint8List salt, Uint8List passphrase, int version) asy
362399
return Uint8List.fromList(mainKeyBytes);
363400
}
364401

402+
/// Argon2id
403+
Future<Uint8List> _argon2id(Uint8List salt, Uint8List passphrase, int version) async {
404+
final parameters = Argon2Parameters(
405+
Argon2Parameters.ARGON2_id,
406+
salt,
407+
version: Argon2Parameters.ARGON2_VERSION_13,
408+
iterations: 1,
409+
lanes: 1,
410+
memory: getArgon2Memory(version),
411+
);
412+
final Argon2BytesGenerator argon2 = Argon2BytesGenerator();
413+
argon2.init(parameters);
414+
415+
var derivedKeyBytes = Uint8List(Xchacha20.poly1305Aead().secretKeyLength);
416+
argon2.generateBytes(passphrase, derivedKeyBytes);
417+
418+
return derivedKeyBytes;
419+
}
420+
365421
/// XChaCha20-Poly1305 encryption
366422
Future<SecretBox> _xChaCha20Poly1305Encrypt(Uint8List key, Uint8List nonce, Uint8List data, Uint8List aad) async {
367423
final Xchacha20 aead = Xchacha20.poly1305Aead();

lib/stack_wallet_backup.dart

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import 'dart:convert';
3030
import 'dart:math';
3131
import 'dart:typed_data';
32+
import 'package:argon2/argon2.dart';
3233
import 'package:collection/collection.dart';
3334
import 'package:cryptography/cryptography.dart';
3435
import 'package:tuple/tuple.dart';
@@ -84,6 +85,24 @@ List<VersionParameters> getAllVersions() {
8485
Blake2b().hashLengthInBytes
8586
));
8687

88+
// Version 3 uses Argon2id, XChaCha20-Poly1305, and Blake2b
89+
version = 3;
90+
aad = protocol + version.toString();
91+
const int owaspRecommendedArgon2idMemoryVersion3 = 47104; // OWASP recommendation: https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#argon2id
92+
const int argon2idSaltLength = 16; // Take that, rainbow tables!
93+
versions.add(VersionParameters(
94+
version,
95+
(passphrase) => _argon2id(passphrase, Uint8List.fromList(utf8.encode(aad)), owaspRecommendedArgon2idMemoryVersion3, Xchacha20.poly1305Aead().secretKeyLength),
96+
(adk, salt) => _argon2id(adk, salt, owaspRecommendedArgon2idMemoryVersion3, Xchacha20.poly1305Aead().secretKeyLength),
97+
(key, nonce, plaintext) => _xChaCha20Poly1305Encrypt(key, nonce, plaintext, aad),
98+
(key, blob) => _xChaCha20Poly1305Decrypt(key, blob, aad),
99+
(data) => _blake2b(data, aad),
100+
argon2idSaltLength,
101+
Xchacha20.poly1305Aead().nonceLength,
102+
Poly1305().macLength,
103+
Blake2b().hashLengthInBytes
104+
));
105+
87106
return versions;
88107
}
89108

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

261+
/// Argon2id
262+
Future<Uint8List> _argon2id(Uint8List passphrase, Uint8List salt, int memory, int derivedKeyLength) async {
263+
final parameters = Argon2Parameters(
264+
Argon2Parameters.ARGON2_id,
265+
salt,
266+
version: Argon2Parameters.ARGON2_VERSION_13,
267+
iterations: 1,
268+
lanes: 1,
269+
memory: memory,
270+
);
271+
final Argon2BytesGenerator argon2 = Argon2BytesGenerator();
272+
argon2.init(parameters);
273+
274+
var derivedKeyBytes = Uint8List(derivedKeyLength);
275+
argon2.generateBytes(passphrase, derivedKeyBytes);
276+
277+
return derivedKeyBytes;
278+
}
279+
242280
//
243281
// AEAD functions
244282
//

pubspec.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ environment:
88
flutter: ">=3.10.2"
99

1010
dependencies:
11+
argon2: ^1.0.1
1112
collection: ^1.16.0
1213
cryptography: ^2.0.5
1314
flutter:

test/secure_storage_test.dart

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -29,30 +29,37 @@ const int saltLength = 16; // must match the library's value, which is private
2929

3030
void main() {
3131
/// Version-independent operations
32-
test ('upgrade, version 1 to 2', () async {
33-
// Create a storage handler with version 1
34-
const String passphrase = 'test';
35-
StorageCryptoHandler handler = await StorageCryptoHandler.fromNewPassphrase(passphrase, 1);
36-
37-
// Encrypt some data
38-
const String name = 'secret_data_that_should_not_be_padded';
39-
const value = 'the secret data not to pad';
40-
final String encryptedValue = await handler.encryptValue(name, value);
41-
42-
// Upgrade to version 2 (in this case, using the same passphrase) and get the new key blob
43-
await handler.resetPassphrase(passphrase, 2);
44-
final String keyBlob = await handler.getKeyBlob();
45-
46-
// Now we can recover the handler with the new passphrase
47-
handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob, 2);
48-
49-
// Confirm that decryption works as expected
50-
final String decryptedValue = await handler.decryptValue(name, encryptedValue);
51-
expect(decryptedValue, value);
52-
});
32+
for (int oldVersion in getVersions()) {
33+
for (int newVersion in getVersions()) {
34+
if (oldVersion >= newVersion) {
35+
continue;
36+
}
37+
test ('upgrade, version $oldVersion to $newVersion', () async {
38+
// Create a storage handler with the old version
39+
const String passphrase = 'test';
40+
StorageCryptoHandler handler = await StorageCryptoHandler.fromNewPassphrase(passphrase, oldVersion);
41+
42+
// Encrypt some data
43+
const String name = 'secret_data_that_should_not_be_padded';
44+
const value = 'the secret data not to pad';
45+
final String encryptedValue = await handler.encryptValue(name, value);
46+
47+
// Upgrade to the new version (in this case, using the same passphrase) and get the new key blob
48+
await handler.resetPassphrase(passphrase, newVersion);
49+
final String keyBlob = await handler.getKeyBlob();
50+
51+
// Now we can recover the handler with the new passphrase
52+
handler = await StorageCryptoHandler.fromExisting(passphrase, keyBlob, newVersion);
53+
54+
// Confirm that decryption works as expected
55+
final String decryptedValue = await handler.decryptValue(name, encryptedValue);
56+
expect(decryptedValue, value);
57+
});
58+
}
59+
}
5360

5461
/// Run with each known version
55-
for (int version in [1, 2]) {
62+
for (int version in getVersions()) {
5663
/// Version-specific operations
5764
test('examples, version $version', () async {
5865
// Create a storage handler from a new passphrase
@@ -174,7 +181,7 @@ void main() {
174181
expect(() => StorageCryptoHandler.fromExisting(passphrase, evilKeyBlob, version), throwsA(const TypeMatcher<IncorrectPassphraseOrVersion>()));
175182

176183
// Evil version
177-
for (int evilVersion in [1, 2]) {
184+
for (int evilVersion in getVersions()) {
178185
if (evilVersion == version) {
179186
continue;
180187
}

0 commit comments

Comments
 (0)