crypto
std/crypto — bindings, not implementations.
Every constant-time guarantee a cipher makes is a guarantee about generated machine code, and Kite compiles through WasmGC to an engine free to reorder anything. A pure-Kite AES would look correct and leak timing, which is worse than none — so this module is a thin declared boundary over the host's own primitives: WebCrypto in a browser or in Node, and whatever the runtime supplies elsewhere.
Three commitments, all of them about removing choices:
- No ECB, no CBC, no MD5, no SHA-1, no raw RSA. A primitive that is
dangerous by default is not offered.
- Salts and nonces are generated, never passed. The most common
failure in applied cryptography is a reused nonce, and an API that accepts one invites it.
- Secrets are compared with
crypto.equal, not==. Structural
equality short-circuits, which is a timing oracle. The compiler warns when a value that came from here is compared with ==.
Keys are the same idea carried further. A key here is an opaque handle to material the host holds: it cannot be printed, it cannot wander into a log, and the private half of a pair cannot be exported at all — not as a discipline the caller must keep, but because the material never crosses the boundary in the first place.
randomtokensha256sha384sha512hmacequalpassword_hashpassword_verifyKeySigningKeyAgreementKeygenerate_keyimport_keysealopensigning_keyverify_keysignverifyagreement_keyexchange_keyagree
random
pub fn random(count: int) -> str
count random bytes, as lowercase hex.
token
pub fn token() -> str
A random 128-bit identifier, as hex. Not a formatted UUID: the hyphens carry no information and invite parsing that does not need to exist.
sha256
pub async fn sha256(text: str) -> (str, error)
sha384
pub async fn sha384(text: str) -> (str, error)
sha512
pub async fn sha512(text: str) -> (str, error)
hmac
pub async fn hmac(key: str, text: str) -> (str, error)
HMAC-SHA-256 of text under key.
equal
pub fn equal(a: str, b: str) -> bool
Whether two secrets are the same, in time that does not depend on where they first differ.
== on two strings short-circuits at the first difference, which tells an attacker how much of a guess was right. This does not.
password_hash
pub async fn password_hash(password: str) -> (str, error)
A password, hashed for storage.
The salt is generated here and carried in the result, because a caller who has to supply one is a caller who will eventually supply the same one twice. The result is pbkdf2$iterations$salt$hash, which is enough to verify against and to raise the iteration count later without invalidating what is already stored.
password_verify
pub async fn password_verify(password: str, stored: str) -> (bool, error)
Whether a password matches something password_hash produced.
Key
pub struct Key
A symmetric key for seal and open.
A key is a handle, not text: the material lives on the host's side of the boundary, and this struct only names it. Nothing here implements Display, so a key cannot reach a log by way of io.print, and comparing two keys with == compares handles — there is no secret for the comparison to leak. There is no export either: a generated key lives as long as the program, and a key that must outlive it should come through import_key, whose hex the caller already holds.
SigningKey
pub struct SigningKey
An Ed25519 key pair for sign. The public half is exported with verify_key; the private half never crosses the boundary at all, which is what makes it impossible to leak rather than merely discouraged.
AgreementKey
pub struct AgreementKey
An X25519 key pair for agree. The public half is exported with exchange_key and handed to the other side; the private half stays on the host, as with SigningKey.
generate_key
pub async fn generate_key() -> (Key, error)
A new random 256-bit AES-GCM key, generated and held by the host.
The key is deliberately ephemeral: there is no way to get its material out, so it lasts as long as the program does and no longer. A key that must be stored or shared starts as hex the caller already has — import_key(crypto.random(32)) — because material that was never in the program cannot be asked for later.
import_key
pub async fn import_key(material: str) -> (Key, error)
A 256-bit AES-GCM key from 64 hex characters the caller already holds: from crypto.random(32), a configuration store, or a derivation.
Import is one-way. The hex goes in and a handle comes out, and the module never hands material back — keeping the source hex safe is the caller's side of the bargain.
seal
pub async fn seal(key: Key, plaintext: str) -> (str, error)
plaintext, sealed under key: encrypted and authenticated AES-256-GCM, as text that can travel anywhere text can.
The 96-bit nonce is generated fresh on every call and carried in the result. It is not a parameter, because a nonce that can be passed is a nonce that will eventually be passed twice, and a repeated nonce under GCM gives away the authentication key. The result is gcm$nonce$ciphertext — the shape password_hash uses, for the same reason: everything needed to open it travels with it.
open
pub async fn open(key: Key, sealed: str) -> (str, error)
The plaintext a seal under the same key produced.
A failure is an error, not a wrong answer: GCM authenticates before it decrypts, so sealed text that was altered in transit — or sealed under a different key — is reported, never returned as garbage.
signing_key
pub async fn signing_key() -> (SigningKey, error)
A new Ed25519 key pair for signing.
The pair is ephemeral for the same reason a generated key is: the private half has no way out, so it cannot be stored and cannot be stolen. A program that needs a signature to outlive it keeps the public half — verify_key — which is the only part a verifier ever needs.
verify_key
pub async fn verify_key(key: SigningKey) -> (str, error)
The public half of a signing pair, as 64 hex characters.
This is the one thing about a pair that is exportable, because it is the half meant to be given away: it verifies, and cannot sign.
sign
pub async fn sign(key: SigningKey, text: str) -> (str, error)
The Ed25519 signature of text under the pair's private half, as hex.
verify
pub async fn verify(public: str, text: str, signature: str) -> (bool, error)
Whether signature is text, signed by the pair whose public half public is.
The public key is hex rather than a type of its own because it is the one piece of key material that is public: it arrives from the other side as text, and a wrapper would add a step without adding a guarantee. A signature that does not match answers false; a public key that is not one at all is an error.
agreement_key
pub async fn agreement_key() -> (AgreementKey, error)
A new X25519 key pair for key agreement. As with a signing pair, the private half never leaves the host.
exchange_key
pub async fn exchange_key(key: AgreementKey) -> (str, error)
The public half of an agreement pair, as 64 hex characters — the piece each side hands the other before agree.
agree
pub async fn agree(key: AgreementKey, their_public: str) -> (Key, error)
The key this pair agrees with their_public — the other side's exchange_key. Both sides compute the same Key; nobody watching the exchange can.
What comes back is a sealing key, not the raw agreement output. A raw X25519 secret is a curve point with structure — some bits are fixed, and related public keys produce related secrets — so handing it out as a key would quietly weaken whatever used it. The host runs it through HKDF-SHA-256 instead, which spreads it into a uniform 256-bit AES-GCM key, and the raw output never crosses the boundary at all.