import { getPublicKeyAsync, signAsync, utils } from "@noble/ed25519" ;
import { getSafeLocalStorage } from "../local-storage.ts" ;
type StoredIdentity = {
version: 1 ;
deviceId: string;
publicKey: string;
privateKey: string;
createdAtMs: number;
};
export type DeviceIdentity = {
deviceId: string;
publicKey: string;
privateKey: string;
};
const STORAGE_KEY = "openclaw-device-identity-v1" ;
function base64UrlEncode(bytes: Uint8Array): string {
let binary = "" ;
for (const byte of bytes) {
binary += String.fromCharCode(byte );
}
return btoa(binary).replaceAll("+" , "-" ).replaceAll("/" , "_" ).replace(/=+$/g, "" );
}
function base64UrlDecode(input: string): Uint8Array {
const normalized = input.replaceAll("-" , "+" ).replaceAll("_" , "/" );
const padded = normalized + "=" .repeat((4 - (normalized.length % 4 )) % 4 );
const binary = atob(padded);
const out = new Uint8Array(binary.length);
for (let i = 0 ; i < binary.length; i += 1 ) {
out[i] = binary.charCodeAt(i);
}
return out;
}
function bytesToHex(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16 ).padStart(2 , "0" ))
.join("" );
}
async function fingerprintPublicKey(publicKey: Uint8Array): Promise<string> {
const hash = await crypto.subtle.digest("SHA-256" , publicKey.slice().buffer);
return bytesToHex(new Uint8Array(hash));
}
async function generateIdentity(): Promise<DeviceIdentity> {
const privateKey = utils.randomSecretKey();
const publicKey = await getPublicKeyAsync(privateKey);
const deviceId = await fingerprintPublicKey(publicKey);
return {
deviceId,
publicKey: base64UrlEncode(publicKey),
privateKey: base64UrlEncode(privateKey),
};
}
export async function loadOrCreateDeviceIdentity(): Promise<DeviceIdentity> {
const storage = getSafeLocalStorage();
try {
const raw = storage?.getItem(STORAGE_KEY);
if (raw) {
const parsed = JSON.parse(raw) as StoredIdentity;
if (
parsed?.version === 1 &&
typeof parsed.deviceId === "string" &&
typeof parsed.publicKey === "string" &&
typeof parsed.privateKey === "string"
) {
const derivedId = await fingerprintPublicKey(base64UrlDecode(parsed.publicKey));
if (derivedId !== parsed.deviceId) {
const updated: StoredIdentity = {
...parsed,
deviceId: derivedId,
};
storage?.setItem(STORAGE_KEY, JSON.stringify(updated));
return {
deviceId: derivedId,
publicKey: parsed.publicKey,
privateKey: parsed.privateKey,
};
}
return {
deviceId: parsed.deviceId,
publicKey: parsed.publicKey,
privateKey: parsed.privateKey,
};
}
}
} catch {
// fall through to regenerate
}
const identity = await generateIdentity();
const stored: StoredIdentity = {
version: 1 ,
deviceId: identity.deviceId,
publicKey: identity.publicKey,
privateKey: identity.privateKey,
createdAtMs: Date.now(),
};
storage?.setItem(STORAGE_KEY, JSON.stringify(stored));
return identity;
}
export async function signDevicePayload(privateKeyBase64Url: string, payload: string) {
const key = base64UrlDecode(privateKeyBase64Url);
const data = new TextEncoder().encode(payload);
const sig = await signAsync(data, key);
return base64UrlEncode(sig);
}
Messung V0.5 in Prozent C=99 H=98 G=98
¤ Dauer der Verarbeitung: 0.16 Sekunden
(vorverarbeitet am 2026-06-07)
¤
*© Formatika GbR, Deutschland