from std/secure import PasswordHash, Secure;
from std/string/base64 import decode, encode;
from test/more import *;
let password := "correct horse battery staple";
let wrong := "correct horse battery stapler";
let salt := decode("AAECAwQFBgcICQoLDA0ODw==");
let pbkdf2_fixture := "$zuzu-pbkdf2-sha256$v=1$i=1000,l=32"
_ "$AAECAwQFBgcICQoLDA0ODw"
_ "$ppsXnjrdPB4KryJ6DrOqKqhkWrhv7PbKAMF1Eml8cZ4";
let scrypt_fixture := "$scrypt$ln=14,r=8,p=1,l=32"
_ "$AAECAwQFBgcICQoLDA0ODw"
_ "$11kKyiyYAc8G7rp3KmncMc44YlkdllIqxOa7pq0fMaU";
async function async_exception_message ( Function f ) {
try {
await {
f();
};
return null;
}
catch ( Exception e ) {
return e{message};
}
}
if ( Secure.capabilities(){host} == "browser" ) {
like(
exception( function () {
PasswordHash.hash(
password,
{ iterations: 1000, salt: salt },
);
} ),
/not available synchronously|hash_async/,
"browser rejects synchronous password hashing",
);
is(
await {
PasswordHash.hash_async(
password,
{ iterations: 1000, salt: salt },
);
},
pbkdf2_fixture,
"pbkdf2 async hash uses the stable Zuzu encoding",
);
is(
await {
PasswordHash.verify_async( password, pbkdf2_fixture );
},
true,
"pbkdf2 async verify resolves true",
);
is(
await {
PasswordHash.verify_async( wrong, pbkdf2_fixture );
},
false,
"pbkdf2 async verify rejects wrong password",
);
is(
await {
PasswordHash.verify_async( password, "$unknown$hash" );
},
false,
"unknown hash formats do not verify asynchronously",
);
is(
PasswordHash.needs_rehash(pbkdf2_fixture),
true,
"low-iteration pbkdf2 fixture needs default rehash",
);
is(
PasswordHash.needs_rehash(
pbkdf2_fixture,
{ iterations: 1000 },
),
false,
"pbkdf2 fixture does not need rehash with matching options",
);
is(
PasswordHash.needs_rehash("$unknown$hash"),
true,
"unknown hash formats need rehash",
);
is(
encode(
await {
PasswordHash.derive_key_async(
password,
{ iterations: 1000, salt: salt },
);
},
),
"ppsXnjrdPB4KryJ6DrOqKqhkWrhv7PbKAMF1Eml8cZ4=",
"pbkdf2 async derive_key returns the expected bytes",
);
like(
exception( function () {
PasswordHash.derive_key(
password,
{ iterations: 1000, salt: salt },
);
} ),
/not available synchronously|derive_key_async/,
"browser rejects synchronous key derivation",
);
like(
await {
async_exception_message( function () {
PasswordHash.derive_key_async(
password,
{ iterations: 1000 },
);
} );
},
/salt/,
"derive_key_async requires an explicit salt",
);
like(
await {
async_exception_message( function () {
PasswordHash.hash_async(
password,
{ algorithm: "argon2id", salt: salt },
);
} );
},
/not available|unsupported/,
"browser rejects async argon2id",
);
like(
await {
async_exception_message( function () {
PasswordHash.hash_async(
password,
{ algorithm: "scrypt", salt: salt },
);
} );
},
/not available|unsupported/,
"browser rejects async scrypt",
);
}
else {
is(
PasswordHash.hash(
password,
{ iterations: 1000, salt: salt },
),
pbkdf2_fixture,
"pbkdf2 hash uses the stable Zuzu encoding",
);
is(
PasswordHash.verify( password, pbkdf2_fixture ),
true,
"pbkdf2 verifies the correct password",
);
is(
PasswordHash.verify( wrong, pbkdf2_fixture ),
false,
"pbkdf2 rejects the wrong password",
);
is(
PasswordHash.needs_rehash(pbkdf2_fixture),
true,
"low-iteration pbkdf2 fixture needs default rehash",
);
is(
PasswordHash.needs_rehash(
pbkdf2_fixture,
{ iterations: 1000 },
),
false,
"pbkdf2 fixture does not need rehash with matching options",
);
is(
encode(
PasswordHash.derive_key(
password,
{ iterations: 1000, salt: salt },
),
),
"ppsXnjrdPB4KryJ6DrOqKqhkWrhv7PbKAMF1Eml8cZ4=",
"pbkdf2 derive_key returns the expected bytes",
);
is(
await {
PasswordHash.verify_async( password, pbkdf2_fixture );
},
true,
"pbkdf2 async verify resolves true",
);
is(
encode(
await {
PasswordHash.derive_key_async(
password,
{ iterations: 1000, salt: salt },
);
},
),
"ppsXnjrdPB4KryJ6DrOqKqhkWrhv7PbKAMF1Eml8cZ4=",
"pbkdf2 async derive_key returns the expected bytes",
);
like(
exception( function () {
PasswordHash.derive_key( password, { iterations: 1000 } );
} ),
/salt/,
"derive_key requires an explicit salt",
);
is(
PasswordHash.verify( password, "$unknown$hash" ),
false,
"unknown hash formats do not verify",
);
is(
PasswordHash.needs_rehash("$unknown$hash"),
true,
"unknown hash formats need rehash",
);
like(
exception( function () {
PasswordHash.hash( password, { algorithm: "unknown" } );
} ),
/not available/,
"unknown hash algorithms are rejected",
);
if ( Secure.has( "password_hash", "argon2id" ) ) {
let argon := PasswordHash.hash(
password,
{
algorithm: "argon2id",
memory: 64,
iterations: 1,
parallelism: 1,
salt: salt,
},
);
like(
argon,
/^\$argon2id\$v=19\$m=64,t=1,p=1\$/,
"argon2id uses PHC-style encoding",
);
is(
PasswordHash.verify( password, argon ),
true,
"argon2id verifies the correct password",
);
is(
PasswordHash.verify( wrong, argon ),
false,
"argon2id rejects the wrong password",
);
is(
PasswordHash.needs_rehash(argon),
true,
"low-cost argon2id hash needs default rehash",
);
is(
length PasswordHash.derive_key(
password,
{
algorithm: "argon2id",
memory: 64,
iterations: 1,
parallelism: 1,
salt: salt,
},
),
32,
"argon2id derive_key returns requested bytes",
);
}
if ( Secure.has( "password_hash", "scrypt" ) ) {
is(
PasswordHash.hash(
password,
{
algorithm: "scrypt",
log_n: 14,
salt: salt,
},
),
scrypt_fixture,
"scrypt hash uses the stable Zuzu encoding",
);
is(
PasswordHash.verify( password, scrypt_fixture ),
true,
"scrypt verifies the correct password",
);
is(
PasswordHash.verify( wrong, scrypt_fixture ),
false,
"scrypt rejects the wrong password",
);
is(
PasswordHash.needs_rehash(scrypt_fixture),
true,
"low-cost scrypt fixture needs default rehash",
);
is(
PasswordHash.needs_rehash(
scrypt_fixture,
{ algorithm: "scrypt", log_n: 14 },
),
false,
"scrypt fixture does not need rehash with matching options",
);
is(
length PasswordHash.derive_key(
password,
{
algorithm: "scrypt",
log_n: 14,
salt: salt,
},
),
32,
"scrypt derive_key returns requested bytes",
);
}
if ( Secure.has( "password_hash", "crypt" ) ) {
let crypt_hash := PasswordHash.hash(
password,
{ algorithm: "crypt" },
);
is(
PasswordHash.verify( password, crypt_hash ),
true,
"crypt verifies the correct password",
);
is(
PasswordHash.verify( wrong, crypt_hash ),
false,
"crypt rejects the wrong password",
);
is(
PasswordHash.needs_rehash(crypt_hash),
true,
"crypt hashes always need rehash by default",
);
}
}
done_testing();