Matrix: harden device-scoped storage reuse

This commit is contained in:
Gustavo Madeira Santana
2026-03-13 17:25:14 +00:00
parent 36b10d71ca
commit 23f4a33880
2 changed files with 95 additions and 0 deletions

View File

@@ -283,6 +283,45 @@ describe("matrix client storage paths", () => {
expect(rotatedStoragePaths.storagePath).toBe(oldStoragePaths.storagePath);
});
it("reuses an existing token-hash storage root for the same device after the access token changes", () => {
const stateDir = setupStateDir();
const oldStoragePaths = resolveMatrixStoragePaths({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "secret-token-old",
deviceId: "DEVICE123",
env: {},
});
fs.mkdirSync(oldStoragePaths.rootDir, { recursive: true });
fs.writeFileSync(oldStoragePaths.storagePath, '{"legacy":true}');
fs.writeFileSync(
path.join(oldStoragePaths.rootDir, "storage-meta.json"),
JSON.stringify(
{
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accountId: "default",
accessTokenHash: oldStoragePaths.tokenHash,
deviceId: "DEVICE123",
},
null,
2,
),
);
const rotatedStoragePaths = resolveMatrixStoragePaths({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "secret-token-new",
deviceId: "DEVICE123",
env: {},
});
expect(rotatedStoragePaths.rootDir).toBe(oldStoragePaths.rootDir);
expect(rotatedStoragePaths.tokenHash).toBe(oldStoragePaths.tokenHash);
expect(rotatedStoragePaths.storagePath).toBe(oldStoragePaths.storagePath);
});
it("prefers a populated older token-hash storage root over a newer empty root", () => {
const stateDir = setupStateDir();
const oldStoragePaths = resolveMatrixStoragePaths({
@@ -366,4 +405,49 @@ describe("matrix client storage paths", () => {
expect(resolvedPaths.rootDir).toBe(newerCanonicalPaths.rootDir);
expect(resolvedPaths.tokenHash).toBe(newerCanonicalPaths.tokenHash);
});
it("does not reuse a populated sibling storage root with ambiguous device metadata", () => {
const stateDir = setupStateDir();
const oldStoragePaths = resolveMatrixStoragePaths({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "secret-token-old",
env: {},
});
fs.mkdirSync(oldStoragePaths.rootDir, { recursive: true });
fs.writeFileSync(oldStoragePaths.storagePath, '{"legacy":true}');
const newerCanonicalPaths = resolveMatrixAccountStorageRoot({
stateDir,
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "secret-token-new",
});
fs.mkdirSync(newerCanonicalPaths.rootDir, { recursive: true });
fs.writeFileSync(
path.join(newerCanonicalPaths.rootDir, "storage-meta.json"),
JSON.stringify(
{
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accountId: "default",
accessTokenHash: newerCanonicalPaths.tokenHash,
deviceId: "NEWDEVICE",
},
null,
2,
),
);
const resolvedPaths = resolveMatrixStoragePaths({
homeserver: "https://matrix.example.org",
userId: "@bot:example.org",
accessToken: "secret-token-new",
deviceId: "NEWDEVICE",
env: {},
});
expect(resolvedPaths.rootDir).toBe(newerCanonicalPaths.rootDir);
expect(resolvedPaths.tokenHash).toBe(newerCanonicalPaths.tokenHash);
});
});

View File

@@ -143,6 +143,7 @@ function isCompatibleStorageRoot(params: {
userId: string;
accountKey: string;
deviceId?: string | null;
requireExplicitDeviceMatch?: boolean;
}): boolean {
const metadata = readStoredRootMetadata(params.candidateRootDir);
if (metadata.homeserver && metadata.homeserver !== params.homeserver) {
@@ -165,6 +166,13 @@ function isCompatibleStorageRoot(params: {
) {
return false;
}
if (
params.requireExplicitDeviceMatch &&
params.deviceId &&
(!metadata.deviceId || metadata.deviceId.trim() !== params.deviceId.trim())
) {
return false;
}
return true;
}
@@ -213,6 +221,9 @@ function resolvePreferredMatrixStorageRoot(params: {
userId: params.userId,
accountKey: params.accountKey,
deviceId: params.deviceId,
// Once auth resolves a concrete device, only sibling roots that explicitly
// declare that same device are safe to reuse across token rotations.
requireExplicitDeviceMatch: Boolean(params.deviceId),
})
) {
continue;