Open phytohydra opened 1 month ago
KDF stands for Key Derivation Function
, not Key Definition Function
. It's deriving a key from your input. Not defining a key.
Comparative Analysis of Password Hashing Algorithms: Argon2, bcrypt, scrypt, and PBKDF2 PBKDF2 vs Argon2 - which is better?
Several years ago, Java only had PBKDF2; now the state-of-the-art Argon2 KDF is also available. We can use a pure Java implementation to run everywhere, and a native C library for maximum efficiency on systems that have it installed.
The Spring Framework wrapper class makes BouncyCastle's pure Java implementation pretty trivial to use: https://github.com/phxql/argon2-playground/blob/main/src/main/java/de/mkammerer/argon2playground/Main.java
Note on running that 'playground' project: Its Maven pom.xml file isn't set up to produce an executable .jar file. Importing it into an IDE like NetBeans (using Team > Git > Clone) appears to be how it's intended to be run.
To build it as an executable .jar file: Apache Maven Archiver - Set Up The Classpath
Add this <build>
section to its pom.xml:
<build>
<plugins>
<plugin>
<!-- Build an executable JAR -->
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
<version>3.1.0</version>
<configuration>
<archive>
<manifest>
<addClasspath>true</addClasspath>
<classpathPrefix>${user.home}/.m2/repository/</classpathPrefix>
<classpathLayoutType>repository</classpathLayoutType>
<mainClass>de.mkammerer.argon2playground.Main</mainClass>
</manifest>
</archive>
</configuration>
</plugin>
</plugins>
</build>
I've done something like this in Dart, sorry if it's not much use but it might give you an idea of how I'm encrypted the secure store / profobufs etc and seems to be working great:
import 'dart:convert';
import 'dart:math';
import 'package:flutter/foundation.dart';
import 'package:haveno_plus/services/secure_storage_service.dart';
import 'package:haveno_plus/utils/database_helper.dart';
import 'package:pointycastle/api.dart';
import 'package:pointycastle/digests/sha256.dart';
import 'package:pointycastle/key_derivators/api.dart';
import 'package:pointycastle/key_derivators/pbkdf2.dart';
import 'package:pointycastle/macs/hmac.dart';
class SecurityService {
final SecureStorageService _secureStorage = SecureStorageService();
final DatabaseHelper _databaseHelper = DatabaseHelper.instance;
Future<void> setupUserPassword(String userPassword) async {
final salt = _generateSalt();
final hashedPassword = _hashPassword(userPassword, salt);
final encryptedPassword = _encrypt('$salt:$hashedPassword', userPassword);
await _secureStorage.writeUserPassword(encryptedPassword);
}
Future<bool> authenticateUserPassword(String inputPassword) async {
final encryptedPassword = await _secureStorage.readUserPassword();
if (encryptedPassword == null) {
return false;
}
final decryptedPassword = _decrypt(encryptedPassword, inputPassword);
if (decryptedPassword == null) {
return false;
}
final parts = decryptedPassword.split(':');
if (parts.length != 2) {
return false;
}
final salt = parts[0];
final storedHashedPassword = parts[1];
final inputHashedPassword = _hashPassword(inputPassword, salt);
return storedHashedPassword == inputHashedPassword;
}
String _generateSalt([int length = 16]) {
final random = Random.secure();
final saltBytes = List<int>.generate(length, (_) => random.nextInt(256));
return base64Url.encode(saltBytes);
}
String _hashPassword(String password, String salt) {
final saltBytes = base64Url.decode(salt);
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))
..init(Pbkdf2Parameters(saltBytes, 10000, 32));
final key = pbkdf2.process(utf8.encode(password));
return base64Url.encode(key);
}
// Encrypts a value using AES
String _encrypt(String value, String password) {
final key = _deriveKey(password);
final iv = _generateIV();
final cipher = _initCipher(true, key, iv);
final input = utf8.encode(value);
final encrypted = cipher.process(input);
final encryptedData = base64Url.encode(encrypted);
final encodedIV = base64Url.encode(iv);
return '$encodedIV:$encryptedData';
}
// Decrypts a value using AES
String? _decrypt(String encryptedValue, String password) {
try {
final parts = encryptedValue.split(':');
if (parts.length != 2) {
return null;
}
final iv = base64Url.decode(parts[0]);
final encryptedData = base64Url.decode(parts[1]);
final key = _deriveKey(password);
final cipher = _initCipher(false, key, iv);
final decrypted = cipher.process(encryptedData);
return utf8.decode(decrypted);
} catch (e) {
debugPrint('Decryption failed: $e');
return null;
}
}
// derives an AES key from the password using PBKDF2
KeyParameter _deriveKey(String password, {int iterations = 10000, int keyLength = 32}) {
final salt = utf8.encode('some_silly_salt'); // use fixed salt, no issue
final pbkdf2 = PBKDF2KeyDerivator(HMac(SHA256Digest(), 64))
..init(Pbkdf2Parameters(salt, iterations, keyLength));
final key = pbkdf2.process(utf8.encode(password));
return KeyParameter(key);
}
// denerates an AES cipher for encryption or decryption
PaddedBlockCipher _initCipher(bool forEncryption, KeyParameter key, Uint8List iv) {
final params = PaddedBlockCipherParameters<ParametersWithIV<KeyParameter>, Null>(
ParametersWithIV<KeyParameter>(key, iv), null);
final cipher = PaddedBlockCipher('AES/CBC/PKCS7');
cipher.init(forEncryption, params);
return cipher;
}
// Generates a random IV for AES encryption
Uint8List _generateIV([int length = 16]) {
final random = Random.secure();
final iv = List<int>.generate(length, (_) => random.nextInt(256));
return Uint8List.fromList(iv);
}
Future<void> resetAppData() async {
// Wipe the secure storage
await _secureStorage.storage.deleteAll();
// Wipe the database
await _databaseHelper.destroyDatabase();
}
}
This devirives AES key from PBKDF2
monero-wallet-cli and monero-wallet-rpc both have a --kdf-rounds parameter and default to running the wallet password through a computationally intensive hash function, which greatly increases the time required to brute force it.
The Java keyring utility used by Haveno very quickly returns "Incorrect password" by comparison.
There isn't much point to using the default kdf rounds on the subsidiary wallets, which can take a long time to open if there are a lot of trade wallets, when the keyring password could just be brute-forced instead.
If the user uses the same password for Haveno that they do for other Monero wallets, the Haveno password will be the weak link that makes them all easier to brute force.
Since it's securing actual money and humans are bad at remembering high-entropy passwords, I think Haveno should add kdf hashing as well.