Kite

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:

dangerous by default is not offered.

failure in applied cryptography is a reused nonce, and an API that accepts one invites it.

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.

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.

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.

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.

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.