import 'dart:typed_data'; import 'package:halkyon_keys/halkyon_keys.dart'; import 'package:sodium/sodium_sumo.dart'; import 'package:test/test.dart'; Future main() async { group("a password as RFC 8265's OpaqueString takes it", () { test('accents composed or decomposed, as systems type them, are one ' 'password', () { // é as one character, and as e with a combining accent (macOS). expect( normalizePassword('café au lait'), normalizePassword('café au lait'), ); expect(normalizePassword('café'), 'café'); }); test('every other space is a plain space', () { for (final space in [ ' ', // no-break space ' ', // en space ' ', // thin space ' ', // narrow no-break space ' ', // ideographic space ]) { expect(normalizePassword('two${space}words'), 'two words'); } }); test('nothing else changes: case, width and what is typed stay', () { expect(normalizePassword('Correct Horse'), 'Correct Horse'); expect(normalizePassword('FULL'), 'FULL'); expect(normalizePassword(' padded '), ' padded '); expect(normalizePassword('пароль 密码 🔑'), 'пароль 密码 🔑'); }); test('an empty password, or one with control characters, is refused', () { expect(() => normalizePassword(''), throwsA(isA())); for (final control in ['\u0000', '\t', '\n', '\u007f', '\u0085']) { expect( () => normalizePassword('pass${control}word'), throwsA(isA()), ); } }); }); test('the keys are made from the normalised password: typed either way, ' 'the same keys', () async { final sodium = await SodiumSumoInit.init(); const cheap = PasswordKdf(opsLimit: 1, memLimit: 8192); final salt = Uint8List.fromList(List.generate(16, (i) => i)); PasswordKeys keysOf(String password) => derivePasswordKeys( sodium, password: password, salt: salt, kdf: cheap, ); final composed = keysOf('café noir'); final decomposed = keysOf('café noir'); expect(composed.srpPrivateValue, decomposed.srpPrivateValue); expect( composed.keyEncryptionKey.extractBytes(), decomposed.keyEncryptionKey.extractBytes(), ); }); }