mirror of
https://github.com/moltbot/moltbot.git
synced 2026-03-08 06:54:24 +00:00
refactor(gateway): unify v3 auth payload builders and vectors
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package ai.openclaw.android.gateway
|
||||
|
||||
internal object DeviceAuthPayload {
|
||||
fun buildV3(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: List<String>,
|
||||
signedAtMs: Long,
|
||||
token: String?,
|
||||
nonce: String,
|
||||
platform: String?,
|
||||
deviceFamily: String?,
|
||||
): String {
|
||||
val scopeString = scopes.joinToString(",")
|
||||
val authToken = token.orEmpty()
|
||||
val platformNorm = normalizeMetadataField(platform)
|
||||
val deviceFamilyNorm = normalizeMetadataField(deviceFamily)
|
||||
return listOf(
|
||||
"v3",
|
||||
deviceId,
|
||||
clientId,
|
||||
clientMode,
|
||||
role,
|
||||
scopeString,
|
||||
signedAtMs.toString(),
|
||||
authToken,
|
||||
nonce,
|
||||
platformNorm,
|
||||
deviceFamilyNorm,
|
||||
).joinToString("|")
|
||||
}
|
||||
|
||||
internal fun normalizeMetadataField(value: String?): String {
|
||||
val trimmed = value?.trim().orEmpty()
|
||||
if (trimmed.isEmpty()) {
|
||||
return ""
|
||||
}
|
||||
// Keep cross-runtime normalization deterministic (TS/Swift/Kotlin):
|
||||
// lowercase ASCII A-Z only for auth payload metadata fields.
|
||||
val out = StringBuilder(trimmed.length)
|
||||
for (ch in trimmed) {
|
||||
if (ch in 'A'..'Z') {
|
||||
out.append((ch.code + 32).toChar())
|
||||
} else {
|
||||
out.append(ch)
|
||||
}
|
||||
}
|
||||
return out.toString()
|
||||
}
|
||||
}
|
||||
@@ -372,7 +372,7 @@ class GatewaySession(
|
||||
|
||||
val signedAtMs = System.currentTimeMillis()
|
||||
val payload =
|
||||
buildDeviceAuthPayloadV3(
|
||||
DeviceAuthPayload.buildV3(
|
||||
deviceId = identity.deviceId,
|
||||
clientId = client.id,
|
||||
clientMode = client.mode,
|
||||
@@ -584,42 +584,6 @@ class GatewaySession(
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildDeviceAuthPayloadV3(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: List<String>,
|
||||
signedAtMs: Long,
|
||||
token: String?,
|
||||
nonce: String,
|
||||
platform: String?,
|
||||
deviceFamily: String?,
|
||||
): String {
|
||||
val scopeString = scopes.joinToString(",")
|
||||
val authToken = token.orEmpty()
|
||||
val platformNorm = normalizeDeviceMetadataField(platform)
|
||||
val deviceFamilyNorm = normalizeDeviceMetadataField(deviceFamily)
|
||||
val parts =
|
||||
mutableListOf(
|
||||
"v3",
|
||||
deviceId,
|
||||
clientId,
|
||||
clientMode,
|
||||
role,
|
||||
scopeString,
|
||||
signedAtMs.toString(),
|
||||
authToken,
|
||||
nonce,
|
||||
platformNorm,
|
||||
deviceFamilyNorm,
|
||||
)
|
||||
return parts.joinToString("|")
|
||||
}
|
||||
|
||||
private fun normalizeDeviceMetadataField(value: String?): String =
|
||||
value?.trim()?.lowercase(Locale.ROOT).orEmpty()
|
||||
|
||||
private fun normalizeCanvasHostUrl(
|
||||
raw: String?,
|
||||
endpoint: GatewayEndpoint,
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package ai.openclaw.android.gateway
|
||||
|
||||
import org.junit.Assert.assertEquals
|
||||
import org.junit.Test
|
||||
|
||||
class DeviceAuthPayloadTest {
|
||||
@Test
|
||||
fun buildV3_matchesCanonicalVector() {
|
||||
val payload =
|
||||
DeviceAuthPayload.buildV3(
|
||||
deviceId = "dev-1",
|
||||
clientId = "openclaw-macos",
|
||||
clientMode = "ui",
|
||||
role = "operator",
|
||||
scopes = listOf("operator.admin", "operator.read"),
|
||||
signedAtMs = 1_700_000_000_000,
|
||||
token = "tok-123",
|
||||
nonce = "nonce-abc",
|
||||
platform = " IOS ",
|
||||
deviceFamily = " iPhone ",
|
||||
)
|
||||
|
||||
assertEquals(
|
||||
"v3|dev-1|openclaw-macos|ui|operator|operator.admin,operator.read|1700000000000|tok-123|nonce-abc|ios|iphone",
|
||||
payload,
|
||||
)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun normalizeMetadataField_asciiOnlyLowercase() {
|
||||
assertEquals("İos", DeviceAuthPayload.normalizeMetadataField(" İOS "))
|
||||
assertEquals("mac", DeviceAuthPayload.normalizeMetadataField(" MAC "))
|
||||
assertEquals("", DeviceAuthPayload.normalizeMetadataField(null))
|
||||
}
|
||||
}
|
||||
@@ -280,7 +280,7 @@ actor GatewayWizardClient {
|
||||
let connectNonce = try await self.waitForConnectChallenge()
|
||||
let identity = DeviceIdentityStore.loadOrCreate()
|
||||
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
|
||||
let payload = buildDeviceAuthPayloadV3(
|
||||
let payload = GatewayDeviceAuthPayload.buildV3(
|
||||
deviceId: identity.deviceId,
|
||||
clientId: clientId,
|
||||
clientMode: clientMode,
|
||||
@@ -327,44 +327,6 @@ actor GatewayWizardClient {
|
||||
}
|
||||
}
|
||||
|
||||
private func buildDeviceAuthPayloadV3(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: [String],
|
||||
signedAtMs: Int,
|
||||
token: String?,
|
||||
nonce: String,
|
||||
platform: String?,
|
||||
deviceFamily: String?) -> String
|
||||
{
|
||||
let scopeString = scopes.joined(separator: ",")
|
||||
let authToken = token ?? ""
|
||||
let normalizedPlatform = normalizeMetadataField(platform)
|
||||
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
|
||||
return [
|
||||
"v3",
|
||||
deviceId,
|
||||
clientId,
|
||||
clientMode,
|
||||
role,
|
||||
scopeString,
|
||||
String(signedAtMs),
|
||||
authToken,
|
||||
nonce,
|
||||
normalizedPlatform,
|
||||
normalizedDeviceFamily,
|
||||
].joined(separator: "|")
|
||||
}
|
||||
|
||||
private func normalizeMetadataField(_ value: String?) -> String {
|
||||
guard let value else { return "" }
|
||||
return value
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased(with: Locale(identifier: "en_US_POSIX"))
|
||||
}
|
||||
|
||||
private func waitForConnectChallenge() async throws -> String {
|
||||
guard let task = self.task else { throw ConnectChallengeError.timeout }
|
||||
return try await AsyncTimeout.withTimeout(
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import Foundation
|
||||
|
||||
public enum GatewayDeviceAuthPayload {
|
||||
public static func buildV3(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: [String],
|
||||
signedAtMs: Int,
|
||||
token: String?,
|
||||
nonce: String,
|
||||
platform: String?,
|
||||
deviceFamily: String?) -> String
|
||||
{
|
||||
let scopeString = scopes.joined(separator: ",")
|
||||
let authToken = token ?? ""
|
||||
let normalizedPlatform = normalizeMetadataField(platform)
|
||||
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
|
||||
return [
|
||||
"v3",
|
||||
deviceId,
|
||||
clientId,
|
||||
clientMode,
|
||||
role,
|
||||
scopeString,
|
||||
String(signedAtMs),
|
||||
authToken,
|
||||
nonce,
|
||||
normalizedPlatform,
|
||||
normalizedDeviceFamily,
|
||||
].joined(separator: "|")
|
||||
}
|
||||
|
||||
static func normalizeMetadataField(_ value: String?) -> String {
|
||||
guard let value else { return "" }
|
||||
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
if trimmed.isEmpty {
|
||||
return ""
|
||||
}
|
||||
// Keep cross-runtime normalization deterministic (TS/Swift/Kotlin):
|
||||
// lowercase ASCII A-Z only for auth payload metadata fields.
|
||||
var output = String()
|
||||
output.reserveCapacity(trimmed.count)
|
||||
for scalar in trimmed.unicodeScalars {
|
||||
let codePoint = scalar.value
|
||||
if codePoint >= 65, codePoint <= 90, let lowered = UnicodeScalar(codePoint + 32) {
|
||||
output.unicodeScalars.append(lowered)
|
||||
} else {
|
||||
output.unicodeScalars.append(scalar)
|
||||
}
|
||||
}
|
||||
return output
|
||||
}
|
||||
}
|
||||
@@ -399,7 +399,7 @@ public actor GatewayChannelActor {
|
||||
let signedAtMs = Int(Date().timeIntervalSince1970 * 1000)
|
||||
let connectNonce = try await self.waitForConnectChallenge()
|
||||
if includeDeviceIdentity, let identity {
|
||||
let payload = buildDeviceAuthPayloadV3(
|
||||
let payload = GatewayDeviceAuthPayload.buildV3(
|
||||
deviceId: identity.deviceId,
|
||||
clientId: clientId,
|
||||
clientMode: clientMode,
|
||||
@@ -443,44 +443,6 @@ public actor GatewayChannelActor {
|
||||
}
|
||||
}
|
||||
|
||||
private func buildDeviceAuthPayloadV3(
|
||||
deviceId: String,
|
||||
clientId: String,
|
||||
clientMode: String,
|
||||
role: String,
|
||||
scopes: [String],
|
||||
signedAtMs: Int,
|
||||
token: String?,
|
||||
nonce: String,
|
||||
platform: String?,
|
||||
deviceFamily: String?) -> String
|
||||
{
|
||||
let scopeString = scopes.joined(separator: ",")
|
||||
let authToken = token ?? ""
|
||||
let normalizedPlatform = normalizeMetadataField(platform)
|
||||
let normalizedDeviceFamily = normalizeMetadataField(deviceFamily)
|
||||
return [
|
||||
"v3",
|
||||
deviceId,
|
||||
clientId,
|
||||
clientMode,
|
||||
role,
|
||||
scopeString,
|
||||
String(signedAtMs),
|
||||
authToken,
|
||||
nonce,
|
||||
normalizedPlatform,
|
||||
normalizedDeviceFamily,
|
||||
].joined(separator: "|")
|
||||
}
|
||||
|
||||
private func normalizeMetadataField(_ value: String?) -> String {
|
||||
guard let value else { return "" }
|
||||
return value
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
.lowercased(with: Locale(identifier: "en_US_POSIX"))
|
||||
}
|
||||
|
||||
private func handleConnectResponse(
|
||||
_ res: ResponseFrame,
|
||||
identity: DeviceIdentity?,
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import Testing
|
||||
@testable import OpenClawKit
|
||||
|
||||
@Suite("DeviceAuthPayload")
|
||||
struct DeviceAuthPayloadTests {
|
||||
@Test("builds canonical v3 payload vector")
|
||||
func buildsCanonicalV3PayloadVector() {
|
||||
let payload = GatewayDeviceAuthPayload.buildV3(
|
||||
deviceId: "dev-1",
|
||||
clientId: "openclaw-macos",
|
||||
clientMode: "ui",
|
||||
role: "operator",
|
||||
scopes: ["operator.admin", "operator.read"],
|
||||
signedAtMs: 1_700_000_000_000,
|
||||
token: "tok-123",
|
||||
nonce: "nonce-abc",
|
||||
platform: " IOS ",
|
||||
deviceFamily: " iPhone ")
|
||||
#expect(
|
||||
payload
|
||||
== "v3|dev-1|openclaw-macos|ui|operator|operator.admin,operator.read|1700000000000|tok-123|nonce-abc|ios|iphone")
|
||||
}
|
||||
|
||||
@Test("normalizes metadata with ASCII-only lowercase")
|
||||
func normalizesMetadataWithAsciiLowercase() {
|
||||
#expect(GatewayDeviceAuthPayload.normalizeMetadataField(" İOS ") == "İos")
|
||||
#expect(GatewayDeviceAuthPayload.normalizeMetadataField(" MAC ") == "mac")
|
||||
#expect(GatewayDeviceAuthPayload.normalizeMetadataField(nil) == "")
|
||||
}
|
||||
}
|
||||
29
src/gateway/device-auth.test.ts
Normal file
29
src/gateway/device-auth.test.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildDeviceAuthPayloadV3, normalizeDeviceMetadataForAuth } from "./device-auth.js";
|
||||
|
||||
describe("device-auth payload vectors", () => {
|
||||
it("builds canonical v3 payload", () => {
|
||||
const payload = buildDeviceAuthPayloadV3({
|
||||
deviceId: "dev-1",
|
||||
clientId: "openclaw-macos",
|
||||
clientMode: "ui",
|
||||
role: "operator",
|
||||
scopes: ["operator.admin", "operator.read"],
|
||||
signedAtMs: 1_700_000_000_000,
|
||||
token: "tok-123",
|
||||
nonce: "nonce-abc",
|
||||
platform: " IOS ",
|
||||
deviceFamily: " iPhone ",
|
||||
});
|
||||
|
||||
expect(payload).toBe(
|
||||
"v3|dev-1|openclaw-macos|ui|operator|operator.admin,operator.read|1700000000000|tok-123|nonce-abc|ios|iphone",
|
||||
);
|
||||
});
|
||||
|
||||
it("normalizes metadata with ASCII-only lowercase", () => {
|
||||
expect(normalizeDeviceMetadataForAuth(" İOS ")).toBe("İos");
|
||||
expect(normalizeDeviceMetadataForAuth(" MAC ")).toBe("mac");
|
||||
expect(normalizeDeviceMetadataForAuth(undefined)).toBe("");
|
||||
});
|
||||
});
|
||||
@@ -14,11 +14,21 @@ export type DeviceAuthPayloadV3Params = DeviceAuthPayloadParams & {
|
||||
deviceFamily?: string | null;
|
||||
};
|
||||
|
||||
function normalizeMetadataField(value?: string | null): string {
|
||||
function toLowerAscii(input: string): string {
|
||||
return input.replace(/[A-Z]/g, (char) => String.fromCharCode(char.charCodeAt(0) + 32));
|
||||
}
|
||||
|
||||
export function normalizeDeviceMetadataForAuth(value?: string | null): string {
|
||||
if (typeof value !== "string") {
|
||||
return "";
|
||||
}
|
||||
return value.trim().toLowerCase();
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) {
|
||||
return "";
|
||||
}
|
||||
// Keep cross-runtime normalization deterministic (TS/Swift/Kotlin) by only
|
||||
// lowercasing ASCII metadata fields used in auth payloads.
|
||||
return toLowerAscii(trimmed);
|
||||
}
|
||||
|
||||
export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string {
|
||||
@@ -40,8 +50,8 @@ export function buildDeviceAuthPayload(params: DeviceAuthPayloadParams): string
|
||||
export function buildDeviceAuthPayloadV3(params: DeviceAuthPayloadV3Params): string {
|
||||
const scopes = params.scopes.join(",");
|
||||
const token = params.token ?? "";
|
||||
const platform = normalizeMetadataField(params.platform);
|
||||
const deviceFamily = normalizeMetadataField(params.deviceFamily);
|
||||
const platform = normalizeDeviceMetadataForAuth(params.platform);
|
||||
const deviceFamily = normalizeDeviceMetadataForAuth(params.deviceFamily);
|
||||
return [
|
||||
"v3",
|
||||
params.deviceId,
|
||||
|
||||
@@ -32,7 +32,11 @@ import {
|
||||
CANVAS_CAPABILITY_TTL_MS,
|
||||
mintCanvasCapabilityToken,
|
||||
} from "../../canvas-capability.js";
|
||||
import { buildDeviceAuthPayload, buildDeviceAuthPayloadV3 } from "../../device-auth.js";
|
||||
import {
|
||||
buildDeviceAuthPayload,
|
||||
buildDeviceAuthPayloadV3,
|
||||
normalizeDeviceMetadataForAuth,
|
||||
} from "../../device-auth.js";
|
||||
import {
|
||||
isLocalishHost,
|
||||
isLoopbackAddress,
|
||||
@@ -131,8 +135,75 @@ function shouldAllowSilentLocalPairing(params: {
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeClientMetadataForComparison(value: string | undefined): string {
|
||||
return typeof value === "string" ? value.trim().toLowerCase() : "";
|
||||
function resolveDeviceSignaturePayloadVersion(params: {
|
||||
device: {
|
||||
id: string;
|
||||
signature: string;
|
||||
publicKey: string;
|
||||
};
|
||||
connectParams: ConnectParams;
|
||||
role: string;
|
||||
scopes: string[];
|
||||
signedAtMs: number;
|
||||
nonce: string;
|
||||
}): "v3" | "v2" | null {
|
||||
const payloadV3 = buildDeviceAuthPayloadV3({
|
||||
deviceId: params.device.id,
|
||||
clientId: params.connectParams.client.id,
|
||||
clientMode: params.connectParams.client.mode,
|
||||
role: params.role,
|
||||
scopes: params.scopes,
|
||||
signedAtMs: params.signedAtMs,
|
||||
token: params.connectParams.auth?.token ?? params.connectParams.auth?.deviceToken ?? null,
|
||||
nonce: params.nonce,
|
||||
platform: params.connectParams.client.platform,
|
||||
deviceFamily: params.connectParams.client.deviceFamily,
|
||||
});
|
||||
if (verifyDeviceSignature(params.device.publicKey, payloadV3, params.device.signature)) {
|
||||
return "v3";
|
||||
}
|
||||
|
||||
const payloadV2 = buildDeviceAuthPayload({
|
||||
deviceId: params.device.id,
|
||||
clientId: params.connectParams.client.id,
|
||||
clientMode: params.connectParams.client.mode,
|
||||
role: params.role,
|
||||
scopes: params.scopes,
|
||||
signedAtMs: params.signedAtMs,
|
||||
token: params.connectParams.auth?.token ?? params.connectParams.auth?.deviceToken ?? null,
|
||||
nonce: params.nonce,
|
||||
});
|
||||
if (verifyDeviceSignature(params.device.publicKey, payloadV2, params.device.signature)) {
|
||||
return "v2";
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function resolvePinnedClientMetadata(params: {
|
||||
claimedPlatform?: string;
|
||||
claimedDeviceFamily?: string;
|
||||
pairedPlatform?: string;
|
||||
pairedDeviceFamily?: string;
|
||||
}): {
|
||||
platformMismatch: boolean;
|
||||
deviceFamilyMismatch: boolean;
|
||||
pinnedPlatform?: string;
|
||||
pinnedDeviceFamily?: string;
|
||||
} {
|
||||
const claimedPlatform = normalizeDeviceMetadataForAuth(params.claimedPlatform);
|
||||
const claimedDeviceFamily = normalizeDeviceMetadataForAuth(params.claimedDeviceFamily);
|
||||
const pairedPlatform = normalizeDeviceMetadataForAuth(params.pairedPlatform);
|
||||
const pairedDeviceFamily = normalizeDeviceMetadataForAuth(params.pairedDeviceFamily);
|
||||
const hasPinnedPlatform = pairedPlatform !== "";
|
||||
const hasPinnedDeviceFamily = pairedDeviceFamily !== "";
|
||||
const platformMismatch = hasPinnedPlatform && claimedPlatform !== pairedPlatform;
|
||||
const deviceFamilyMismatch = hasPinnedDeviceFamily && claimedDeviceFamily !== pairedDeviceFamily;
|
||||
return {
|
||||
platformMismatch,
|
||||
deviceFamilyMismatch,
|
||||
pinnedPlatform: hasPinnedPlatform ? params.pairedPlatform : undefined,
|
||||
pinnedDeviceFamily: hasPinnedDeviceFamily ? params.pairedDeviceFamily : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
export function attachGatewayWsMessageHandler(params: {
|
||||
@@ -588,42 +659,21 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
rejectDeviceAuthInvalid("device-nonce-mismatch", "device nonce mismatch");
|
||||
return;
|
||||
}
|
||||
const payloadV3 = buildDeviceAuthPayloadV3({
|
||||
deviceId: device.id,
|
||||
clientId: connectParams.client.id,
|
||||
clientMode: connectParams.client.mode,
|
||||
role,
|
||||
scopes,
|
||||
signedAtMs: signedAt,
|
||||
token: connectParams.auth?.token ?? connectParams.auth?.deviceToken ?? null,
|
||||
nonce: providedNonce,
|
||||
platform: connectParams.client.platform,
|
||||
deviceFamily: connectParams.client.deviceFamily,
|
||||
});
|
||||
const payloadV2 = buildDeviceAuthPayload({
|
||||
deviceId: device.id,
|
||||
clientId: connectParams.client.id,
|
||||
clientMode: connectParams.client.mode,
|
||||
role,
|
||||
scopes,
|
||||
signedAtMs: signedAt,
|
||||
token: connectParams.auth?.token ?? connectParams.auth?.deviceToken ?? null,
|
||||
nonce: providedNonce,
|
||||
});
|
||||
const rejectDeviceSignatureInvalid = () =>
|
||||
rejectDeviceAuthInvalid("device-signature", "device signature invalid");
|
||||
const signatureOkV3 = verifyDeviceSignature(
|
||||
device.publicKey,
|
||||
payloadV3,
|
||||
device.signature,
|
||||
);
|
||||
const signatureOkV2 =
|
||||
!signatureOkV3 && verifyDeviceSignature(device.publicKey, payloadV2, device.signature);
|
||||
if (!signatureOkV3 && !signatureOkV2) {
|
||||
const payloadVersion = resolveDeviceSignaturePayloadVersion({
|
||||
device,
|
||||
connectParams,
|
||||
role,
|
||||
scopes,
|
||||
signedAtMs: signedAt,
|
||||
nonce: providedNonce,
|
||||
});
|
||||
if (!payloadVersion) {
|
||||
rejectDeviceSignatureInvalid();
|
||||
return;
|
||||
}
|
||||
deviceAuthPayloadVersion = signatureOkV3 ? "v3" : "v2";
|
||||
deviceAuthPayloadVersion = payloadVersion;
|
||||
devicePublicKey = normalizeDevicePublicKeyBase64Url(device.publicKey);
|
||||
if (!devicePublicKey) {
|
||||
rejectDeviceAuthInvalid("device-public-key", "device public key invalid");
|
||||
@@ -784,17 +834,13 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
const pairedPlatform = paired.platform;
|
||||
const claimedDeviceFamily = connectParams.client.deviceFamily;
|
||||
const pairedDeviceFamily = paired.deviceFamily;
|
||||
const hasPinnedPlatform = normalizeClientMetadataForComparison(pairedPlatform) !== "";
|
||||
const hasPinnedDeviceFamily =
|
||||
normalizeClientMetadataForComparison(pairedDeviceFamily) !== "";
|
||||
const platformMismatch =
|
||||
hasPinnedPlatform &&
|
||||
normalizeClientMetadataForComparison(claimedPlatform) !==
|
||||
normalizeClientMetadataForComparison(pairedPlatform);
|
||||
const deviceFamilyMismatch =
|
||||
hasPinnedDeviceFamily &&
|
||||
normalizeClientMetadataForComparison(claimedDeviceFamily) !==
|
||||
normalizeClientMetadataForComparison(pairedDeviceFamily);
|
||||
const metadataPinning = resolvePinnedClientMetadata({
|
||||
claimedPlatform,
|
||||
claimedDeviceFamily,
|
||||
pairedPlatform,
|
||||
pairedDeviceFamily,
|
||||
});
|
||||
const { platformMismatch, deviceFamilyMismatch } = metadataPinning;
|
||||
if (platformMismatch || deviceFamilyMismatch) {
|
||||
logGateway.warn(
|
||||
`security audit: device metadata upgrade requested reason=metadata-upgrade device=${device.id} ip=${reportedClientIp ?? "unknown-ip"} auth=${authMethod} payload=${deviceAuthPayloadVersion ?? "unknown"} claimedPlatform=${claimedPlatform ?? "<none>"} pinnedPlatform=${pairedPlatform ?? "<none>"} claimedDeviceFamily=${claimedDeviceFamily ?? "<none>"} pinnedDeviceFamily=${pairedDeviceFamily ?? "<none>"} client=${connectParams.client.id} conn=${connId}`,
|
||||
@@ -804,11 +850,11 @@ export function attachGatewayWsMessageHandler(params: {
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
if (hasPinnedPlatform && pairedPlatform) {
|
||||
connectParams.client.platform = pairedPlatform;
|
||||
if (metadataPinning.pinnedPlatform) {
|
||||
connectParams.client.platform = metadataPinning.pinnedPlatform;
|
||||
}
|
||||
if (hasPinnedDeviceFamily) {
|
||||
connectParams.client.deviceFamily = pairedDeviceFamily;
|
||||
if (metadataPinning.pinnedDeviceFamily) {
|
||||
connectParams.client.deviceFamily = metadataPinning.pinnedDeviceFamily;
|
||||
}
|
||||
}
|
||||
const pairedRoles = Array.isArray(paired.roles)
|
||||
|
||||
Reference in New Issue
Block a user