zuzu-rust 0.2.0

Rust implementation of ZuzuScript
Documentation
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();