mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-22 21:50:13 +00:00
Improve Bitwarden compatibility across account, sync, attachment, and send flows
This commit is contained in:
+93
-42
@@ -1,4 +1,4 @@
|
|||||||
import { Env, User, ProfileResponse, DEFAULT_DEV_SECRET } from '../types';
|
import { Env, User, DEFAULT_DEV_SECRET } from '../types';
|
||||||
import { StorageService } from '../services/storage';
|
import { StorageService } from '../services/storage';
|
||||||
import { AuthService } from '../services/auth';
|
import { AuthService } from '../services/auth';
|
||||||
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
import { RateLimitService, getClientIdentifier } from '../services/ratelimit';
|
||||||
@@ -9,6 +9,7 @@ import { LIMITS } from '../config/limits';
|
|||||||
import { isTotpEnabled, verifyTotpToken } from '../utils/totp';
|
import { isTotpEnabled, verifyTotpToken } from '../utils/totp';
|
||||||
import { createRecoveryCode, recoveryCodeEquals } from '../utils/recovery-code';
|
import { createRecoveryCode, recoveryCodeEquals } from '../utils/recovery-code';
|
||||||
import { buildAccountKeys } from '../utils/user-decryption';
|
import { buildAccountKeys } from '../utils/user-decryption';
|
||||||
|
import { buildProfileResponse } from '../utils/profile-response';
|
||||||
|
|
||||||
const TWO_FACTOR_PROVIDER_AUTHENTICATOR = 0;
|
const TWO_FACTOR_PROVIDER_AUTHENTICATOR = 0;
|
||||||
const TOTP_USER_VERIFICATION_TOKEN_TTL_MS = 10 * 60 * 1000;
|
const TOTP_USER_VERIFICATION_TOKEN_TTL_MS = 10 * 60 * 1000;
|
||||||
@@ -174,6 +175,24 @@ function readBodyString(body: Record<string, unknown>, names: string[]): string
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function readNestedString(source: unknown, path: string[]): string {
|
||||||
|
let current = source;
|
||||||
|
for (const key of path) {
|
||||||
|
if (!current || typeof current !== 'object') return '';
|
||||||
|
current = (current as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
return typeof current === 'string' ? current : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
function readNestedNumber(source: unknown, path: string[]): number | undefined {
|
||||||
|
let current = source;
|
||||||
|
for (const key of path) {
|
||||||
|
if (!current || typeof current !== 'object') return undefined;
|
||||||
|
current = (current as Record<string, unknown>)[key];
|
||||||
|
}
|
||||||
|
return typeof current === 'number' ? current : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
async function readRequestBody(request: Request): Promise<Record<string, unknown>> {
|
async function readRequestBody(request: Request): Promise<Record<string, unknown>> {
|
||||||
const contentType = request.headers.get('content-type') || '';
|
const contentType = request.headers.get('content-type') || '';
|
||||||
if (contentType.includes('application/x-www-form-urlencoded')) {
|
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
@@ -183,34 +202,32 @@ async function readRequestBody(request: Request): Promise<Record<string, unknown
|
|||||||
return await request.json();
|
return await request.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
function toProfile(user: User, env: Env): ProfileResponse {
|
function masterPasswordPolicyResponse(): Record<string, unknown> {
|
||||||
void env;
|
return {
|
||||||
|
minComplexity: 0,
|
||||||
|
minLength: 0,
|
||||||
|
requireUpper: false,
|
||||||
|
requireLower: false,
|
||||||
|
requireNumbers: false,
|
||||||
|
requireSpecial: false,
|
||||||
|
enforceOnLogin: false,
|
||||||
|
object: 'masterPasswordPolicy',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function keysResponse(user: User): Record<string, unknown> {
|
||||||
const accountKeys = buildAccountKeys(user);
|
const accountKeys = buildAccountKeys(user);
|
||||||
return {
|
return {
|
||||||
id: user.id,
|
Key: user.key,
|
||||||
name: user.name,
|
PublicKey: user.publicKey ?? '',
|
||||||
email: user.email,
|
PrivateKey: user.privateKey ?? '',
|
||||||
emailVerified: true,
|
AccountKeys: accountKeys,
|
||||||
premium: true,
|
Object: 'keys',
|
||||||
premiumFromOrganization: false,
|
|
||||||
usesKeyConnector: false,
|
|
||||||
masterPasswordHint: user.masterPasswordHint,
|
|
||||||
culture: 'en-US',
|
|
||||||
twoFactorEnabled: !!user.totpSecret,
|
|
||||||
key: user.key,
|
key: user.key,
|
||||||
privateKey: user.privateKey,
|
publicKey: user.publicKey ?? '',
|
||||||
|
privateKey: user.privateKey ?? '',
|
||||||
accountKeys,
|
accountKeys,
|
||||||
securityStamp: user.securityStamp || user.id,
|
object: 'keys',
|
||||||
organizations: [],
|
|
||||||
providers: [],
|
|
||||||
providerOrganizations: [],
|
|
||||||
forcePasswordReset: false,
|
|
||||||
avatarColor: null,
|
|
||||||
creationDate: user.createdAt,
|
|
||||||
verifyDevices: user.verifyDevices,
|
|
||||||
role: user.role,
|
|
||||||
status: user.status,
|
|
||||||
object: 'profile',
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -445,7 +462,7 @@ export async function handleGetProfile(request: Request, env: Env, userId: strin
|
|||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
const user = await storage.getUserById(userId);
|
const user = await storage.getUserById(userId);
|
||||||
if (!user) return errorResponse('User not found', 404);
|
if (!user) return errorResponse('User not found', 404);
|
||||||
return jsonResponse(toProfile(user, env));
|
return jsonResponse(buildProfileResponse(user, env));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT /api/accounts/profile
|
// PUT /api/accounts/profile
|
||||||
@@ -484,7 +501,7 @@ export async function handleUpdateProfile(request: Request, env: Env, userId: st
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return jsonResponse(toProfile(user, env));
|
return jsonResponse(buildProfileResponse(user, env));
|
||||||
}
|
}
|
||||||
|
|
||||||
// PUT/POST /api/accounts/verify-devices
|
// PUT/POST /api/accounts/verify-devices
|
||||||
@@ -498,6 +515,7 @@ export async function handleSetVerifyDevices(request: Request, env: Env, userId:
|
|||||||
secret?: string;
|
secret?: string;
|
||||||
masterPasswordHash?: string;
|
masterPasswordHash?: string;
|
||||||
verifyDevices?: boolean;
|
verifyDevices?: boolean;
|
||||||
|
VerifyDevices?: boolean;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
body = await request.json();
|
body = await request.json();
|
||||||
@@ -505,7 +523,8 @@ export async function handleSetVerifyDevices(request: Request, env: Env, userId:
|
|||||||
return errorResponse('Invalid JSON', 400);
|
return errorResponse('Invalid JSON', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof body.verifyDevices !== 'boolean') {
|
const verifyDevices = typeof body.verifyDevices === 'boolean' ? body.verifyDevices : body.VerifyDevices;
|
||||||
|
if (typeof verifyDevices !== 'boolean') {
|
||||||
return errorResponse('verifyDevices must be true or false', 400);
|
return errorResponse('verifyDevices must be true or false', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -514,7 +533,7 @@ export async function handleSetVerifyDevices(request: Request, env: Env, userId:
|
|||||||
return errorResponse('User verification failed.', 400);
|
return errorResponse('User verification failed.', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
user.verifyDevices = body.verifyDevices;
|
user.verifyDevices = verifyDevices;
|
||||||
user.updatedAt = new Date().toISOString();
|
user.updatedAt = new Date().toISOString();
|
||||||
await storage.saveUser(user);
|
await storage.saveUser(user);
|
||||||
await writeAuditEvent(storage, {
|
await writeAuditEvent(storage, {
|
||||||
@@ -533,6 +552,19 @@ export async function handleSetVerifyDevices(request: Request, env: Env, userId:
|
|||||||
return new Response(null, { status: 200 });
|
return new Response(null, { status: 200 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GET /api/accounts/keys
|
||||||
|
export async function handleGetKeys(request: Request, env: Env, userId: string): Promise<Response> {
|
||||||
|
void request;
|
||||||
|
const storage = new StorageService(env.DB);
|
||||||
|
const user = await storage.getUserById(userId);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return errorResponse('User not found', 404);
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonResponse(keysResponse(user));
|
||||||
|
}
|
||||||
|
|
||||||
// POST /api/accounts/keys
|
// POST /api/accounts/keys
|
||||||
export async function handleSetKeys(request: Request, env: Env, userId: string): Promise<Response> {
|
export async function handleSetKeys(request: Request, env: Env, userId: string): Promise<Response> {
|
||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
@@ -593,7 +625,7 @@ export async function handleSetKeys(request: Request, env: Env, userId: string):
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return handleGetProfile(request, env, userId);
|
return jsonResponse(keysResponse(user));
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST/PUT /api/accounts/password
|
// POST/PUT /api/accounts/password
|
||||||
@@ -607,6 +639,7 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
|||||||
masterPasswordHash?: string;
|
masterPasswordHash?: string;
|
||||||
currentPasswordHash?: string;
|
currentPasswordHash?: string;
|
||||||
newMasterPasswordHash?: string;
|
newMasterPasswordHash?: string;
|
||||||
|
masterPasswordHint?: string | null;
|
||||||
key?: string;
|
key?: string;
|
||||||
newKey?: string;
|
newKey?: string;
|
||||||
encryptedPrivateKey?: string;
|
encryptedPrivateKey?: string;
|
||||||
@@ -617,6 +650,8 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
|||||||
kdfIterations?: number;
|
kdfIterations?: number;
|
||||||
kdfMemory?: number;
|
kdfMemory?: number;
|
||||||
kdfParallelism?: number;
|
kdfParallelism?: number;
|
||||||
|
authenticationData?: Record<string, unknown>;
|
||||||
|
unlockData?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
try {
|
try {
|
||||||
body = await request.json();
|
body = await request.json();
|
||||||
@@ -629,10 +664,16 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
|||||||
const valid = await auth.verifyPassword(currentHash, user.masterPasswordHash, user.email);
|
const valid = await auth.verifyPassword(currentHash, user.masterPasswordHash, user.email);
|
||||||
if (!valid) return errorResponse('Invalid password', 400);
|
if (!valid) return errorResponse('Invalid password', 400);
|
||||||
|
|
||||||
if (!body.newMasterPasswordHash) {
|
const newMasterPasswordHash =
|
||||||
|
body.newMasterPasswordHash ||
|
||||||
|
readNestedString(body, ['authenticationData', 'masterPasswordAuthenticationHash']);
|
||||||
|
if (!newMasterPasswordHash) {
|
||||||
return errorResponse('newMasterPasswordHash is required', 400);
|
return errorResponse('newMasterPasswordHash is required', 400);
|
||||||
}
|
}
|
||||||
const nextKey = body.newKey || body.key;
|
const nextKey =
|
||||||
|
body.newKey ||
|
||||||
|
body.key ||
|
||||||
|
readNestedString(body, ['unlockData', 'masterKeyWrappedUserKey']);
|
||||||
const nextPrivateKey = body.newEncryptedPrivateKey || body.encryptedPrivateKey;
|
const nextPrivateKey = body.newEncryptedPrivateKey || body.encryptedPrivateKey;
|
||||||
const nextPublicKey = body.newPublicKey || body.publicKey;
|
const nextPublicKey = body.newPublicKey || body.publicKey;
|
||||||
if (nextKey && !looksLikeEncString(nextKey)) {
|
if (nextKey && !looksLikeEncString(nextKey)) {
|
||||||
@@ -642,17 +683,24 @@ export async function handleChangePassword(request: Request, env: Env, userId: s
|
|||||||
return errorResponse('new encryptedPrivateKey is not a valid encrypted string', 400);
|
return errorResponse('new encryptedPrivateKey is not a valid encrypted string', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const kdfErr = validateKdfParams(body.kdf ?? user.kdfType, body.kdfIterations, body.kdfMemory, body.kdfParallelism);
|
const nextKdf = body.kdf ?? readNestedNumber(body, ['unlockData', 'kdf', 'kdfType']) ?? user.kdfType;
|
||||||
|
const nextKdfIterations = body.kdfIterations ?? readNestedNumber(body, ['unlockData', 'kdf', 'iterations']);
|
||||||
|
const nextKdfMemory = body.kdfMemory ?? readNestedNumber(body, ['unlockData', 'kdf', 'memory']);
|
||||||
|
const nextKdfParallelism = body.kdfParallelism ?? readNestedNumber(body, ['unlockData', 'kdf', 'parallelism']);
|
||||||
|
const kdfErr = validateKdfParams(nextKdf, nextKdfIterations, nextKdfMemory, nextKdfParallelism);
|
||||||
if (kdfErr) return errorResponse(kdfErr, 400);
|
if (kdfErr) return errorResponse(kdfErr, 400);
|
||||||
|
|
||||||
user.masterPasswordHash = await auth.hashPasswordServer(body.newMasterPasswordHash, user.email);
|
user.masterPasswordHash = await auth.hashPasswordServer(newMasterPasswordHash, user.email);
|
||||||
if (nextKey) user.key = nextKey;
|
if (nextKey) user.key = nextKey;
|
||||||
if (nextPrivateKey) user.privateKey = nextPrivateKey;
|
if (nextPrivateKey) user.privateKey = nextPrivateKey;
|
||||||
if (nextPublicKey) user.publicKey = nextPublicKey;
|
if (nextPublicKey) user.publicKey = nextPublicKey;
|
||||||
if (typeof body.kdf === 'number') user.kdfType = body.kdf;
|
if (typeof nextKdf === 'number') user.kdfType = nextKdf;
|
||||||
if (typeof body.kdfIterations === 'number') user.kdfIterations = body.kdfIterations;
|
if (typeof nextKdfIterations === 'number') user.kdfIterations = nextKdfIterations;
|
||||||
if (typeof body.kdfMemory === 'number') user.kdfMemory = body.kdfMemory;
|
if (typeof nextKdfMemory === 'number') user.kdfMemory = nextKdfMemory;
|
||||||
if (typeof body.kdfParallelism === 'number') user.kdfParallelism = body.kdfParallelism;
|
if (typeof nextKdfParallelism === 'number') user.kdfParallelism = nextKdfParallelism;
|
||||||
|
if (typeof body.masterPasswordHint === 'string' || body.masterPasswordHint === null) {
|
||||||
|
user.masterPasswordHint = body.masterPasswordHint;
|
||||||
|
}
|
||||||
user.securityStamp = generateUUID();
|
user.securityStamp = generateUUID();
|
||||||
user.updatedAt = new Date().toISOString();
|
user.updatedAt = new Date().toISOString();
|
||||||
await storage.saveUser(user);
|
await storage.saveUser(user);
|
||||||
@@ -1061,23 +1109,26 @@ export async function handleVerifyPassword(request: Request, env: Env, userId: s
|
|||||||
return errorResponse('User not found', 404);
|
return errorResponse('User not found', 404);
|
||||||
}
|
}
|
||||||
|
|
||||||
let body: { masterPasswordHash?: string };
|
let body: { masterPasswordHash?: string; authenticationData?: Record<string, unknown> };
|
||||||
try {
|
try {
|
||||||
body = await request.json();
|
body = await request.json();
|
||||||
} catch {
|
} catch {
|
||||||
return errorResponse('Invalid JSON', 400);
|
return errorResponse('Invalid JSON', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!body.masterPasswordHash) {
|
const masterPasswordHash =
|
||||||
|
body.masterPasswordHash ||
|
||||||
|
readNestedString(body, ['authenticationData', 'masterPasswordAuthenticationHash']);
|
||||||
|
if (!masterPasswordHash) {
|
||||||
return errorResponse('masterPasswordHash is required', 400);
|
return errorResponse('masterPasswordHash is required', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
const valid = await auth.verifyPassword(body.masterPasswordHash, user.masterPasswordHash, user.email);
|
const valid = await auth.verifyPassword(masterPasswordHash, user.masterPasswordHash, user.email);
|
||||||
if (!valid) {
|
if (!valid) {
|
||||||
return errorResponse('Invalid password', 400);
|
return errorResponse('Invalid password', 400);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new Response(null, { status: 200 });
|
return jsonResponse(masterPasswordPolicyResponse());
|
||||||
}
|
}
|
||||||
|
|
||||||
// POST /api/accounts/api-key
|
// POST /api/accounts/api-key
|
||||||
|
|||||||
@@ -31,6 +31,14 @@ function notifyVaultSyncForRequest(
|
|||||||
notifyUserVaultSync(env, userId, revisionDate, readActingDeviceIdentifier(request));
|
notifyUserVaultSync(env, userId, revisionDate, readActingDeviceIdentifier(request));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function contentDispositionAttachment(fileName: string | null | undefined): string {
|
||||||
|
const fallback = 'attachment';
|
||||||
|
const value = String(fileName || fallback)
|
||||||
|
.replace(/[\r\n"]/g, '_')
|
||||||
|
.trim() || fallback;
|
||||||
|
return `attachment; filename="${value}"`;
|
||||||
|
}
|
||||||
|
|
||||||
async function writeAttachmentAudit(
|
async function writeAttachmentAudit(
|
||||||
storage: StorageService,
|
storage: StorageService,
|
||||||
request: Request,
|
request: Request,
|
||||||
@@ -415,7 +423,9 @@ export async function handlePublicDownloadAttachment(
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': object.contentType || 'application/octet-stream',
|
'Content-Type': object.contentType || 'application/octet-stream',
|
||||||
'Content-Length': String(object.size),
|
'Content-Length': String(object.size),
|
||||||
|
'Content-Disposition': contentDispositionAttachment(attachment.fileName),
|
||||||
'Cache-Control': 'private, no-cache',
|
'Cache-Control': 'private, no-cache',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -463,9 +473,13 @@ export async function handleDeleteAttachment(
|
|||||||
// Get updated cipher for response
|
// Get updated cipher for response
|
||||||
const updatedCipher = await storage.getCipher(cipherId);
|
const updatedCipher = await storage.getCipher(cipherId);
|
||||||
const attachments = await storage.getAttachmentsByCipher(cipherId);
|
const attachments = await storage.getAttachmentsByCipher(cipherId);
|
||||||
|
const cipherResponse = cipherToResponse(updatedCipher!, attachments);
|
||||||
|
|
||||||
return jsonResponse({
|
return jsonResponse({
|
||||||
cipher: cipherToResponse(updatedCipher!, attachments),
|
Cipher: cipherResponse,
|
||||||
|
cipher: cipherResponse,
|
||||||
|
Object: 'deleteAttachment',
|
||||||
|
object: 'deleteAttachment',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+19
-15
@@ -140,6 +140,20 @@ function buildPreloginResponse(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function masterPasswordPolicyResponse(): TokenResponse['MasterPasswordPolicy'] {
|
||||||
|
return {
|
||||||
|
minComplexity: 0,
|
||||||
|
minLength: 0,
|
||||||
|
requireUpper: false,
|
||||||
|
requireLower: false,
|
||||||
|
requireNumbers: false,
|
||||||
|
requireSpecial: false,
|
||||||
|
enforceOnLogin: false,
|
||||||
|
Object: 'masterPasswordPolicy',
|
||||||
|
object: 'masterPasswordPolicy',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function twoFactorRequiredResponse(message: string = 'Two factor required.'): Response {
|
function twoFactorRequiredResponse(message: string = 'Two factor required.'): Response {
|
||||||
// Match Bitwarden Identity: TwoFactorProviders2 lists enabled 2FA providers only.
|
// Match Bitwarden Identity: TwoFactorProviders2 lists enabled 2FA providers only.
|
||||||
// Clients expose recovery-code entry points themselves; Android 2026.4 fails to
|
// Clients expose recovery-code entry points themselves; Android 2026.4 fails to
|
||||||
@@ -151,9 +165,7 @@ function twoFactorRequiredResponse(message: string = 'Two factor required.'): Re
|
|||||||
TwoFactorProviders: providers,
|
TwoFactorProviders: providers,
|
||||||
TwoFactorProviders2: providers2,
|
TwoFactorProviders2: providers2,
|
||||||
SsoEmail2faSessionToken: null,
|
SsoEmail2faSessionToken: null,
|
||||||
MasterPasswordPolicy: {
|
MasterPasswordPolicy: masterPasswordPolicyResponse(),
|
||||||
Object: 'masterPasswordPolicy',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Bitwarden clients rely on these fields to trigger the 2FA UI flow.
|
// Bitwarden clients rely on these fields to trigger the 2FA UI flow.
|
||||||
@@ -446,9 +458,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
KdfParallelism: user.kdfParallelism,
|
KdfParallelism: user.kdfParallelism,
|
||||||
ForcePasswordReset: false,
|
ForcePasswordReset: false,
|
||||||
ResetMasterPassword: false,
|
ResetMasterPassword: false,
|
||||||
MasterPasswordPolicy: {
|
MasterPasswordPolicy: masterPasswordPolicyResponse(),
|
||||||
Object: 'masterPasswordPolicy',
|
|
||||||
},
|
|
||||||
ApiUseKeyConnector: false,
|
ApiUseKeyConnector: false,
|
||||||
scope: 'api offline_access',
|
scope: 'api offline_access',
|
||||||
unofficialServer: true,
|
unofficialServer: true,
|
||||||
@@ -566,9 +576,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
KdfParallelism: user.kdfParallelism,
|
KdfParallelism: user.kdfParallelism,
|
||||||
ForcePasswordReset: false,
|
ForcePasswordReset: false,
|
||||||
ResetMasterPassword: false,
|
ResetMasterPassword: false,
|
||||||
MasterPasswordPolicy: {
|
MasterPasswordPolicy: masterPasswordPolicyResponse(),
|
||||||
Object: 'masterPasswordPolicy',
|
|
||||||
},
|
|
||||||
ApiUseKeyConnector: false,
|
ApiUseKeyConnector: false,
|
||||||
scope: 'api offline_access',
|
scope: 'api offline_access',
|
||||||
unofficialServer: true,
|
unofficialServer: true,
|
||||||
@@ -696,9 +704,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
KdfParallelism: user.kdfParallelism,
|
KdfParallelism: user.kdfParallelism,
|
||||||
ForcePasswordReset: false,
|
ForcePasswordReset: false,
|
||||||
ResetMasterPassword: false,
|
ResetMasterPassword: false,
|
||||||
MasterPasswordPolicy: {
|
MasterPasswordPolicy: masterPasswordPolicyResponse(),
|
||||||
Object: 'masterPasswordPolicy',
|
|
||||||
},
|
|
||||||
ApiUseKeyConnector: false,
|
ApiUseKeyConnector: false,
|
||||||
scope: 'api offline_access',
|
scope: 'api offline_access',
|
||||||
unofficialServer: true,
|
unofficialServer: true,
|
||||||
@@ -836,9 +842,7 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
|||||||
KdfParallelism: user.kdfParallelism,
|
KdfParallelism: user.kdfParallelism,
|
||||||
ForcePasswordReset: false,
|
ForcePasswordReset: false,
|
||||||
ResetMasterPassword: false,
|
ResetMasterPassword: false,
|
||||||
MasterPasswordPolicy: {
|
MasterPasswordPolicy: masterPasswordPolicyResponse(),
|
||||||
Object: 'masterPasswordPolicy',
|
|
||||||
},
|
|
||||||
ApiUseKeyConnector: false,
|
ApiUseKeyConnector: false,
|
||||||
scope: 'api offline_access',
|
scope: 'api offline_access',
|
||||||
unofficialServer: true,
|
unofficialServer: true,
|
||||||
|
|||||||
@@ -33,6 +33,14 @@ import {
|
|||||||
verifySendPasswordHashB64,
|
verifySendPasswordHashB64,
|
||||||
} from './sends-shared';
|
} from './sends-shared';
|
||||||
|
|
||||||
|
function contentDispositionAttachment(fileName: string | null | undefined): string {
|
||||||
|
const fallback = 'send-file';
|
||||||
|
const value = String(fileName || fallback)
|
||||||
|
.replace(/[\r\n"]/g, '_')
|
||||||
|
.trim() || fallback;
|
||||||
|
return `attachment; filename="${value}"`;
|
||||||
|
}
|
||||||
|
|
||||||
export async function handleAccessSend(request: Request, env: Env, accessId: string): Promise<Response> {
|
export async function handleAccessSend(request: Request, env: Env, accessId: string): Promise<Response> {
|
||||||
const storage = new StorageService(env.DB);
|
const storage = new StorageService(env.DB);
|
||||||
const sendId = fromAccessId(accessId);
|
const sendId = fromAccessId(accessId);
|
||||||
@@ -282,6 +290,9 @@ export async function handleDownloadSendFile(
|
|||||||
if (!object) {
|
if (!object) {
|
||||||
return errorResponse('Send file not found', 404);
|
return errorResponse('Send file not found', 404);
|
||||||
}
|
}
|
||||||
|
const send = await storage.getSend(sendId);
|
||||||
|
const data = send ? parseStoredSendData(send) : {};
|
||||||
|
const fileName = typeof data.fileName === 'string' ? data.fileName : fileId;
|
||||||
|
|
||||||
const firstUse = await storage.consumeAttachmentDownloadToken(`send:${claims.jti}`, claims.exp);
|
const firstUse = await storage.consumeAttachmentDownloadToken(`send:${claims.jti}`, claims.exp);
|
||||||
if (!firstUse) {
|
if (!firstUse) {
|
||||||
@@ -292,7 +303,9 @@ export async function handleDownloadSendFile(
|
|||||||
headers: {
|
headers: {
|
||||||
'Content-Type': object.contentType || 'application/octet-stream',
|
'Content-Type': object.contentType || 'application/octet-stream',
|
||||||
'Content-Length': String(object.size),
|
'Content-Length': String(object.size),
|
||||||
|
'Content-Disposition': contentDispositionAttachment(fileName),
|
||||||
'Cache-Control': 'private, no-cache',
|
'Cache-Control': 'private, no-cache',
|
||||||
|
'X-Content-Type-Options': 'nosniff',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
+4
-26
@@ -5,12 +5,12 @@ import { cipherToResponse, isCipherResponseSyncCompatible, shouldPreserveRepaira
|
|||||||
import { sendToResponse } from './sends';
|
import { sendToResponse } from './sends';
|
||||||
import { LIMITS } from '../config/limits';
|
import { LIMITS } from '../config/limits';
|
||||||
import {
|
import {
|
||||||
buildAccountKeys,
|
|
||||||
buildUserDecryptionCompat,
|
buildUserDecryptionCompat,
|
||||||
buildUserDecryptionOptions,
|
buildUserDecryptionOptions,
|
||||||
} from '../utils/user-decryption';
|
} from '../utils/user-decryption';
|
||||||
import { buildDomainsResponse } from '../services/domain-rules';
|
import { buildDomainsResponse } from '../services/domain-rules';
|
||||||
import { buildWebAuthnPrfOption } from '../utils/account-passkeys';
|
import { buildWebAuthnPrfOption } from '../utils/account-passkeys';
|
||||||
|
import { buildProfileResponse } from '../utils/profile-response';
|
||||||
|
|
||||||
// CONTRACT:
|
// CONTRACT:
|
||||||
// /api/sync reuses cipherToResponse() as the single cipher response shaper.
|
// /api/sync reuses cipherToResponse() as the single cipher response shaper.
|
||||||
@@ -84,36 +84,12 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
storage.getAttachmentsByUserId(userId),
|
storage.getAttachmentsByUserId(userId),
|
||||||
excludeDomains ? Promise.resolve(null) : storage.getUserDomainSettings(userId),
|
excludeDomains ? Promise.resolve(null) : storage.getUserDomainSettings(userId),
|
||||||
]);
|
]);
|
||||||
const accountKeys = buildAccountKeys(user);
|
|
||||||
const webAuthnPrfOptions = accountPasskeys
|
const webAuthnPrfOptions = accountPasskeys
|
||||||
.map(buildWebAuthnPrfOption)
|
.map(buildWebAuthnPrfOption)
|
||||||
.filter((option): option is NonNullable<typeof option> => !!option);
|
.filter((option): option is NonNullable<typeof option> => !!option);
|
||||||
const userDecryptionOptions = buildUserDecryptionOptions(user, webAuthnPrfOptions[0] || null);
|
const userDecryptionOptions = buildUserDecryptionOptions(user, webAuthnPrfOptions[0] || null);
|
||||||
|
|
||||||
const profile: ProfileResponse = {
|
const profile: ProfileResponse = buildProfileResponse(user, env);
|
||||||
id: user.id,
|
|
||||||
name: user.name,
|
|
||||||
email: user.email,
|
|
||||||
emailVerified: true,
|
|
||||||
premium: true,
|
|
||||||
premiumFromOrganization: false,
|
|
||||||
usesKeyConnector: false,
|
|
||||||
masterPasswordHint: user.masterPasswordHint,
|
|
||||||
culture: 'en-US',
|
|
||||||
twoFactorEnabled: !!user.totpSecret,
|
|
||||||
key: user.key,
|
|
||||||
privateKey: user.privateKey,
|
|
||||||
accountKeys,
|
|
||||||
securityStamp: user.securityStamp || user.id,
|
|
||||||
organizations: [],
|
|
||||||
providers: [],
|
|
||||||
providerOrganizations: [],
|
|
||||||
forcePasswordReset: false,
|
|
||||||
avatarColor: null,
|
|
||||||
creationDate: user.createdAt,
|
|
||||||
verifyDevices: user.verifyDevices,
|
|
||||||
object: 'profile',
|
|
||||||
};
|
|
||||||
|
|
||||||
const cipherResponses: CipherResponse[] = [];
|
const cipherResponses: CipherResponse[] = [];
|
||||||
for (const cipher of ciphers) {
|
for (const cipher of ciphers) {
|
||||||
@@ -149,6 +125,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
{ omitExcludedGlobals: true }
|
{ omitExcludedGlobals: true }
|
||||||
),
|
),
|
||||||
policies: [],
|
policies: [],
|
||||||
|
policiesNew: [],
|
||||||
sends: sendResponses,
|
sends: sendResponses,
|
||||||
UserDecryption: {
|
UserDecryption: {
|
||||||
MasterPasswordUnlock: userDecryptionOptions.MasterPasswordUnlock,
|
MasterPasswordUnlock: userDecryptionOptions.MasterPasswordUnlock,
|
||||||
@@ -156,6 +133,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
|||||||
KeyConnectorOption: null,
|
KeyConnectorOption: null,
|
||||||
WebAuthnPrfOption: webAuthnPrfOptions[0] || null,
|
WebAuthnPrfOption: webAuthnPrfOptions[0] || null,
|
||||||
WebAuthnPrfOptions: webAuthnPrfOptions,
|
WebAuthnPrfOptions: webAuthnPrfOptions,
|
||||||
|
V2UpgradeToken: null,
|
||||||
Object: 'userDecryption',
|
Object: 'userDecryption',
|
||||||
},
|
},
|
||||||
UserDecryptionOptions: userDecryptionOptions,
|
UserDecryptionOptions: userDecryptionOptions,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { errorResponse, jsonResponse } from './utils/response';
|
|||||||
import {
|
import {
|
||||||
handleGetProfile,
|
handleGetProfile,
|
||||||
handleUpdateProfile,
|
handleUpdateProfile,
|
||||||
|
handleGetKeys,
|
||||||
handleSetKeys,
|
handleSetKeys,
|
||||||
handleGetRevisionDate,
|
handleGetRevisionDate,
|
||||||
handleVerifyPassword,
|
handleVerifyPassword,
|
||||||
@@ -115,8 +116,10 @@ export async function handleAuthenticatedRoute(
|
|||||||
return handleChangePassword(request, env, userId);
|
return handleChangePassword(request, env, userId);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path === '/api/accounts/keys' && method === 'POST') {
|
if (path === '/api/accounts/keys') {
|
||||||
return handleSetKeys(request, env, userId);
|
if (method === 'GET') return handleGetKeys(request, env, userId);
|
||||||
|
if (method === 'POST') return handleSetKeys(request, env, userId);
|
||||||
|
return errorResponse('Method not allowed', 405);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (path === '/api/accounts/totp') {
|
if (path === '/api/accounts/totp') {
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ function parseCipherRow(row: CipherRow | null | undefined): Cipher | null {
|
|||||||
createdAt: row.created_at,
|
createdAt: row.created_at,
|
||||||
updatedAt: row.updated_at,
|
updatedAt: row.updated_at,
|
||||||
archivedAt: row.archived_at ?? parsed.archivedAt ?? parsed.archivedDate ?? null,
|
archivedAt: row.archived_at ?? parsed.archivedAt ?? parsed.archivedDate ?? null,
|
||||||
deletedAt: row.deleted_at ?? null,
|
deletedAt: row.deleted_at ?? parsed.deletedAt ?? parsed.deletedDate ?? null,
|
||||||
};
|
};
|
||||||
} catch {
|
} catch {
|
||||||
console.error('Corrupted cipher data, id:', row.id);
|
console.error('Corrupted cipher data, id:', row.id);
|
||||||
@@ -244,7 +244,9 @@ export async function getCiphersPage(
|
|||||||
limit: number,
|
limit: number,
|
||||||
offset: number
|
offset: number
|
||||||
): Promise<Cipher[]> {
|
): Promise<Cipher[]> {
|
||||||
const whereDeleted = includeDeleted ? '' : 'AND deleted_at IS NULL';
|
const whereDeleted = includeDeleted
|
||||||
|
? ''
|
||||||
|
: "AND deleted_at IS NULL AND json_extract(data, '$.deletedAt') IS NULL AND json_extract(data, '$.deletedDate') IS NULL";
|
||||||
const res = await db
|
const res = await db
|
||||||
.prepare(
|
.prepare(
|
||||||
`SELECT ${selectCipherColumns()} FROM ciphers
|
`SELECT ${selectCipherColumns()} FROM ciphers
|
||||||
@@ -341,7 +343,10 @@ export async function bulkArchiveCiphers(
|
|||||||
`UPDATE ciphers
|
`UPDATE ciphers
|
||||||
SET archived_at = ?, updated_at = ?,
|
SET archived_at = ?, updated_at = ?,
|
||||||
data = json_remove(data, '$.archivedAt', '$.archivedDate', '$.updatedAt', '$.revisionDate')
|
data = json_remove(data, '$.archivedAt', '$.archivedDate', '$.updatedAt', '$.revisionDate')
|
||||||
WHERE user_id = ? AND id IN (${placeholders}) AND deleted_at IS NULL`
|
WHERE user_id = ? AND id IN (${placeholders})
|
||||||
|
AND deleted_at IS NULL
|
||||||
|
AND json_extract(data, '$.deletedAt') IS NULL
|
||||||
|
AND json_extract(data, '$.deletedDate') IS NULL`
|
||||||
)
|
)
|
||||||
.bind(now, now, userId, ...chunk)
|
.bind(now, now, userId, ...chunk)
|
||||||
.run();
|
.run();
|
||||||
|
|||||||
+15
-1
@@ -465,7 +465,15 @@ export interface TokenResponse {
|
|||||||
scope: string;
|
scope: string;
|
||||||
unofficialServer: boolean;
|
unofficialServer: boolean;
|
||||||
MasterPasswordPolicy?: {
|
MasterPasswordPolicy?: {
|
||||||
|
minComplexity: number;
|
||||||
|
minLength: number;
|
||||||
|
requireUpper: boolean;
|
||||||
|
requireLower: boolean;
|
||||||
|
requireNumbers: boolean;
|
||||||
|
requireSpecial: boolean;
|
||||||
|
enforceOnLogin: boolean;
|
||||||
Object: string;
|
Object: string;
|
||||||
|
object?: string;
|
||||||
} | null;
|
} | null;
|
||||||
ApiUseKeyConnector?: boolean;
|
ApiUseKeyConnector?: boolean;
|
||||||
AccountKeys?: any | null;
|
AccountKeys?: any | null;
|
||||||
@@ -494,12 +502,13 @@ export interface ProfileResponse {
|
|||||||
accountKeys: any | null;
|
accountKeys: any | null;
|
||||||
securityStamp: string;
|
securityStamp: string;
|
||||||
organizations: any[];
|
organizations: any[];
|
||||||
|
organizationsNew?: any[];
|
||||||
providers: any[];
|
providers: any[];
|
||||||
providerOrganizations: any[];
|
providerOrganizations: any[];
|
||||||
forcePasswordReset: boolean;
|
forcePasswordReset: boolean;
|
||||||
avatarColor: string | null;
|
avatarColor: string | null;
|
||||||
creationDate: string;
|
creationDate: string;
|
||||||
verifyDevices?: boolean;
|
verifyDevices: boolean;
|
||||||
role?: UserRole;
|
role?: UserRole;
|
||||||
status?: UserStatus;
|
status?: UserStatus;
|
||||||
object: string;
|
object: string;
|
||||||
@@ -558,6 +567,7 @@ export interface SyncResponse {
|
|||||||
ciphers: CipherResponse[];
|
ciphers: CipherResponse[];
|
||||||
domains: any;
|
domains: any;
|
||||||
policies: any[];
|
policies: any[];
|
||||||
|
policiesNew?: any[];
|
||||||
sends: SendResponse[];
|
sends: SendResponse[];
|
||||||
UserDecryption?: {
|
UserDecryption?: {
|
||||||
MasterPasswordUnlock: MasterPasswordUnlock | null;
|
MasterPasswordUnlock: MasterPasswordUnlock | null;
|
||||||
@@ -565,6 +575,10 @@ export interface SyncResponse {
|
|||||||
KeyConnectorOption?: null;
|
KeyConnectorOption?: null;
|
||||||
WebAuthnPrfOption?: WebAuthnPrfDecryptionOption | null;
|
WebAuthnPrfOption?: WebAuthnPrfDecryptionOption | null;
|
||||||
WebAuthnPrfOptions?: WebAuthnPrfDecryptionOption[];
|
WebAuthnPrfOptions?: WebAuthnPrfDecryptionOption[];
|
||||||
|
V2UpgradeToken?: {
|
||||||
|
WrappedUserKey1: string;
|
||||||
|
WrappedUserKey2: string;
|
||||||
|
} | null;
|
||||||
Object?: string;
|
Object?: string;
|
||||||
} | null;
|
} | null;
|
||||||
// PascalCase for desktop/browser clients
|
// PascalCase for desktop/browser clients
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import type { Env, ProfileResponse, User } from '../types';
|
||||||
|
import { buildAccountKeys } from './user-decryption';
|
||||||
|
|
||||||
|
export function buildProfileResponse(user: User, env?: Env): ProfileResponse {
|
||||||
|
void env;
|
||||||
|
const organizations: any[] = [];
|
||||||
|
const accountKeys = buildAccountKeys(user);
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: user.id,
|
||||||
|
name: user.name,
|
||||||
|
email: user.email,
|
||||||
|
emailVerified: true,
|
||||||
|
premium: true,
|
||||||
|
premiumFromOrganization: false,
|
||||||
|
usesKeyConnector: false,
|
||||||
|
masterPasswordHint: user.masterPasswordHint,
|
||||||
|
culture: 'en-US',
|
||||||
|
twoFactorEnabled: !!user.totpSecret,
|
||||||
|
key: user.key,
|
||||||
|
privateKey: user.privateKey,
|
||||||
|
accountKeys,
|
||||||
|
securityStamp: user.securityStamp || user.id,
|
||||||
|
organizations,
|
||||||
|
organizationsNew: organizations,
|
||||||
|
providers: [],
|
||||||
|
providerOrganizations: [],
|
||||||
|
forcePasswordReset: false,
|
||||||
|
avatarColor: null,
|
||||||
|
creationDate: user.createdAt,
|
||||||
|
verifyDevices: user.verifyDevices !== false,
|
||||||
|
role: user.role,
|
||||||
|
status: user.status,
|
||||||
|
object: 'profile',
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ export function buildAccountKeys(user: Pick<User, 'privateKey' | 'publicKey'>):
|
|||||||
publicKeyEncryptionKeyPair: {
|
publicKeyEncryptionKeyPair: {
|
||||||
wrappedPrivateKey: user.privateKey,
|
wrappedPrivateKey: user.privateKey,
|
||||||
publicKey,
|
publicKey,
|
||||||
|
signedPublicKey: null,
|
||||||
Object: 'publicKeyEncryptionKeyPair',
|
Object: 'publicKeyEncryptionKeyPair',
|
||||||
},
|
},
|
||||||
Object: 'privateKeys',
|
Object: 'privateKeys',
|
||||||
|
|||||||
@@ -500,7 +500,6 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
|
|||||||
if (!session?.accessToken) throw new Error(t('txt_offline_vault_readonly'));
|
if (!session?.accessToken) throw new Error(t('txt_offline_vault_readonly'));
|
||||||
const headers = new Headers(init.headers || {});
|
const headers = new Headers(init.headers || {});
|
||||||
headers.set('Authorization', `Bearer ${session.accessToken}`);
|
headers.set('Authorization', `Bearer ${session.accessToken}`);
|
||||||
headers.set('X-NodeWarden-Web', '1');
|
|
||||||
|
|
||||||
let resp = await retryableRequest(headers);
|
let resp = await retryableRequest(headers);
|
||||||
if (resp.status !== 401 || (!session.refreshToken && session.authMode !== 'web-cookie')) return resp;
|
if (resp.status !== 401 || (!session.refreshToken && session.authMode !== 'web-cookie')) return resp;
|
||||||
@@ -509,7 +508,6 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
|
|||||||
if (latest?.accessToken && latest.accessToken !== session.accessToken) {
|
if (latest?.accessToken && latest.accessToken !== session.accessToken) {
|
||||||
const latestHeaders = new Headers(init.headers || {});
|
const latestHeaders = new Headers(init.headers || {});
|
||||||
latestHeaders.set('Authorization', `Bearer ${latest.accessToken}`);
|
latestHeaders.set('Authorization', `Bearer ${latest.accessToken}`);
|
||||||
latestHeaders.set('X-NodeWarden-Web', '1');
|
|
||||||
resp = await retryableRequest(latestHeaders);
|
resp = await retryableRequest(latestHeaders);
|
||||||
if (resp.status !== 401) return resp;
|
if (resp.status !== 401) return resp;
|
||||||
}
|
}
|
||||||
@@ -535,7 +533,6 @@ export function createAuthedFetch(getSession: () => SessionState | null, setSess
|
|||||||
|
|
||||||
const retryHeaders = new Headers(init.headers || {});
|
const retryHeaders = new Headers(init.headers || {});
|
||||||
retryHeaders.set('Authorization', `Bearer ${nextSession.accessToken}`);
|
retryHeaders.set('Authorization', `Bearer ${nextSession.accessToken}`);
|
||||||
retryHeaders.set('X-NodeWarden-Web', '1');
|
|
||||||
resp = await retryableRequest(retryHeaders);
|
resp = await retryableRequest(retryHeaders);
|
||||||
return resp;
|
return resp;
|
||||||
};
|
};
|
||||||
@@ -599,14 +596,35 @@ export async function changeMasterPassword(
|
|||||||
const nextEnc = await hkdfExpand(nextMasterKey, 'enc', 32);
|
const nextEnc = await hkdfExpand(nextMasterKey, 'enc', 32);
|
||||||
const nextMac = await hkdfExpand(nextMasterKey, 'mac', 32);
|
const nextMac = await hkdfExpand(nextMasterKey, 'mac', 32);
|
||||||
const newKey = await encryptBw(userSym.slice(0, 64), nextEnc, nextMac);
|
const newKey = await encryptBw(userSym.slice(0, 64), nextEnc, nextMac);
|
||||||
|
const newMasterPasswordHash = bytesToBase64(nextHash);
|
||||||
|
|
||||||
const resp = await authedFetch('/api/accounts/password', {
|
const resp = await authedFetch('/api/accounts/password', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
currentPasswordHash: current.hash,
|
masterPasswordHash: current.hash,
|
||||||
newMasterPasswordHash: bytesToBase64(nextHash),
|
newMasterPasswordHash,
|
||||||
newKey,
|
key: newKey,
|
||||||
|
authenticationData: {
|
||||||
|
kdf: {
|
||||||
|
kdfType: 0,
|
||||||
|
iterations: current.kdfIterations,
|
||||||
|
memory: null,
|
||||||
|
parallelism: null,
|
||||||
|
},
|
||||||
|
masterPasswordAuthenticationHash: newMasterPasswordHash,
|
||||||
|
salt: args.email.trim().toLowerCase(),
|
||||||
|
},
|
||||||
|
unlockData: {
|
||||||
|
kdf: {
|
||||||
|
kdfType: 0,
|
||||||
|
iterations: current.kdfIterations,
|
||||||
|
memory: null,
|
||||||
|
parallelism: null,
|
||||||
|
},
|
||||||
|
masterKeyWrappedUserKey: newKey,
|
||||||
|
salt: args.email.trim().toLowerCase(),
|
||||||
|
},
|
||||||
kdf: 0,
|
kdf: 0,
|
||||||
kdfIterations: current.kdfIterations,
|
kdfIterations: current.kdfIterations,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ import { readResponseBytesWithProgress } from '../download';
|
|||||||
import { loadVaultCoreSyncSnapshot } from './vault-sync';
|
import { loadVaultCoreSyncSnapshot } from './vault-sync';
|
||||||
|
|
||||||
type CipherLoginData = NonNullable<Cipher['login']>;
|
type CipherLoginData = NonNullable<Cipher['login']>;
|
||||||
|
const NODEWARDEN_WEB_REPAIR_HEADER = 'X-NodeWarden-Web';
|
||||||
|
|
||||||
export async function getFolders(authedFetch: AuthedFetch, cacheKey: string): Promise<Folder[]> {
|
export async function getFolders(authedFetch: AuthedFetch, cacheKey: string): Promise<Folder[]> {
|
||||||
const body = await loadVaultCoreSyncSnapshot(authedFetch, cacheKey);
|
const body = await loadVaultCoreSyncSnapshot(authedFetch, cacheKey);
|
||||||
@@ -933,7 +934,7 @@ export async function repairCipherUriChecksums(
|
|||||||
|
|
||||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: { 'Content-Type': 'application/json', [NODEWARDEN_WEB_REPAIR_HEADER]: '1' },
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error(await parseErrorMessage(resp, 'Repair URI checksum failed'));
|
if (!resp.ok) throw new Error(await parseErrorMessage(resp, 'Repair URI checksum failed'));
|
||||||
@@ -1092,9 +1093,14 @@ export async function repairCipherKeyMismatches(
|
|||||||
if (!cipher?.id || !looksLikeCipherString(cipher.key)) continue;
|
if (!cipher?.id || !looksLikeCipherString(cipher.key)) continue;
|
||||||
if (!(await hasItemKeyFieldMismatch(cipher, userEnc, userMac))) continue;
|
if (!(await hasItemKeyFieldMismatch(cipher, userEnc, userMac))) continue;
|
||||||
if (hasUnresolvedEncryptedFields(cipher)) continue;
|
if (hasUnresolvedEncryptedFields(cipher)) continue;
|
||||||
await updateCipher(authedFetch, session, cipher, draftFromDecryptedCipher(cipher), {
|
await updateCipher(
|
||||||
preserveRevisionDate: true,
|
authedFetch,
|
||||||
});
|
session,
|
||||||
|
cipher,
|
||||||
|
draftFromDecryptedCipher(cipher),
|
||||||
|
{ preserveRevisionDate: true },
|
||||||
|
{ webRepair: true }
|
||||||
|
);
|
||||||
repaired += 1;
|
repaired += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1229,7 +1235,8 @@ export async function updateCipher(
|
|||||||
session: SessionState,
|
session: SessionState,
|
||||||
cipher: Cipher,
|
cipher: Cipher,
|
||||||
draft: VaultDraft,
|
draft: VaultDraft,
|
||||||
extraPayload?: Record<string, unknown>
|
extraPayload?: Record<string, unknown>,
|
||||||
|
options?: { webRepair?: boolean }
|
||||||
): Promise<Cipher> {
|
): Promise<Cipher> {
|
||||||
const payload = await buildCipherPayload(session, draft, cipher);
|
const payload = await buildCipherPayload(session, draft, cipher);
|
||||||
if (extraPayload) {
|
if (extraPayload) {
|
||||||
@@ -1238,7 +1245,10 @@ export async function updateCipher(
|
|||||||
|
|
||||||
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
const resp = await authedFetch(`/api/ciphers/${encodeURIComponent(cipher.id)}`, {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
headers: { 'Content-Type': 'application/json' },
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
...(options?.webRepair ? { [NODEWARDEN_WEB_REPAIR_HEADER]: '1' } : {}),
|
||||||
|
},
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload),
|
||||||
});
|
});
|
||||||
if (!resp.ok) throw new Error('Update item failed');
|
if (!resp.ok) throw new Error('Update item failed');
|
||||||
|
|||||||
Reference in New Issue
Block a user