haveno-dex / haveno

Decentralized P2P exchange platform built on Monero and Tor
https://haveno.exchange
GNU Affero General Public License v3.0
979 stars 108 forks source link

The Haveno password not using a key derivation function makes it relatively vulnerable to brute forcing #1161

Open phytohydra opened 1 month ago

phytohydra commented 1 month ago

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.

maxz commented 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.

phytohydra commented 1 month ago

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.

Argon2 Binding for the JVM

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>
KewbitXMR commented 1 week ago

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