openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
228 lines (227 loc) • 8.88 kB
JavaScript
import { y as resolveStateDir } from "./paths-mvMm5bYV.js";
import { n as privateFileStoreSync } from "./private-file-store-BAvApZYp.js";
import fs from "node:fs";
import path from "node:path";
import crypto from "node:crypto";
//#region src/infra/device-identity.ts
function resolveDefaultIdentityPath() {
return path.join(resolveStateDir(), "identity", "device.json");
}
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
const ED25519_PKCS8_PRIVATE_PREFIX = Buffer.from("302e020100300506032b657004220420", "hex");
function base64UrlEncode(buf) {
return buf.toString("base64").replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/g, "");
}
function base64UrlDecode(input) {
const normalized = input.replaceAll("-", "+").replaceAll("_", "/");
const padded = normalized + "=".repeat((4 - normalized.length % 4) % 4);
return Buffer.from(padded, "base64");
}
function pemEncode(label, der) {
return `-----BEGIN ${label}-----\n${der.toString("base64").match(/.{1,64}/g)?.join("\n") ?? ""}\n-----END ${label}-----\n`;
}
function publicKeyPemFromRaw(publicKeyRaw) {
return pemEncode("PUBLIC KEY", Buffer.concat([ED25519_SPKI_PREFIX, publicKeyRaw]));
}
function privateKeyPemFromRaw(privateKeyRaw) {
return pemEncode("PRIVATE KEY", Buffer.concat([ED25519_PKCS8_PRIVATE_PREFIX, privateKeyRaw]));
}
function derivePublicKeyRaw(publicKeyPem) {
const spki = crypto.createPublicKey(publicKeyPem).export({
type: "spki",
format: "der"
});
if (spki.length === ED25519_SPKI_PREFIX.length + 32 && spki.subarray(0, ED25519_SPKI_PREFIX.length).equals(ED25519_SPKI_PREFIX)) return spki.subarray(ED25519_SPKI_PREFIX.length);
return spki;
}
function fingerprintPublicKey(publicKeyPem) {
const raw = derivePublicKeyRaw(publicKeyPem);
return crypto.createHash("sha256").update(raw).digest("hex");
}
function tryFingerprintPublicKey(publicKeyPem) {
try {
return fingerprintPublicKey(publicKeyPem);
} catch {
return null;
}
}
function keyPairMatches(publicKeyPem, privateKeyPem) {
try {
const payload = Buffer.from("openclaw-device-identity-self-check", "utf8");
const signature = crypto.sign(null, payload, crypto.createPrivateKey(privateKeyPem));
return crypto.verify(null, payload, crypto.createPublicKey(publicKeyPem), signature);
} catch {
return false;
}
}
function generateIdentity() {
const { publicKey, privateKey } = crypto.generateKeyPairSync("ed25519");
const publicKeyPem = publicKey.export({
type: "spki",
format: "pem"
});
const privateKeyPem = privateKey.export({
type: "pkcs8",
format: "pem"
});
return {
deviceId: fingerprintPublicKey(publicKeyPem),
publicKeyPem,
privateKeyPem
};
}
function isRecord(value) {
return Boolean(value) && typeof value === "object";
}
function hasRecognizedIdentityShape(parsed) {
return isRecord(parsed) && ("publicKeyPem" in parsed || "privateKeyPem" in parsed || "publicKey" in parsed || "privateKey" in parsed);
}
function normalizeStoredIdentity(parsed) {
if (isRecord(parsed) && "version" in parsed && parsed.version === 1 && "deviceId" in parsed && typeof parsed.deviceId === "string" && "publicKeyPem" in parsed && typeof parsed.publicKeyPem === "string" && "privateKeyPem" in parsed && typeof parsed.privateKeyPem === "string") {
const stored = parsed;
const derivedId = tryFingerprintPublicKey(stored.publicKeyPem);
if (!derivedId || !keyPairMatches(stored.publicKeyPem, stored.privateKeyPem)) return { kind: "recognized-invalid" };
const identity = {
deviceId: derivedId,
publicKeyPem: stored.publicKeyPem,
privateKeyPem: stored.privateKeyPem
};
return derivedId === stored.deviceId ? {
kind: "identity",
identity,
validForReadOnly: true
} : {
kind: "identity",
identity,
validForReadOnly: false,
stored: {
...stored,
deviceId: derivedId
}
};
}
if (isRecord(parsed) && !("version" in parsed) && "deviceId" in parsed && typeof parsed.deviceId === "string" && "publicKey" in parsed && typeof parsed.publicKey === "string" && "privateKey" in parsed && typeof parsed.privateKey === "string") {
const stored = parsed;
const publicKeyRaw = base64UrlDecode(stored.publicKey);
const privateKeyRaw = base64UrlDecode(stored.privateKey);
if (publicKeyRaw.length !== 32 || privateKeyRaw.length !== 32) return { kind: "recognized-invalid" };
const publicKeyPem = publicKeyPemFromRaw(publicKeyRaw);
const privateKeyPem = privateKeyPemFromRaw(privateKeyRaw);
if (!keyPairMatches(publicKeyPem, privateKeyPem)) return { kind: "recognized-invalid" };
const derivedId = fingerprintPublicKey(publicKeyPem);
const validForReadOnly = derivedId === stored.deviceId;
const migrated = {
version: 1,
deviceId: derivedId,
publicKeyPem,
privateKeyPem,
createdAtMs: typeof stored.createdAtMs === "number" && Number.isFinite(stored.createdAtMs) ? stored.createdAtMs : Date.now()
};
return {
kind: "identity",
identity: {
deviceId: derivedId,
publicKeyPem,
privateKeyPem
},
validForReadOnly,
stored: migrated
};
}
return hasRecognizedIdentityShape(parsed) ? { kind: "recognized-invalid" } : null;
}
function identityFileExists(filePath) {
try {
return fs.statSync(filePath).isFile();
} catch {
return false;
}
}
/** Load a valid persisted identity, repair/migrate when safe, or create a new one. */
function loadOrCreateDeviceIdentity(filePath = resolveDefaultIdentityPath()) {
try {
const store = privateFileStoreSync(path.dirname(filePath));
const normalized = normalizeStoredIdentity(store.readJsonIfExists(path.basename(filePath)));
if (normalized?.kind === "identity") {
if (normalized.stored) try {
store.writeJson(path.basename(filePath), normalized.stored, { trailingNewline: true });
} catch {}
return normalized.identity;
}
if (normalized?.kind === "recognized-invalid") return generateIdentity();
} catch {
if (identityFileExists(filePath)) return generateIdentity();
}
const identity = generateIdentity();
const stored = {
version: 1,
deviceId: identity.deviceId,
publicKeyPem: identity.publicKeyPem,
privateKeyPem: identity.privateKeyPem,
createdAtMs: Date.now()
};
privateFileStoreSync(path.dirname(filePath)).writeJson(path.basename(filePath), stored, { trailingNewline: true });
return identity;
}
/** Load a valid persisted device identity without creating, repairing, or migrating files. */
function loadDeviceIdentityIfPresent(filePath = resolveDefaultIdentityPath()) {
try {
const normalized = normalizeStoredIdentity(privateFileStoreSync(path.dirname(filePath)).readJsonIfExists(path.basename(filePath)));
if (normalized?.kind !== "identity" || !normalized.validForReadOnly) return null;
return normalized.identity;
} catch {
return null;
}
}
/** Sign a UTF-8 payload with a PEM Ed25519 private key and return base64url bytes. */
function signDevicePayload(privateKeyPem, payload) {
const key = crypto.createPrivateKey(privateKeyPem);
return base64UrlEncode(crypto.sign(null, Buffer.from(payload, "utf8"), key));
}
/** Normalize PEM or raw base64/base64url public keys to canonical raw base64url bytes. */
function normalizeDevicePublicKeyBase64Url(publicKey) {
try {
if (publicKey.includes("BEGIN")) return base64UrlEncode(derivePublicKeyRaw(publicKey));
const raw = base64UrlDecode(publicKey);
if (raw.length === 0) return null;
return base64UrlEncode(raw);
} catch {
return null;
}
}
/** Derive the stable device id from PEM or raw base64/base64url public key material. */
function deriveDeviceIdFromPublicKey(publicKey) {
try {
const raw = publicKey.includes("BEGIN") ? derivePublicKeyRaw(publicKey) : base64UrlDecode(publicKey);
if (raw.length === 0) return null;
return crypto.createHash("sha256").update(raw).digest("hex");
} catch {
return null;
}
}
/** Export a PEM Ed25519 public key as canonical raw base64url bytes. */
function publicKeyRawBase64UrlFromPem(publicKeyPem) {
return base64UrlEncode(derivePublicKeyRaw(publicKeyPem));
}
/** Verify a UTF-8 payload signature against PEM or raw base64/base64url public key material. */
function verifyDeviceSignature(publicKey, payload, signatureBase64Url) {
try {
const key = publicKey.includes("BEGIN") ? crypto.createPublicKey(publicKey) : crypto.createPublicKey({
key: Buffer.concat([ED25519_SPKI_PREFIX, base64UrlDecode(publicKey)]),
type: "spki",
format: "der"
});
const sig = (() => {
try {
return base64UrlDecode(signatureBase64Url);
} catch {
return Buffer.from(signatureBase64Url, "base64");
}
})();
return crypto.verify(null, Buffer.from(payload, "utf8"), key, sig);
} catch {
return false;
}
}
//#endregion
export { publicKeyRawBase64UrlFromPem as a, normalizeDevicePublicKeyBase64Url as i, loadDeviceIdentityIfPresent as n, signDevicePayload as o, loadOrCreateDeviceIdentity as r, verifyDeviceSignature as s, deriveDeviceIdFromPublicKey as t };