Merge: fix auth token encoding (URL-safe base64 with padding)
Live-tested against real Ente API. Login, key unwrap, and authenticated collection listing all succeed.
This commit is contained in:
@@ -3,7 +3,7 @@ import {
|
||||
decryptSealed,
|
||||
deriveKEK,
|
||||
fromBase64,
|
||||
toBase64URL,
|
||||
toBase64URLPadded,
|
||||
} from "../crypto/index.js";
|
||||
import type { AuthorizationResponse } from "./types.js";
|
||||
|
||||
@@ -58,7 +58,10 @@ export const unwrapAuth = async (
|
||||
publicKey,
|
||||
secretKey,
|
||||
);
|
||||
const token = toBase64URL(tokenBytes);
|
||||
// The Ente server validates the token as URL-safe base64 WITH padding,
|
||||
// matching Go's base64.URLEncoding. Using the no-padding form results
|
||||
// in HTTP 401 "invalid token".
|
||||
const token = toBase64URLPadded(tokenBytes);
|
||||
|
||||
return { masterKey, secretKey, publicKey, token };
|
||||
};
|
||||
|
||||
@@ -6,6 +6,12 @@ export const toBase64 = (b: Uint8Array): string =>
|
||||
export const toBase64URL = (b: Uint8Array): string =>
|
||||
sodium.to_base64(b, sodium.base64_variants.URLSAFE_NO_PADDING);
|
||||
|
||||
// URL-safe base64 WITH padding. Go's base64.URLEncoding produces this form,
|
||||
// and the Ente server validates the auth token in this format. Use toBase64URL
|
||||
// for general use (JWT-style, no padding); use this for the auth token.
|
||||
export const toBase64URLPadded = (b: Uint8Array): string =>
|
||||
sodium.to_base64(b, sodium.base64_variants.URLSAFE);
|
||||
|
||||
// Ente uses standard base64 for most fields, URL-safe (with padding stripped)
|
||||
// for the auth token. Rather than make callers specify, fromBase64 accepts
|
||||
// any of the four variants libsodium understands and returns the bytes.
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
export { init } from "./sodium.js";
|
||||
export { fromBase64, toBase64, toBase64URL } from "./encoding.js";
|
||||
export {
|
||||
fromBase64,
|
||||
toBase64,
|
||||
toBase64URL,
|
||||
toBase64URLPadded,
|
||||
} from "./encoding.js";
|
||||
export { deriveKEK, deriveLoginSubkey } from "./kdf.js";
|
||||
export { decryptBox, decryptSealed } from "./box.js";
|
||||
export {
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
* ▼ sealed_box_open(encryptedToken, publicKey, secretKey)
|
||||
* tokenBytes
|
||||
* │
|
||||
* ▼ toBase64URL
|
||||
* ▼ toBase64URLPadded
|
||||
* token ───── X-Auth-Token header value
|
||||
*
|
||||
* No HTTP happens here. The caller is responsible for the round trip
|
||||
@@ -44,7 +44,7 @@
|
||||
|
||||
import sodium from "libsodium-wrappers-sumo";
|
||||
import { beforeAll, describe, expect, it } from "vitest";
|
||||
import { init, toBase64, toBase64URL } from "../../src/crypto/index.js";
|
||||
import { init, toBase64, toBase64URLPadded } from "../../src/crypto/index.js";
|
||||
import { unwrapAuth } from "../../src/auth/unwrap.js";
|
||||
import type {
|
||||
AuthorizationResponse,
|
||||
@@ -154,7 +154,9 @@ describe("auth.unwrapAuth", () => {
|
||||
// Token is returned as URL-safe-no-padding base64 of the bytes
|
||||
// sealed by the server. The caller passes this string directly
|
||||
// as the X-Auth-Token header.
|
||||
expect(result.token).toBe(toBase64URL(tokenBytes));
|
||||
// Token uses URL-safe base64 WITH padding to match the Go CLI's
|
||||
// base64.URLEncoding and the Ente server's token validation.
|
||||
expect(result.token).toBe(toBase64URLPadded(tokenBytes));
|
||||
});
|
||||
|
||||
it("rejects a wrong password", async () => {
|
||||
|
||||
64
test/integration/live-login.ts
Normal file
64
test/integration/live-login.ts
Normal file
@@ -0,0 +1,64 @@
|
||||
import { init } from "../../src/crypto/index.js";
|
||||
import { ApiClient } from "../../src/api/client.js";
|
||||
import { beginLogin } from "../../src/auth/login.js";
|
||||
import { unwrapAuth } from "../../src/auth/unwrap.js";
|
||||
|
||||
// Dev account — not a secret, throwaway account for integration testing.
|
||||
const EMAIL = "entedev2026jp@acidhou.se";
|
||||
const PASSWORD = "loldongs";
|
||||
const RECOVERY_KEY =
|
||||
"deliver have behave collect void chicken boring embrace coast reflect squeeze cotton dish resemble license remain quick dwarf plastic ensure amused cry nasty equip";
|
||||
|
||||
void RECOVERY_KEY; // available if needed for account recovery tests
|
||||
|
||||
const main = async () => {
|
||||
await init();
|
||||
const api = new ApiClient();
|
||||
|
||||
console.log(`Logging in as ${EMAIL}...`);
|
||||
const challenge = await beginLogin(api, EMAIL, PASSWORD);
|
||||
console.log("beginLogin returned:", challenge.kind);
|
||||
|
||||
let response;
|
||||
if (challenge.kind === "complete") {
|
||||
response = challenge.response;
|
||||
} else if (challenge.kind === "totp") {
|
||||
console.error("Account requires TOTP — disable 2FA for dev testing");
|
||||
process.exit(1);
|
||||
} else if (challenge.kind === "emailOTP") {
|
||||
console.error("Account uses email OTP — SRP not set up?");
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.error("Unexpected challenge:", challenge);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log("Got AuthorizationResponse, user ID:", response.id);
|
||||
|
||||
console.log("Unwrapping keys...");
|
||||
const { masterKey, token } = await unwrapAuth(response, PASSWORD);
|
||||
console.log("Master key length:", masterKey.length, "bytes");
|
||||
console.log("Token length:", token.length, "chars");
|
||||
|
||||
api.setAuthToken(token);
|
||||
|
||||
console.log("\nFetching collections...");
|
||||
const { collections } = await api.getJSON<{ collections: unknown[] }>(
|
||||
"/collections/v2",
|
||||
{ sinceTime: 0 },
|
||||
);
|
||||
console.log(`Got ${collections.length} collection(s)`);
|
||||
for (const c of collections.slice(0, 5)) {
|
||||
const col = c as Record<string, unknown>;
|
||||
console.log(
|
||||
` id=${col.id} type=${col.type} updationTime=${col.updationTime}`,
|
||||
);
|
||||
}
|
||||
|
||||
console.log("\nLive login test passed.");
|
||||
};
|
||||
|
||||
main().catch((err) => {
|
||||
console.error("FAILED:", err);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user