mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 12:10:00 +00:00
fix(auth): harden sqlite device credentials
This commit is contained in:
@@ -78,7 +78,7 @@ describe("infra/device-auth-store", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("stores one device token without deleting other devices", async () => {
|
||||
it("drops tokens from previous devices when storing a replacement device token", async () => {
|
||||
await withTempDir("openclaw-device-auth-", async (stateDir) => {
|
||||
const env = createEnv(stateDir);
|
||||
|
||||
@@ -98,9 +98,7 @@ describe("infra/device-auth-store", () => {
|
||||
expect(loadDeviceAuthToken({ deviceId: "device-1", role: "operator", env })).toMatchObject({
|
||||
token: "device-1-token",
|
||||
});
|
||||
expect(loadDeviceAuthToken({ deviceId: "device-2", role: "operator", env })).toMatchObject({
|
||||
token: "device-2-token",
|
||||
});
|
||||
expect(loadDeviceAuthToken({ deviceId: "device-2", role: "operator", env })).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -211,6 +211,10 @@ export function storeDeviceAuthToken(params: {
|
||||
const row = deviceAuthEntryToRow(params.deviceId, entry);
|
||||
runOpenClawStateWriteTransaction((database) => {
|
||||
const db = getNodeSqliteKysely<DeviceAuthDatabase>(database.db);
|
||||
executeSqliteQuerySync(
|
||||
database.db,
|
||||
db.deleteFrom("device_auth_tokens").where("device_id", "!=", params.deviceId),
|
||||
);
|
||||
upsertDeviceAuthTokenRow(db, database.db, row);
|
||||
}, sqliteOptions(params.env));
|
||||
return entry;
|
||||
|
||||
@@ -4,6 +4,7 @@ import { describe, expect, it } from "vitest";
|
||||
import { withTempDir } from "../test-utils/temp-dir.js";
|
||||
import {
|
||||
DeviceIdentityMigrationRequiredError,
|
||||
DeviceIdentityStorageError,
|
||||
deriveDeviceIdFromPublicKey,
|
||||
loadDeviceIdentityIfPresent,
|
||||
loadOrCreateDeviceIdentity,
|
||||
@@ -96,6 +97,27 @@ describe("device identity crypto helpers", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects SQLite identities with mismatched private key material", async () => {
|
||||
await withTempDir("openclaw-device-identity-mismatched-key-", async (dir) => {
|
||||
const store = { env: { ...process.env, OPENCLAW_STATE_DIR: dir }, key: "mismatched-key" };
|
||||
const original = loadOrCreateDeviceIdentity(store);
|
||||
const other = loadOrCreateDeviceIdentity({ ...store, key: "other-key" });
|
||||
writeStoredDeviceIdentitySnapshot(
|
||||
{
|
||||
version: 1,
|
||||
deviceId: original.deviceId,
|
||||
publicKeyPem: original.publicKeyPem,
|
||||
privateKeyPem: other.privateKeyPem,
|
||||
createdAtMs: Date.now(),
|
||||
},
|
||||
store,
|
||||
);
|
||||
|
||||
expect(() => loadOrCreateDeviceIdentity(store)).toThrow(DeviceIdentityStorageError);
|
||||
expect(loadDeviceIdentityIfPresent(store)).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
it("derives the same canonical raw key and device id from pem and encoded public keys", async () => {
|
||||
await withIdentity((identity) => {
|
||||
const publicKeyRaw = publicKeyRawBase64UrlFromPem(identity.publicKeyPem);
|
||||
|
||||
@@ -151,7 +151,7 @@ function storedIdentityToRow(
|
||||
|
||||
function deriveStoredDeviceIdOrThrow(stored: StoredDeviceIdentity): string {
|
||||
const derivedId = deriveDeviceIdFromPublicKey(stored.publicKeyPem);
|
||||
if (!derivedId) {
|
||||
if (!derivedId || !storedPrivateKeyMatchesPublicKey(stored.publicKeyPem, stored.privateKeyPem)) {
|
||||
throw new DeviceIdentityStorageError(
|
||||
'Stored device identity is invalid. Run "openclaw doctor --fix" before starting the gateway or connecting this client.',
|
||||
);
|
||||
@@ -159,6 +159,15 @@ function deriveStoredDeviceIdOrThrow(stored: StoredDeviceIdentity): string {
|
||||
return derivedId;
|
||||
}
|
||||
|
||||
function storedPrivateKeyMatchesPublicKey(publicKeyPem: string, privateKeyPem: string): boolean {
|
||||
const payload = "openclaw-device-identity-self-check";
|
||||
try {
|
||||
return verifyDeviceSignature(publicKeyPem, payload, signDevicePayload(privateKeyPem, payload));
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function readStoredIdentity(options?: DeviceIdentityStoreOptions): StoredDeviceIdentity | null {
|
||||
const store = normalizeIdentityStoreOptions(options);
|
||||
const database = openOpenClawStateDatabase({ env: store.env });
|
||||
@@ -274,6 +283,9 @@ export function loadDeviceIdentityIfPresent(
|
||||
if (!derivedId || derivedId !== parsed.deviceId) {
|
||||
return null;
|
||||
}
|
||||
if (!storedPrivateKeyMatchesPublicKey(parsed.publicKeyPem, parsed.privateKeyPem)) {
|
||||
return null;
|
||||
}
|
||||
return {
|
||||
deviceId: parsed.deviceId,
|
||||
publicKeyPem: parsed.publicKeyPem,
|
||||
|
||||
Reference in New Issue
Block a user