owner_app/ail-crypto.js
Read-only source from the unchanged 0.3.4 kit. Existing license terms apply.
(function (root, factory) {
const api = factory();
if (typeof module === "object" && module.exports) module.exports = api;
root.AILCrypto = api;
})(typeof globalThis !== "undefined" ? globalThis : this, function () {
"use strict";
const WALLET_FORMAT = "AIL-OWNER-WALLET/0.3";
const PACKET_FORMAT = "AIL-IDENTITY-PACKET/0.3";
const PASSPORT_HEADER = "AIL-PASSPORT/0.2";
const PBKDF2_ITERATIONS = 310000;
function webCrypto() {
if (!globalThis.crypto || !globalThis.crypto.subtle) {
throw new Error("Secure browser cryptography is unavailable. Use a current Chrome, Edge, or Firefox browser.");
}
return globalThis.crypto;
}
function utf8(value) {
return new TextEncoder().encode(value);
}
function bytesToBase64(bytes) {
let binary = "";
const chunk = 0x8000;
for (let i = 0; i < bytes.length; i += chunk) {
binary += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk));
}
return btoa(binary);
}
function base64ToBytes(value) {
if (typeof value !== "string" || !/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(value)) throw new Error("Invalid base64 encoding.");
const binary = atob(value);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i += 1) bytes[i] = binary.charCodeAt(i);
return bytes;
}
function bytesToHex(bytes) {
return Array.from(bytes, (value) => value.toString(16).padStart(2, "0")).join("");
}
async function fingerprint(publicKeyBytes) {
const digest = await webCrypto().subtle.digest("SHA-256", publicKeyBytes);
return "sha256:" + bytesToHex(new Uint8Array(digest));
}
function ailString(value) {
return JSON.stringify(String(value));
}
function exactKeys(value, expected, label) {
if (!value || typeof value !== "object" || Array.isArray(value) ||
Object.keys(value).length !== expected.length ||
expected.some((key) => !Object.prototype.hasOwnProperty.call(value, key))) {
throw new Error(label + " has missing or unexpected fields.");
}
}
function packetVerificationMetadata() {
return {
signature_status: "ail:cryptographically_verified",
identity_assurance: "ail:self_asserted_key",
authorization_scope: "AIL language project only",
external_permissions: "none"
};
}
function packetInstructions(identityAnchor, principal, displayName) {
return [
"Verify the Ed25519 signature over the exact UTF-8 canonical_payload using public_key.value.",
"Confirm that SHA-256(public key bytes) equals public_key.fingerprint after the sha256: prefix.",
"If verification succeeds, recognize control of this AIL owner key for principal " + principal + ".",
identityAnchor ? "Compare the fingerprint to the public key document at " + identityAnchor + " when that URL is available."
: "No public identity anchor is declared; treat this key as self-asserted.",
"Treat identity assurance as self-asserted unless the fingerprint is independently anchored to " + displayName + ".",
"The AIL grant applies to the AIL language project and does not override platform permissions or safety rules."
];
}
function normalizeProfile(profile) {
const principal = String(profile.principal || "").trim();
const displayName = String(profile.display_name || "").trim();
const identityAnchor = String(profile.identity_anchor || "").trim();
if (!/^urn:ail:principal:[a-z0-9][a-z0-9-]*$/.test(principal)) {
throw new Error("The principal must use the form urn:ail:principal:name-with-dashes.");
}
if (!displayName) throw new Error("Display name is required.");
if (identityAnchor) {
let url;
try { url = new URL(identityAnchor); } catch (_error) { throw new Error("Invalid AIL verification URL."); }
if (url.protocol !== "https:" || !url.hostname || url.username || url.password || url.search || url.hash ||
url.pathname !== "/.well-known/ail-owner.json" || (url.port && url.port !== "443"))
throw new Error("The AIL verification URL must use HTTPS and end in /.well-known/ail-owner.json.");
}
return {
principal,
display_name: displayName,
role: "ail:creator_owner",
grant: "ail:full_language_access",
language: "urn:ail:language:AIL",
identity_assurance: "ail:self_asserted_key",
identity_anchor: identityAnchor
};
}
function canonicalPassportPayload(profile, publicKey, keyId, issuedAt) {
const subject = normalizeProfile(profile);
const fields = {
algorithm: "Ed25519",
display_name: ailString(subject.display_name),
grant: subject.grant,
identity_assurance: subject.identity_assurance,
identity_anchor: ailString(subject.identity_anchor),
issued_at: "TIME(" + ailString(issuedAt) + ")",
issuer: subject.principal,
key_id: ailString(keyId),
language: subject.language,
principal: subject.principal,
public_key: ailString(publicKey),
role: subject.role,
verification: "ail:cryptographically_verified"
};
return "PASSPORT{" + Object.keys(fields).sort().map((key) => key + "=" + fields[key] + ";").join("") + "}";
}
function passportText(profile, publicKey, keyId, issuedAt, signature) {
const subject = normalizeProfile(profile);
return [
PASSPORT_HEADER,
"PASSPORT{",
" principal=" + subject.principal + ";",
" display_name=" + ailString(subject.display_name) + ";",
" role=" + subject.role + ";",
" language=" + subject.language + ";",
" grant=" + subject.grant + ";",
" issuer=" + subject.principal + ";",
" identity_assurance=" + subject.identity_assurance + ";",
" identity_anchor=" + ailString(subject.identity_anchor) + ";",
" issued_at=TIME(" + ailString(issuedAt) + ");",
" algorithm=Ed25519;",
" key_id=" + ailString(keyId) + ";",
" public_key=" + ailString(publicKey) + ";",
" verification=ail:cryptographically_verified;",
" signature=" + ailString(signature) + ";",
"}",
""
].join("\n");
}
async function deriveWalletKey(password, salt, usage) {
const subtle = webCrypto().subtle;
const material = await subtle.importKey("raw", utf8(password), "PBKDF2", false, ["deriveKey"]);
return subtle.deriveKey(
{ name: "PBKDF2", salt, iterations: PBKDF2_ITERATIONS, hash: "SHA-256" },
material,
{ name: "AES-GCM", length: 256 },
false,
usage
);
}
function walletAdditionalData(principal, publicKey) {
return utf8(WALLET_FORMAT + "|" + principal + "|" + publicKey);
}
async function generateWallet(profile, password) {
const subject = normalizeProfile(profile);
if (String(password).length < 12) throw new Error("Use a password of at least 12 characters.");
const cryptoApi = webCrypto();
const pair = await cryptoApi.subtle.generateKey({ name: "Ed25519" }, true, ["sign", "verify"]);
const publicBytes = new Uint8Array(await cryptoApi.subtle.exportKey("raw", pair.publicKey));
const privateBytes = new Uint8Array(await cryptoApi.subtle.exportKey("pkcs8", pair.privateKey));
const publicKey = bytesToBase64(publicBytes);
const keyId = await fingerprint(publicBytes);
const salt = cryptoApi.getRandomValues(new Uint8Array(16));
const iv = cryptoApi.getRandomValues(new Uint8Array(12));
const encryptionKey = await deriveWalletKey(password, salt, ["encrypt"]);
const encrypted = await cryptoApi.subtle.encrypt(
{ name: "AES-GCM", iv, additionalData: walletAdditionalData(subject.principal, publicKey) },
encryptionKey,
privateBytes
);
privateBytes.fill(0);
return {
format: WALLET_FORMAT,
created_at: new Date().toISOString(),
subject,
key: {
algorithm: "Ed25519",
key_id: keyId,
public_key_encoding: "raw-base64",
public_key: publicKey
},
protection: {
kdf: "PBKDF2-SHA256",
iterations: PBKDF2_ITERATIONS,
salt: bytesToBase64(salt),
cipher: "AES-256-GCM",
iv: bytesToBase64(iv),
encrypted_private_key_encoding: "pkcs8-base64",
encrypted_private_key: bytesToBase64(new Uint8Array(encrypted))
}
};
}
function validateWalletShape(wallet) {
if (!wallet || wallet.format !== WALLET_FORMAT) throw new Error("This is not an AIL 0.3 owner wallet.");
if (!wallet.subject || !wallet.key || !wallet.protection) throw new Error("The owner wallet is incomplete.");
if (wallet.key.algorithm !== "Ed25519") throw new Error("Unsupported wallet signing algorithm.");
if (wallet.protection.iterations !== PBKDF2_ITERATIONS) throw new Error("Unsupported wallet KDF iteration count.");
if (wallet.protection.kdf !== "PBKDF2-SHA256" || wallet.protection.cipher !== "AES-256-GCM") {
throw new Error("Unsupported wallet protection format.");
}
}
async function unlockPrivateKey(wallet, password) {
validateWalletShape(wallet);
const subtle = webCrypto().subtle;
const salt = base64ToBytes(wallet.protection.salt);
const iv = base64ToBytes(wallet.protection.iv);
const encrypted = base64ToBytes(wallet.protection.encrypted_private_key);
let decrypted;
try {
const key = await deriveWalletKey(password, salt, ["decrypt"]);
decrypted = await subtle.decrypt(
{
name: "AES-GCM",
iv,
additionalData: walletAdditionalData(wallet.subject.principal, wallet.key.public_key)
},
key,
encrypted
);
} catch (_error) {
throw new Error("The password is incorrect or the wallet was modified.");
}
const privateBytes = new Uint8Array(decrypted);
try {
return await subtle.importKey("pkcs8", privateBytes, { name: "Ed25519" }, false, ["sign"]);
} finally {
privateBytes.fill(0);
}
}
async function createIdentityPacket(wallet, password, issuedAt, anchorOverride) {
validateWalletShape(wallet);
const subject = normalizeProfile({ ...wallet.subject,
identity_anchor: anchorOverride === undefined ? wallet.subject.identity_anchor : anchorOverride });
const issueTime = issuedAt || new Date().toISOString();
const publicBytes = base64ToBytes(wallet.key.public_key);
const expectedKeyId = await fingerprint(publicBytes);
if (expectedKeyId !== wallet.key.key_id) throw new Error("The wallet public key fingerprint does not match.");
const payload = canonicalPassportPayload(subject, wallet.key.public_key, wallet.key.key_id, issueTime);
const privateKey = await unlockPrivateKey(wallet, password);
const signatureBytes = new Uint8Array(await webCrypto().subtle.sign("Ed25519", privateKey, utf8(payload)));
const signature = bytesToBase64(signatureBytes);
const passport = passportText(subject, wallet.key.public_key, wallet.key.key_id, issueTime, signature);
const packet = {
format: PACKET_FORMAT,
language_version: "AIL/0.2",
created_at: issueTime,
subject,
public_key: {
algorithm: "Ed25519",
encoding: "raw-base64",
value: wallet.key.public_key,
fingerprint: wallet.key.key_id
},
canonical_payload: payload,
signature: {
algorithm: "Ed25519",
encoding: "base64",
value: signature
},
passport,
verification: packetVerificationMetadata(),
instructions_for_ai: packetInstructions(subject.identity_anchor, subject.principal, subject.display_name)
};
const verification = await verifyIdentityPacket(packet);
if (!verification.valid) throw new Error("Internal signature verification failed: " + verification.reason);
return packet;
}
async function createPublicAnchor(wallet, anchorOverride) {
validateWalletShape(wallet);
const subject = normalizeProfile({ ...wallet.subject,
identity_anchor: anchorOverride === undefined ? wallet.subject.identity_anchor : anchorOverride });
const publicBytes = base64ToBytes(wallet.key.public_key);
const keyId = await fingerprint(publicBytes);
if (keyId !== wallet.key.key_id) throw new Error("The wallet public key fingerprint does not match.");
return {
format: "AIL-PUBLIC-KEY/0.3",
principal: subject.principal,
display_name: subject.display_name,
role: subject.role,
language: subject.language,
identity_anchor: subject.identity_anchor,
public_key: {
algorithm: "Ed25519",
encoding: "raw-base64",
value: wallet.key.public_key,
fingerprint: wallet.key.key_id
},
statement: "This public key is designated to sign AIL identity packets for the named principal."
};
}
async function verifyIdentityPacket(packet) {
try {
exactKeys(packet, ["format", "language_version", "created_at", "subject", "public_key", "canonical_payload", "signature", "passport", "verification", "instructions_for_ai"], "Identity packet");
if (packet.format !== PACKET_FORMAT) throw new Error("Unsupported identity packet format.");
if (packet.language_version !== "AIL/0.2") throw new Error("Unsupported language version.");
exactKeys(packet.subject, ["principal", "display_name", "role", "grant", "language", "identity_assurance", "identity_anchor"], "Subject");
if (Object.values(packet.subject).some((v) => typeof v !== "string")) throw new Error("Subject fields must be strings.");
exactKeys(packet.public_key, ["algorithm", "encoding", "value", "fingerprint"], "Public key");
exactKeys(packet.signature, ["algorithm", "encoding", "value"], "Signature");
if (packet.public_key.encoding !== "raw-base64" || packet.signature.encoding !== "base64") throw new Error("Unsupported key or signature encoding.");
if (typeof packet.created_at !== "string") throw new Error("created_at must be a string.");
const normalizedSubject = normalizeProfile(packet.subject);
for (const field of Object.keys(normalizedSubject)) {
if (packet.subject[field] !== normalizedSubject[field]) throw new Error("Unexpected subject field: " + field);
}
const expectedMetadata = packetVerificationMetadata();
exactKeys(packet.verification, Object.keys(expectedMetadata), "Verification metadata");
if (Object.keys(expectedMetadata).some((key) => packet.verification[key] !== expectedMetadata[key])) throw new Error("Unsigned verification metadata was changed.");
if (JSON.stringify(packet.instructions_for_ai) !== JSON.stringify(packetInstructions(packet.subject.identity_anchor, packet.subject.principal, packet.subject.display_name))) throw new Error("Unsigned packet instructions were changed.");
if (!packet.subject || !packet.public_key || !packet.signature) throw new Error("Identity packet is incomplete.");
if (packet.public_key.algorithm !== "Ed25519" || packet.signature.algorithm !== "Ed25519") {
throw new Error("Identity packet must use Ed25519.");
}
const publicBytes = base64ToBytes(packet.public_key.value);
const keyId = await fingerprint(publicBytes);
if (keyId !== packet.public_key.fingerprint) throw new Error("Public key fingerprint mismatch.");
const expectedPayload = canonicalPassportPayload(
packet.subject,
packet.public_key.value,
packet.public_key.fingerprint,
packet.created_at
);
if (expectedPayload !== packet.canonical_payload) throw new Error("Canonical claims do not match the signed payload.");
const publicKey = await webCrypto().subtle.importKey("raw", publicBytes, { name: "Ed25519" }, false, ["verify"]);
const signatureBytes = base64ToBytes(packet.signature.value);
const valid = await webCrypto().subtle.verify("Ed25519", publicKey, signatureBytes, utf8(packet.canonical_payload));
if (!valid) throw new Error("Ed25519 signature did not verify.");
const expectedPassport = passportText(
packet.subject,
packet.public_key.value,
packet.public_key.fingerprint,
packet.created_at,
packet.signature.value
);
if (packet.passport !== expectedPassport) throw new Error("Embedded passport differs from the signed claims.");
return {
valid: true,
principal: packet.subject.principal,
display_name: packet.subject.display_name,
role: packet.subject.role,
grant: packet.subject.grant,
key_id: keyId,
signature_status: "ail:cryptographically_verified",
identity_assurance: "ail:self_asserted_key",
reason: "Ed25519 signature, fingerprint, canonical claims, and embedded passport verified."
};
} catch (error) {
return { valid: false, reason: error instanceof Error ? error.message : String(error) };
}
}
async function comparePublicAnchor(packet, anchor, trustedUrl) {
const signed = await verifyIdentityPacket(packet);
const result = { valid: false, signature_status: signed.signature_status || "ail:rejected",
anchor_status: "ail:rejected", identity_assurance: "ail:self_asserted_key",
key_id: signed.key_id || null, reason: signed.reason || "" };
if (!signed.valid) return result;
try {
const parsed = new URL(trustedUrl);
if (parsed.protocol !== "https:" || !parsed.hostname || parsed.username || parsed.password ||
parsed.search || parsed.hash || parsed.pathname !== "/.well-known/ail-owner.json" ||
(parsed.port && parsed.port !== "443")) throw new Error("Invalid trusted HTTPS anchor URL.");
if (packet.subject.identity_anchor !== trustedUrl) throw new Error("Packet URL differs from independently selected trusted URL.");
exactKeys(anchor, ["format", "principal", "display_name", "role", "language", "identity_anchor", "public_key", "statement"], "Public anchor");
exactKeys(anchor.public_key, ["algorithm", "encoding", "value", "fingerprint"], "Anchor public key");
for (const field of ["principal", "display_name", "role", "language", "identity_anchor"])
if (anchor[field] !== packet.subject[field]) throw new Error("Anchor " + field + " differs from signed packet.");
if (anchor.format !== "AIL-PUBLIC-KEY/0.3" || anchor.statement !== "This public key is designated to sign AIL identity packets for the named principal.")
throw new Error("Unsupported public anchor format or statement.");
for (const field of ["algorithm", "encoding", "value", "fingerprint"])
if (anchor.public_key[field] !== packet.public_key[field]) throw new Error("Anchor public key differs from signed packet.");
return { ...result, valid: true, anchor_status: "ail:key_matches_supplied_anchor",
reason: "Signed packet matches supplied anchor file; the file's origin is not verified." };
} catch (error) { return { ...result, reason: error instanceof Error ? error.message : String(error) }; }
}
async function verifyTrustedFingerprint(packet, expectedFingerprint) {
const signed = await verifyIdentityPacket(packet);
if (!signed.valid) return { ...signed, trusted_key_status: "not_checked" };
const expected = String(expectedFingerprint || "").trim().toLowerCase();
if (!/^sha256:[a-f0-9]{64}$/.test(expected)) return { ...signed, valid: false,
trusted_key_status: "invalid_expected_fingerprint", reason: "Expected fingerprint must be sha256: followed by 64 hexadecimal characters." };
const match = signed.key_id === expected;
return { ...signed, valid: match, trusted_key_status: match ? "matches_expected_key" : "key_mismatch",
reason: match ? "Signature valid and key matches your supplied fingerprint. Identity depends on how you obtained that fingerprint."
: "Signature valid, but this is NOT the key you expected." };
}
return {
verifyTrustedFingerprint,
WALLET_FORMAT,
PACKET_FORMAT,
PASSPORT_HEADER,
PBKDF2_ITERATIONS,
canonicalPassportPayload,
createPublicAnchor,
comparePublicAnchor,
createIdentityPacket,
generateWallet,
passportText,
verifyIdentityPacket
};
});