Skip to content

Commit be4461f

Browse files
committed
Update to Argon2
1 parent 39a9426 commit be4461f

4 files changed

Lines changed: 128 additions & 26 deletions

File tree

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';
@@ -130,7 +147,14 @@ class StorageCryptoHandler {
130147
final salt = _randomBytes(saltLength);
131148

132149
// Use the passphrase and salt to derive the main key with the PBKDF
133-
final mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
150+
Uint8List mainKey = Uint8List(Xchacha20.poly1305Aead().secretKeyLength);
151+
if (version == 1 || version == 2) {
152+
mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
153+
} else if (version == 3) {
154+
mainKey = await _argon2id(salt, _stringToBytes(passphrase), version);
155+
} else {
156+
throw VersionError();
157+
}
134158

135159
// Generate a random data key
136160
final dataKey = _randomBytes(Xchacha20.poly1305Aead().secretKeyLength);
@@ -151,7 +175,14 @@ class StorageCryptoHandler {
151175
Uint8List encryptedDataKey = keyBlobBytes.sublist(saltLength);
152176

153177
// Derive the candidate main key
154-
final Uint8List mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
178+
Uint8List mainKey = Uint8List(Xchacha20.poly1305Aead().secretKeyLength);
179+
if (version == 1 || version == 2) {
180+
mainKey = await _pbkdf2(salt, _stringToBytes(passphrase), version);
181+
} else if (version == 3) {
182+
mainKey = await _argon2id(salt, _stringToBytes(passphrase), version);
183+
} else {
184+
throw VersionError();
185+
}
155186

156187
// Determine if the main key is valid against the encrypted data key
157188
try {
@@ -183,7 +214,13 @@ class StorageCryptoHandler {
183214
_salt = _randomBytes(saltLength);
184215

185216
// Use the passphrase and salt to derive the main key with the PBKDF
186-
_mainKey = await _pbkdf2(_salt, _stringToBytes(passphrase), version);
217+
if (version == 1 || version == 2) {
218+
_mainKey = await _pbkdf2(_salt, _stringToBytes(passphrase), version);
219+
} else if (version == 3) {
220+
_mainKey = await _argon2id(_salt, _stringToBytes(passphrase), version);
221+
} else {
222+
throw VersionError();
223+
}
187224
}
188225

189226
/// Get the key blob, which is safe to store
@@ -357,6 +394,25 @@ Future<Uint8List> _pbkdf2(Uint8List salt, Uint8List passphrase, int version) asy
357394
return Uint8List.fromList(mainKeyBytes);
358395
}
359396

397+
/// Argon2id
398+
Future<Uint8List> _argon2id(Uint8List salt, Uint8List passphrase, int version) async {
399+
final parameters = Argon2Parameters(
400+
Argon2Parameters.ARGON2_id,
401+
salt,
402+
version: Argon2Parameters.ARGON2_VERSION_13,
403+
iterations: 1,
404+
lanes: 1,
405+
memory: getArgon2Memory(version),
406+
);
407+
final Argon2BytesGenerator argon2 = Argon2BytesGenerator();
408+
argon2.init(parameters);
409+
410+
var derivedKeyBytes = Uint8List(Xchacha20.poly1305Aead().secretKeyLength);
411+
argon2.generateBytes(passphrase, derivedKeyBytes);
412+
413+
return derivedKeyBytes;
414+
}
415+
360416
/// XChaCha20-Poly1305 encryption
361417
Future<SecretBox> _xChaCha20Poly1305Encrypt(Uint8List key, Uint8List nonce, Uint8List data, Uint8List aad) async {
362418
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)