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
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.
handle: int
SigningKey
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.
handle: int
AgreementKey
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.
handle: int
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.