Validate session snapshots and wipe keys on logout (closes #10)
check / check (push) Successful in 41s
check / check (push) Successful in 41s
Client.fromJSON checks every snapshot field and each key's decoded length and throws an error naming the bad field. toJSON reads the token through a new ApiClient.getAuthToken and throws when there is none. logout zeroes the key buffers in place; collectionsSince re-checks for logout after its request so it never decrypts with zeroed keys. The CLI now reports a corrupt session file separately from a missing one. Model: opus-5-5
This commit was merged in pull request #79.
This commit is contained in:
+62
-11
@@ -125,18 +125,57 @@ export class Client {
|
||||
);
|
||||
}
|
||||
|
||||
static fromJSON(
|
||||
snapshot: ClientSnapshot,
|
||||
apiOptions?: ApiClientOptions,
|
||||
): Client {
|
||||
const api = new ApiClient({ ...apiOptions, authToken: snapshot.token });
|
||||
// Restore a client from a `toJSON()` snapshot. The snapshot usually comes
|
||||
// straight from `JSON.parse` of a file on disk, so every field is checked
|
||||
// before use; a bad one throws an error naming it. Needs `init()` first.
|
||||
static fromJSON(snapshot: unknown, apiOptions?: ApiClientOptions): Client {
|
||||
const invalid = (field: string, problem: string): Error =>
|
||||
new Error(`Invalid session data: ${field} ${problem}`);
|
||||
|
||||
if (typeof snapshot !== "object" || snapshot === null) {
|
||||
throw new Error("Invalid session data: not a JSON object");
|
||||
}
|
||||
const s = snapshot as Record<string, unknown>;
|
||||
for (const field of ["email", "token"]) {
|
||||
if (typeof s[field] !== "string" || s[field] === "") {
|
||||
throw invalid(field, "must be a non-empty string");
|
||||
}
|
||||
}
|
||||
if (!Number.isInteger(s.userID)) {
|
||||
throw invalid("userID", "must be an integer");
|
||||
}
|
||||
const key = (field: string): Uint8Array => {
|
||||
const value = s[field];
|
||||
if (typeof value !== "string") {
|
||||
throw invalid(field, "must be a base64 string");
|
||||
}
|
||||
let bytes: Uint8Array;
|
||||
try {
|
||||
bytes = fromBase64(value);
|
||||
} catch {
|
||||
throw invalid(field, "is not valid base64");
|
||||
}
|
||||
// The master key (secretbox) and the key pair (box) are all 32 bytes.
|
||||
if (bytes.length !== 32) {
|
||||
throw invalid(
|
||||
field,
|
||||
`must decode to 32 bytes, got ${bytes.length}`,
|
||||
);
|
||||
}
|
||||
return bytes;
|
||||
};
|
||||
|
||||
const api = new ApiClient({
|
||||
...apiOptions,
|
||||
authToken: s.token as string,
|
||||
});
|
||||
return new Client(
|
||||
api,
|
||||
snapshot.email,
|
||||
snapshot.userID,
|
||||
fromBase64(snapshot.masterKey),
|
||||
fromBase64(snapshot.secretKey),
|
||||
fromBase64(snapshot.publicKey),
|
||||
s.email as string,
|
||||
s.userID as number,
|
||||
key("masterKey"),
|
||||
key("secretKey"),
|
||||
key("publicKey"),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -164,19 +203,29 @@ export class Client {
|
||||
|
||||
toJSON(): ClientSnapshot {
|
||||
this.assertLoggedIn();
|
||||
const token = this.api.getAuthToken();
|
||||
if (!token) {
|
||||
throw new Error("Cannot serialize client: it has no auth token");
|
||||
}
|
||||
return {
|
||||
email: this.email,
|
||||
userID: this.userID,
|
||||
token: this.api["token"]!,
|
||||
token,
|
||||
masterKey: toBase64(this.masterKey),
|
||||
secretKey: toBase64(this.secretKey),
|
||||
publicKey: toBase64(this.publicKey),
|
||||
};
|
||||
}
|
||||
|
||||
// Zeroes the key buffers in place, so any copy of the reference held
|
||||
// elsewhere is wiped too. Every method checks `assertLoggedIn` before
|
||||
// touching the keys, so nothing decrypts with the zeroed keys.
|
||||
logout(): void {
|
||||
this.loggedOut = true;
|
||||
this.api.clearAuthToken();
|
||||
this.masterKey.fill(0);
|
||||
this.secretKey.fill(0);
|
||||
this.publicKey.fill(0);
|
||||
}
|
||||
|
||||
// Enumerate collections changed since `sinceTime`. Live collections are
|
||||
@@ -192,6 +241,8 @@ export class Client {
|
||||
const { collections: raws } = await this.api.getJSON<{
|
||||
collections: RawCollection[];
|
||||
}>("/collections/v2", { sinceTime: args.sinceTime });
|
||||
// logout() may have zeroed the keys while the request was in flight.
|
||||
this.assertLoggedIn();
|
||||
|
||||
const collections: Collection[] = [];
|
||||
const deleted: number[] = [];
|
||||
|
||||
Reference in New Issue
Block a user