Skip to content

Commit

Permalink
Move tuneArgon2Security function to example
Browse files Browse the repository at this point in the history
  • Loading branch information
dipu-bd committed Aug 24, 2024
1 parent e3a4020 commit 09f571e
Show file tree
Hide file tree
Showing 15 changed files with 327 additions and 218 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
The previous default one accepting a sink is removed.
- New behavior for `HashlibDigest.oct`: The output should
follow the standard now.
- Removes support for `tuneArgon2Security`

# 1.19.2

Expand Down
128 changes: 119 additions & 9 deletions example/argon2_tuning.dart
Original file line number Diff line number Diff line change
@@ -1,25 +1,135 @@
// Copyright (c) 2023, Sudipto Chandra
// All rights reserved. Check LICENSE file for details.

import 'dart:math';
import 'dart:typed_data';

import 'package:hashlib/hashlib.dart';

void main() async {
void main() {
var target = Duration(milliseconds: 100);
print('Target runtime: ${target.inMilliseconds}ms');

var watch = Stopwatch()..start();
var optimized = await tuneArgon2Security(
target,
verbose: false,
);
var result = tuneArgon2Security(target);
print('Tuning time: ${watch.elapsedMilliseconds / 1000} seconds');
print("Result: m=${result.m} t=${result.t} p=${result.p}");

watch.reset();
var h = argon2i(
argon2i(
"password".codeUnits,
"some salt".codeUnits,
security: optimized,
security: result,
);
print('Sample hash: $h');
print('Actual runtime: ${watch.elapsedMilliseconds}ms');
var r = (watch.elapsedMilliseconds);
print('Actual runtime: ${r}ms');
}

// Copyright (c) 2023, Sudipto Chandra
// All rights reserved. Check LICENSE file for details.

/// Find the Argon2 parameters that can be used to encode password in
/// [desiredRuntime] time on the current device.
///
/// Parameters:
/// - [desiredRuntime] : The target runtime. Must be greater than 10ms.
/// - [maxMemoryAsPowerOf2] : The maxmimum memory as a power of 2. A value of 3
/// would mean memory of 2^3 = 8. Minimum value is 3. Default: 22.
/// - [saltLength] : The target salt length. Default: 16.
/// - [passwordLength] : The target password length. Default: 10.
/// - [hashLength] : The target hash length. Default: 32.
/// - [version] : The Argon2 version. Default: [Argon2Version.v13]
/// - [type] : The Argon2 type. Default: [Argon2Type.argon2id]
/// - [testPerSample] : The number of test to run for the runtime calculation.
Argon2Security tuneArgon2Security(
Duration desiredRuntime, {
int hashLength = 32,
int saltLength = 16,
int passwordLength = 4,
int maxMemoryAsPowerOf2 = 22,
int testPerSample = 8,
Argon2Type type = Argon2Type.argon2id,
Argon2Version version = Argon2Version.v13,
}) {
if (maxMemoryAsPowerOf2 < 3) {
throw ArgumentError('Max memory as power of 2 must be at least 3');
}

final target = desiredRuntime.inMicroseconds;
if (target < 10000) {
throw ArgumentError('Duration should be at least 10ms');
}

final watch = Stopwatch()..start();
final e = min(10000, (target * 0.01).round());

int measure(int m, int p, int t) {
var salt = Uint8List(saltLength);
var password = Uint8List(passwordLength);
int best = 0;
for (int i = 0; i < testPerSample; ++i) {
watch.reset();
Argon2(
type: type,
version: version,
hashLength: hashLength,
salt: salt,
memorySizeKB: m,
parallelism: p,
iterations: t,
).convert(password);
int time = watch.elapsedMicroseconds;
if (i == 0 || time < best) {
best = time;
}
}
return best;
}

int pow = 3;
int memory = 8;
int passes = 1;
int lanes = 4;
int time = 0;
int prev = 0;

// tune the memory
for (; pow <= maxMemoryAsPowerOf2; pow++) {
memory = 1 << pow;
lanes = pow < 6 ? 1 << (pow - 3) : 4;
time = measure(memory, lanes, passes);
if (time + e > target) break;
if (1000 * time < target) {
pow += 7;
} else if (100 * time < target) {
pow += 3;
} else if (50 * time < target) {
pow += 2;
} else if (10 * time < target) {
pow++;
}
pow = min(pow, maxMemoryAsPowerOf2 - 1);
prev = time;
}
if ((prev - target).abs() < (time - target).abs()) {
time = prev;
memory >>= 1;
pow--;
}

// tune the passes
if (time - e < target) {
prev = time;
for (passes++;; passes++) {
time = measure(memory, lanes, passes);
if (time + e > target) break;
prev = time;
}
if ((prev - target).abs() < (time - target).abs()) {
time = prev;
passes--;
}
}

return Argon2Security('optimized', m: memory, t: passes, p: lanes);
}
109 changes: 1 addition & 108 deletions lib/src/algorithms/argon2/security.dart
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
// Copyright (c) 2023, Sudipto Chandra
// All rights reserved. Check LICENSE file for details.

import 'dart:math' show min;

import 'argon2.dart';

/// This contains some recommended values of memory, iteration and parallelism
/// values for [Argon2] algorithm.
///
/// It is best to try out different combinations of these values to achieve the
/// desired runtime on a target machine. You can use the [tuneArgon2Security]
/// method for tuning out the best parameters.
/// desired runtime on a target machine.
class Argon2Security {
final String name;

Expand Down Expand Up @@ -64,107 +61,3 @@ class Argon2Security {
/// [rfc]: https://www.ietf.org/rfc/rfc9106.html
static const strong = Argon2Security('strong', m: 1 << 21, p: 4, t: 1);
}

/// Find the Argon2 parameters that can be used to encode password in
/// [desiredRuntime] time on the current device.
///
/// This function may take up to `50 * desiredRuntime` time to compute.
Future<Argon2Security> tuneArgon2Security(
Duration desiredRuntime, {
int strictness = 10,
int saltLength = 16,
int hashLength = 32,
int maxMemoryAsPowerOf2 = 22,
Argon2Type type = Argon2Type.argon2i,
Argon2Version version = Argon2Version.v13,
bool verbose = false,
}) async {
if (strictness < 1) {
throw ArgumentError('Strictness value must be at least 1');
}
if (maxMemoryAsPowerOf2 < 3) {
throw ArgumentError('Max memory as power of 2 must be at least 3');
}

var watch = Stopwatch()..start();
var salt = List.filled(saltLength, 1);
var password = List.filled(saltLength << 2, 2);
int target = desiredRuntime.inMicroseconds;

// maximize memory
int pow = 3, memory, lanes = 1, passes = 1;
for (; pow <= maxMemoryAsPowerOf2; pow++) {
memory = 1 << pow;
lanes = min(16, memory >>> 3);
var samples = List.generate(10, (_) {
var f = Argon2(
salt: salt,
hashLength: hashLength,
type: type,
version: version,
iterations: passes,
parallelism: lanes,
memorySizeKB: memory,
);
watch.reset();
f.convert(password);
return watch.elapsedMicroseconds;
});
int best = samples.fold(samples.first, min);
if (verbose) {
int delta = target - best;
print("[Argon2Security] t=$passes,p=$lanes,m=$memory ~ $delta us");
}
if ((strictness * target / best).round() < strictness) {
if (pow > 12) {
pow -= 2;
} else {
pow--;
}
break;
}
}

// found the maximum memory
memory = 1 << pow;

// now maximize the passes
for (passes++;; passes++) {
var samples = List.generate(10, (_) {
var f = Argon2(
salt: salt,
hashLength: hashLength,
type: type,
version: version,
iterations: passes,
parallelism: lanes,
memorySizeKB: memory,
);
watch.reset();
f.convert(password);
return watch.elapsedMicroseconds;
});
int best = samples.fold(samples.first, min);
if (verbose) {
int delta = target - best;
print("[Argon2Security] t=$passes,p=$lanes,m=$memory ~ $delta us");
}
if ((strictness * target / best).round() < strictness) {
passes--;
break;
}
}
if (passes == 1 && pow > 10) {
pow++;
memory = 1 << pow;
}

if (verbose) {
print('[Argon2Security] ------------');
print('[Argon2Security] t: $passes');
print('[Argon2Security] p: $lanes');
print('[Argon2Security] m: $memory (2^$pow)');
}

return Argon2Security('optimized', m: memory, t: passes, p: lanes);
}
21 changes: 14 additions & 7 deletions lib/src/algorithms/bcrypt/common.dart
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ BcryptVersion _nameToVersion(String name) {
case '2y':
return BcryptVersion.$2y;
default:
throw ArgumentError('Invalid version');
throw FormatException('Invalid version');
}
}

Expand Down Expand Up @@ -96,14 +96,14 @@ class BcryptContext {
}) {
// validate parameters
if (cost < 0) {
throw StateError('The cost must be at least 0');
throw ArgumentError('The cost must be at least 0');
}
if (cost > 31) {
throw StateError('The cost must be at most 31');
throw ArgumentError('The cost must be at most 31');
}
salt ??= randomBytes(16);
if (salt.length != 16) {
throw StateError('The salt must be exactly 16-bytes');
throw ArgumentError('The salt must be exactly 16-bytes');
}
return BcryptContext._(
cost: cost,
Expand All @@ -120,11 +120,18 @@ class BcryptContext {
var version = _nameToVersion(data.id);
var cost = int.tryParse(data.salt ?? '0');
if (cost == null) {
throw ArgumentError('Invalid cost');
throw FormatException('Invalid cost');
}
Uint8List? salt;
if (data.hash != null) {
salt = fromBase64(data.hash!.substring(0, 22), codec: Base64Codec.bcrypt);
var hash = data.hash;
if (hash != null) {
if (hash.length != 22 && hash.length != 53) {
throw FormatException('Invalid hash');
}
salt = fromBase64(
hash.substring(0, 22),
codec: Base64Codec.bcrypt,
);
}
return BcryptContext(
salt: salt,
Expand Down
12 changes: 4 additions & 8 deletions lib/src/algorithms/keccak/keccak_32bit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@ const int _d = _c4 + 2;
class KeccakHash extends BlockHashSink {
final int stateSize;
final int paddingByte;
late final Uint32List state;
final _var = Uint32List(_d + 2);
late final Uint32List state = sbuffer;

@override
final int hashLength;
Expand All @@ -145,16 +145,12 @@ class KeccakHash extends BlockHashSink {
required this.stateSize,
required this.paddingByte,
int? outputSize, // equals to state size if not provided
}) : hashLength = outputSize ?? stateSize,
}) : assert(stateSize < 0 || stateSize >= 100),
hashLength = outputSize ?? stateSize,
super(
200 - (stateSize << 1), // rate as blockLength
bufferLength: 200, // 1600-bit state as buffer
) {
if (stateSize < 0 || stateSize > 100) {
throw ArgumentError('The state size is not valid');
}
state = sbuffer;
}
);

@override
void reset() {
Expand Down
12 changes: 4 additions & 8 deletions lib/src/algorithms/keccak/keccak_64bit.dart
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ const List<int> _rc = <int>[
class KeccakHash extends BlockHashSink {
final int stateSize;
final int paddingByte;
late final Uint64List qstate;
late final Uint64List qstate = Uint64List.view(buffer.buffer);

@override
final int hashLength;
Expand All @@ -83,16 +83,12 @@ class KeccakHash extends BlockHashSink {
required this.stateSize,
required this.paddingByte,
int? outputSize, // equals to state size if not provided
}) : hashLength = outputSize ?? stateSize,
}) : assert(stateSize >= 0 && stateSize < 100),
hashLength = outputSize ?? stateSize,
super(
200 - (stateSize << 1), // rate as blockLength
bufferLength: 200, // 1600-bit state as buffer
) {
if (stateSize < 0 || stateSize >= 100) {
throw ArgumentError('The state size is not valid');
}
qstate = Uint64List.view(buffer.buffer);
}
);

@override
void reset() {
Expand Down
Loading

0 comments on commit 09f571e

Please sign in to comment.