diff --git a/packages/hashing/README.md b/packages/hashing/README.md index 757f4c9..8b55e73 100644 --- a/packages/hashing/README.md +++ b/packages/hashing/README.md @@ -1 +1,39 @@ -

\ No newline at end of file + + +TODO: Put a short description of the package here that helps potential users +know whether this package might be useful for them. + +## Features + +TODO: List what your package can do. Maybe include images, gifs, or videos. + +## Getting started + +TODO: List prerequisites and provide or point to information on how to +start using the package. + +## Usage + +TODO: Include short and useful examples for package users. Add longer examples +to `/example` folder. + +```dart +const like = 'sample'; +``` + +## Additional information + +TODO: Tell users more about the package: where to find more information, how to +contribute to the package, how to file issues, what response they can expect +from the package authors, and more. diff --git a/packages/hashing/lib/hashing.dart b/packages/hashing/lib/hashing.dart new file mode 100644 index 0000000..f961244 --- /dev/null +++ b/packages/hashing/lib/hashing.dart @@ -0,0 +1,20 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +/// This library provides hashing functionality for the Protevus Platform. +/// +/// It exports two main components: +/// - PBKDF2 (Password-Based Key Derivation Function 2) implementation +/// - Salt generation utilities +/// +/// These components are essential for secure password hashing and storage. +library hashing; + +export 'package:protevus_hashing/src/pbkdf2.dart'; +export 'package:protevus_hashing/src/salt.dart'; diff --git a/packages/hashing/lib/src/.gitkeep b/packages/hashing/lib/src/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/packages/hashing/lib/src/pbkdf2.dart b/packages/hashing/lib/src/pbkdf2.dart new file mode 100644 index 0000000..0aa94f0 --- /dev/null +++ b/packages/hashing/lib/src/pbkdf2.dart @@ -0,0 +1,140 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +import 'package:crypto/crypto.dart'; + +/// Instances of this type derive a key from a password, salt, and hash function. +/// +/// https://en.wikipedia.org/wiki/PBKDF2 +class PBKDF2 { + /// Creates instance capable of generating a key. + /// + /// [hashAlgorithm] defaults to [sha256]. + PBKDF2({Hash? hashAlgorithm}) { + this.hashAlgorithm = hashAlgorithm ?? sha256; + } + + Hash get hashAlgorithm => _hashAlgorithm; + set hashAlgorithm(Hash algorithm) { + _hashAlgorithm = algorithm; + _blockSize = _hashAlgorithm.convert([1, 2, 3]).bytes.length; + } + + late Hash _hashAlgorithm; + late int _blockSize; + + /// Hashes a [password] with a given [salt]. + /// + /// The length of this return value will be [keyLength]. + /// + /// See [generateAsBase64String] for generating a random salt. + /// + /// See also [generateBase64Key], which base64 encodes the key returned from this method for storage. + List generateKey( + String password, + String salt, + int rounds, + int keyLength, + ) { + if (keyLength > (pow(2, 32) - 1) * _blockSize) { + throw PBKDF2Exception("Derived key too long"); + } + + final numberOfBlocks = (keyLength / _blockSize).ceil(); + final hmac = Hmac(hashAlgorithm, utf8.encode(password)); + final key = ByteData(keyLength); + var offset = 0; + + final saltBytes = utf8.encode(salt); + final saltLength = saltBytes.length; + final inputBuffer = ByteData(saltBytes.length + 4) + ..buffer.asUint8List().setRange(0, saltBytes.length, saltBytes); + + for (var blockNumber = 1; blockNumber <= numberOfBlocks; blockNumber++) { + inputBuffer.setUint8(saltLength, blockNumber >> 24); + inputBuffer.setUint8(saltLength + 1, blockNumber >> 16); + inputBuffer.setUint8(saltLength + 2, blockNumber >> 8); + inputBuffer.setUint8(saltLength + 3, blockNumber); + + final block = _XORDigestSink.generate(inputBuffer, hmac, rounds); + var blockLength = _blockSize; + if (offset + blockLength > keyLength) { + blockLength = keyLength - offset; + } + key.buffer.asUint8List().setRange(offset, offset + blockLength, block); + + offset += blockLength; + } + + return key.buffer.asUint8List(); + } + + /// Hashed a [password] with a given [salt] and base64 encodes the result. + /// + /// This method invokes [generateKey] and base64 encodes the result. + String generateBase64Key( + String password, + String salt, + int rounds, + int keyLength, + ) { + const converter = Base64Encoder(); + + return converter.convert(generateKey(password, salt, rounds, keyLength)); + } +} + +/// Thrown when [PBKDF2] throws an exception. +class PBKDF2Exception implements Exception { + PBKDF2Exception(this.message); + String message; + + @override + String toString() => "PBKDF2Exception: $message"; +} + +class _XORDigestSink implements Sink { + _XORDigestSink(ByteData inputBuffer, Hmac hmac) { + lastDigest = hmac.convert(inputBuffer.buffer.asUint8List()).bytes; + bytes = ByteData(lastDigest.length) + ..buffer.asUint8List().setRange(0, lastDigest.length, lastDigest); + } + + static Uint8List generate(ByteData inputBuffer, Hmac hmac, int rounds) { + final hashSink = _XORDigestSink(inputBuffer, hmac); + + // If rounds == 1, we have already run the first hash in the constructor + // so this loop won't run. + for (var round = 1; round < rounds; round++) { + final hmacSink = hmac.startChunkedConversion(hashSink); + hmacSink.add(hashSink.lastDigest); + hmacSink.close(); + } + + return hashSink.bytes.buffer.asUint8List(); + } + + late ByteData bytes; + late List lastDigest; + + @override + void add(Digest digest) { + lastDigest = digest.bytes; + for (var i = 0; i < digest.bytes.length; i++) { + bytes.setUint8(i, bytes.getUint8(i) ^ lastDigest[i]); + } + } + + @override + void close() {} +} diff --git a/packages/hashing/lib/src/salt.dart b/packages/hashing/lib/src/salt.dart new file mode 100644 index 0000000..2e5ca8d --- /dev/null +++ b/packages/hashing/lib/src/salt.dart @@ -0,0 +1,34 @@ +/* + * This file is part of the Protevus Platform. + * + * (C) Protevus + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +import 'dart:convert'; +import 'dart:math'; +import 'dart:typed_data'; + +/// Generates a random salt of [length] bytes from a cryptographically secure random number generator. +/// +/// Each element of this list is a byte. +List generate(int length) { + final buffer = Uint8List(length); + final rng = Random.secure(); + for (var i = 0; i < length; i++) { + buffer[i] = rng.nextInt(256); + } + + return buffer; +} + +/// Generates a random salt of [length] bytes from a cryptographically secure random number generator and encodes it to Base64. +/// +/// [length] is the number of bytes generated, not the [length] of the base64 encoded string returned. Decoding +/// the base64 encoded string will yield [length] number of bytes. +String generateAsBase64String(int length) { + const encoder = Base64Encoder(); + return encoder.convert(generate(length)); +}