565 lines
17 KiB
Dart
565 lines
17 KiB
Dart
// Copyright (c) 2023, Sudipto Chandra
|
|
// All rights reserved. Check LICENSE file for details.
|
|
|
|
import 'dart:typed_data';
|
|
|
|
import 'package:hashlib/codecs.dart';
|
|
import 'package:hashlib/hashlib.dart';
|
|
import 'package:test/test.dart';
|
|
|
|
void main() {
|
|
group('functionality test', () {
|
|
test("name", () {
|
|
expect(
|
|
Argon2(
|
|
parallelism: 1,
|
|
memorySizeKB: 10,
|
|
iterations: 1,
|
|
).name,
|
|
r'argon2id',
|
|
);
|
|
expect(
|
|
Argon2(
|
|
parallelism: 1,
|
|
memorySizeKB: 10,
|
|
iterations: 1,
|
|
type: Argon2Type.argon2id,
|
|
).name,
|
|
r'argon2id',
|
|
);
|
|
expect(
|
|
Argon2(
|
|
parallelism: 1,
|
|
memorySizeKB: 10,
|
|
iterations: 1,
|
|
type: Argon2Type.argon2d,
|
|
).name,
|
|
r'argon2d',
|
|
);
|
|
expect(
|
|
Argon2(
|
|
parallelism: 1,
|
|
memorySizeKB: 10,
|
|
iterations: 1,
|
|
type: Argon2Type.argon2i,
|
|
).name,
|
|
r'argon2i',
|
|
);
|
|
});
|
|
});
|
|
// Test cases are generated by https://argon2.online/
|
|
group('Argon2 v19 test', () {
|
|
test("argon2i m=16, t=2, p=1 @ out = 16", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2i,
|
|
hashLength: 16,
|
|
iterations: 2,
|
|
parallelism: 1,
|
|
memorySizeKB: 16,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final password = 'password'.codeUnits;
|
|
final matcher = "bb5794ea66451b8fce3a84dd02d33949";
|
|
final encoded =
|
|
r"$argon2i$v=19$m=16,t=2,p=1$c29tZSBzYWx0$u1eU6mZFG4/OOoTdAtM5SQ";
|
|
var result = argon2.convert(password);
|
|
expect(result.toString(), equals(encoded));
|
|
expect(result.hex(), matcher);
|
|
expect(result.encoded(), encoded);
|
|
expect(argon2.encode(password), encoded);
|
|
});
|
|
test("argon2d m=16, t=2, p=1 @ out = 16", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2d,
|
|
hashLength: 16,
|
|
iterations: 2,
|
|
parallelism: 1,
|
|
memorySizeKB: 16,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final matcher = "cf916880b91ba8a1390fff6b624baa27";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
test("argon2id m=16, t=2, p=1 @ out = 16", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2id,
|
|
hashLength: 16,
|
|
iterations: 2,
|
|
parallelism: 1,
|
|
memorySizeKB: 16,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final matcher = "88c91661b3cea3c3853593608881f324";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
|
|
test("argon2i m=256, t=2, p=4 @ out = 32", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2i,
|
|
hashLength: 32,
|
|
iterations: 2,
|
|
parallelism: 4,
|
|
memorySizeKB: 256,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final matcher =
|
|
"aaef1c23ce86889c7d76f5ea214760fb66900916546cde42ebdc47914daed123";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
test("argon2d m=256, t=2, p=4 @ out = 32", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2d,
|
|
hashLength: 32,
|
|
iterations: 2,
|
|
parallelism: 4,
|
|
memorySizeKB: 256,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final matcher =
|
|
"c31433dbefadf7aa527bbbcc7beace0d8d70973719c6efb1c24fb21278569701";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
test("argon2id m=256, t=2, p=4 @ out = 32", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2id,
|
|
hashLength: 32,
|
|
iterations: 2,
|
|
parallelism: 4,
|
|
memorySizeKB: 256,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final matcher =
|
|
"c23e4a305f649971527eda884bda6b481004aedd31740460da3d43db8946786f";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
|
|
test("encoded hash instance check", () {
|
|
final encoded =
|
|
r"$argon2id$v=19$m=128,t=1,p=4$c29tZSBzYWx0$24VHMpaU5EkkdH5rpdnb5zeOf3Y";
|
|
final matcher = "db8547329694e44924747e6ba5d9dbe7378e7f76";
|
|
final argon2 = Argon2.fromEncoded(fromCrypt(encoded));
|
|
expect(argon2.type, Argon2Type.argon2id);
|
|
expect(argon2.version, Argon2Version.v13);
|
|
expect(argon2.memorySizeKB, 128);
|
|
expect(argon2.parallelism, 4);
|
|
expect(argon2.iterations, 1);
|
|
expect(argon2.hashLength, 20);
|
|
expect(argon2.salt, "some salt".codeUnits);
|
|
var result = argon2.convert("password".codeUnits);
|
|
expect(result.hex(), matcher);
|
|
expect(result.encoded(), encoded);
|
|
});
|
|
|
|
test("argon2Verify with encoded", () {
|
|
final encoded =
|
|
r"$argon2id$v=19$m=128,t=1,p=4$c29tZSBzYWx0$24VHMpaU5EkkdH5rpdnb5zeOf3Y";
|
|
expect(argon2Verify(encoded, "password".codeUnits), isTrue);
|
|
});
|
|
|
|
test("argon2Verify without the hash", () {
|
|
final password = "password".codeUnits;
|
|
final encoded = r"$argon2id$v=19$m=128,t=1,p=4$c29tZSBzYWx0";
|
|
expect(() => argon2Verify(encoded, password), throwsArgumentError);
|
|
});
|
|
|
|
test("argon2Verify with password", () {
|
|
final matcher = "db8547329694e44924747e6ba5d9dbe7378e7f76";
|
|
var result = Argon2(
|
|
type: Argon2Type.argon2id,
|
|
salt: 'some salt'.codeUnits,
|
|
hashLength: 20,
|
|
iterations: 1,
|
|
parallelism: 4,
|
|
memorySizeKB: 128,
|
|
).verify(
|
|
fromHex(matcher),
|
|
"password".codeUnits,
|
|
);
|
|
expect(result, true);
|
|
});
|
|
|
|
test('argon2i', () {
|
|
final result = argon2i(
|
|
'password'.codeUnits,
|
|
"some salt".codeUnits,
|
|
hashLength: 16,
|
|
security: Argon2Security(
|
|
'custom',
|
|
m: 16,
|
|
p: 1,
|
|
t: 2,
|
|
),
|
|
);
|
|
final matcher = "bb5794ea66451b8fce3a84dd02d33949";
|
|
final encoded =
|
|
r"$argon2i$v=19$m=16,t=2,p=1$c29tZSBzYWx0$u1eU6mZFG4/OOoTdAtM5SQ";
|
|
expect(result.hex(), matcher);
|
|
expect(result.encoded(), encoded);
|
|
});
|
|
|
|
test('argon2d', () {
|
|
final result = argon2d(
|
|
'password'.codeUnits,
|
|
"some salt".codeUnits,
|
|
hashLength: 16,
|
|
security: Argon2Security(
|
|
'custom',
|
|
m: 16,
|
|
p: 1,
|
|
t: 2,
|
|
),
|
|
);
|
|
final matcher = "cf916880b91ba8a1390fff6b624baa27";
|
|
final salt = r"$argon2d$v=19$m=16,t=2,p=1$c29tZSBzYWx0";
|
|
expect(result.hex(), matcher);
|
|
expect(result.encoded(), startsWith(salt));
|
|
});
|
|
|
|
test('argon2id', () {
|
|
final result = argon2id(
|
|
'password'.codeUnits,
|
|
"some salt".codeUnits,
|
|
hashLength: 16,
|
|
security: Argon2Security(
|
|
'custom',
|
|
m: 16,
|
|
p: 1,
|
|
t: 2,
|
|
),
|
|
);
|
|
final matcher = "88c91661b3cea3c3853593608881f324";
|
|
final salt = r"$argon2id$v=19$m=16,t=2,p=1$c29tZSBzYWx0";
|
|
expect(result.hex(), matcher);
|
|
expect(result.encoded(), startsWith(salt));
|
|
});
|
|
|
|
test("multiple call with same instance", () {
|
|
final argon2 = Argon2(
|
|
version: Argon2Version.v13,
|
|
type: Argon2Type.argon2i,
|
|
hashLength: 16,
|
|
iterations: 2,
|
|
parallelism: 1,
|
|
memorySizeKB: 16,
|
|
salt: "some salt".codeUnits,
|
|
);
|
|
final matcher = "bb5794ea66451b8fce3a84dd02d33949";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
|
|
test("with personalization", () {
|
|
final personalization = "personalization".codeUnits;
|
|
final argon2 = Argon2.fromSecurity(
|
|
Argon2Security.test,
|
|
hashLength: 16,
|
|
salt: "some salt".codeUnits,
|
|
personalization: personalization,
|
|
);
|
|
final matcher = "33561b7ad59b447aa5f6e3113d2f32ca";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
expect(argon2.personalization, equals(personalization));
|
|
});
|
|
|
|
test("with key", () {
|
|
final key = "random key".codeUnits;
|
|
final argon2 = Argon2.fromSecurity(
|
|
Argon2Security.test,
|
|
hashLength: 16,
|
|
salt: "some salt".codeUnits,
|
|
key: key,
|
|
);
|
|
final matcher = "4ad0d2f98a6e7f6e3e99c520d1813c07";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
expect(argon2.key, equals(key));
|
|
});
|
|
|
|
test("with key and personalization", () {
|
|
final argon2 = Argon2.fromSecurity(
|
|
Argon2Security.test,
|
|
hashLength: 16,
|
|
salt: "some salt".codeUnits,
|
|
key: "random key".codeUnits,
|
|
personalization: "personalization".codeUnits,
|
|
);
|
|
final matcher = "0bd2a8c0386b4125a6f439f2f863fc35";
|
|
expect(argon2.convert('password'.codeUnits).hex(), matcher);
|
|
});
|
|
});
|
|
|
|
group('Argon2Context Tests', () {
|
|
// Test for Argon2Context constructor
|
|
test('Creates Argon2Context with valid parameters', () {
|
|
var salt = List<int>.generate(16, (i) => i);
|
|
var context = Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
salt: salt,
|
|
hashLength: 32,
|
|
);
|
|
|
|
expect(context.salt, equals(salt));
|
|
expect(context.passes, equals(3));
|
|
expect(context.lanes, equals(2));
|
|
expect(context.memorySizeKB, equals(8192));
|
|
expect(context.hashLength, equals(32));
|
|
});
|
|
|
|
// Tests for invalid parameters
|
|
test('Throws if hashLength is too small', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
hashLength: 3,
|
|
),
|
|
throwsA(isA<ArgumentError>().having(
|
|
(e) => e.message, 'message', 'The tag length must be at least 4')),
|
|
);
|
|
});
|
|
|
|
test('Throws if hashLength is too large', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
hashLength: 0x3FFFFFF + 1,
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The tag length must be at most 67108863')),
|
|
);
|
|
});
|
|
|
|
test('Throws if parallelism is too small', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 0,
|
|
memorySizeKB: 8192,
|
|
),
|
|
throwsA(isA<ArgumentError>().having(
|
|
(e) => e.message, 'message', 'The parallelism must be at least 1')),
|
|
);
|
|
});
|
|
|
|
test('Throws if parallelism is too large', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 0x7FFF + 1,
|
|
memorySizeKB: 8192,
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The parallelism must be at most 32767')),
|
|
);
|
|
});
|
|
|
|
test('Throws if iterations are too few', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 0,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
),
|
|
throwsA(isA<ArgumentError>().having(
|
|
(e) => e.message, 'message', 'The iterations must be at least 1')),
|
|
);
|
|
});
|
|
|
|
test('Throws if iterations are too many', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 0x3FFFFFF + 1,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The iterations must be at most 67108863')),
|
|
);
|
|
});
|
|
|
|
test('Throws if memorySizeKB is too small for parallelism', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 15,
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The memory size must be at least 8 * parallelism')),
|
|
);
|
|
});
|
|
|
|
test('Throws if memorySizeKB is too large', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 0x3FFFFFF + 1,
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The memorySizeKB must be at most 67108863')),
|
|
);
|
|
});
|
|
|
|
test('Throws if salt is too short', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
salt: List<int>.generate(7, (i) => i),
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The salt must be at least 8 bytes long')),
|
|
);
|
|
});
|
|
|
|
test('Throws if salt is too long', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
salt: Uint8List(0x3FFFFFF + 1),
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The salt must be at most 67108863 bytes long')),
|
|
);
|
|
});
|
|
|
|
test('Throws if key is too short', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
key: [],
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The key must be at least 1 bytes long')),
|
|
);
|
|
});
|
|
|
|
test('Throws if key is too long', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
key: Uint8List(0x3FFFFFF + 1),
|
|
),
|
|
throwsA(isA<ArgumentError>().having((e) => e.message, 'message',
|
|
'The key must be at most 67108863 bytes long')),
|
|
);
|
|
});
|
|
|
|
test('Throws if personalization data is too short', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
personalization: [],
|
|
),
|
|
throwsA(isA<ArgumentError>()),
|
|
);
|
|
});
|
|
|
|
test('Throws if personalization data is too long', () {
|
|
expect(
|
|
() => Argon2Context(
|
|
iterations: 3,
|
|
parallelism: 2,
|
|
memorySizeKB: 8192,
|
|
personalization: Uint8List(0x3FFFFFF + 1),
|
|
),
|
|
throwsArgumentError,
|
|
);
|
|
});
|
|
|
|
// Test for Argon2Context.fromEncoded factory method
|
|
test('Creates Argon2Context from encoded string with valid parameters', () {
|
|
var cryptData = fromCrypt(
|
|
r'$argon2id$v=19$m=8192,t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
var context = Argon2Context.fromEncoded(cryptData);
|
|
|
|
expect(context.type, equals(Argon2Type.argon2id));
|
|
expect(context.version, equals(Argon2Version.v13));
|
|
expect(context.memorySizeKB, equals(8192));
|
|
expect(context.passes, equals(3));
|
|
expect(context.lanes, equals(2));
|
|
expect(context.salt.length, equals(9));
|
|
});
|
|
|
|
test('Throws if encoded string has invalid type', () {
|
|
var cryptData = fromCrypt(
|
|
r'$argon2x$v=19$m=8192,t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
expect(
|
|
() => Argon2Context.fromEncoded(cryptData),
|
|
throwsA(isA<ArgumentError>()
|
|
.having((e) => e.message, 'message', 'Unknown type')),
|
|
);
|
|
});
|
|
|
|
test('Throws if encoded string has invalid version', () {
|
|
var cryptData = fromCrypt(
|
|
r'$argon2i$v=99$m=8192,t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
expect(
|
|
() => Argon2Context.fromEncoded(cryptData),
|
|
throwsA(isA<ArgumentError>()
|
|
.having((e) => e.message, 'message', 'Unknown version')),
|
|
);
|
|
});
|
|
|
|
test('Throws if encoded string has no parameters', () {
|
|
var cryptData =
|
|
fromCrypt(r'$argon2i$v=19$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
expect(
|
|
() => Argon2Context.fromEncoded(cryptData),
|
|
throwsA(isA<ArgumentError>()
|
|
.having((e) => e.message, 'message', 'No paramters')),
|
|
);
|
|
});
|
|
|
|
test('Throws if encoded string is missing "m" parameter', () {
|
|
var cryptData = fromCrypt(
|
|
r'$argon2i$v=19$t=3,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
expect(
|
|
() => Argon2Context.fromEncoded(cryptData),
|
|
throwsA(isA<ArgumentError>()
|
|
.having((e) => e.message, 'message', 'Missing parameter: m')),
|
|
);
|
|
});
|
|
|
|
test('Throws if encoded string is missing "t" parameter', () {
|
|
var cryptData = fromCrypt(
|
|
r'$argon2i$v=19$m=8192,p=2$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
expect(
|
|
() => Argon2Context.fromEncoded(cryptData),
|
|
throwsA(isA<ArgumentError>()
|
|
.having((e) => e.message, 'message', 'Missing parameter: t')),
|
|
);
|
|
});
|
|
|
|
test('Throws if encoded string is missing "p" parameter', () {
|
|
var cryptData = fromCrypt(
|
|
r'$argon2i$v=19$m=8192,t=3$c29tZSBzYWx0$CZOgzrCgoVUzMoR/dcUZyw');
|
|
expect(
|
|
() => Argon2Context.fromEncoded(cryptData),
|
|
throwsA(isA<ArgumentError>()
|
|
.having((e) => e.message, 'message', 'Missing parameter: p')),
|
|
);
|
|
});
|
|
});
|
|
}
|