mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 13:00:39 +00:00
Compare commits
3 Commits
a75955ca6d
...
192071e4a7
| Author | SHA1 | Date | |
|---|---|---|---|
| 192071e4a7 | |||
| fcf7c80daa | |||
| ed9251c014 |
+81
-12
@@ -10,6 +10,7 @@ import {
|
|||||||
Attachment,
|
Attachment,
|
||||||
PasswordHistory,
|
PasswordHistory,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import { LIMITS } from '../config/limits';
|
||||||
import { StorageService } from '../services/storage';
|
import { StorageService } from '../services/storage';
|
||||||
import { notifyUserVaultSync } from '../durable/notifications-hub';
|
import { notifyUserVaultSync } from '../durable/notifications-hub';
|
||||||
import { jsonResponse, errorResponse } from '../utils/response';
|
import { jsonResponse, errorResponse } from '../utils/response';
|
||||||
@@ -129,6 +130,16 @@ function optionalEncString(value: unknown): string | null {
|
|||||||
return isValidEncString(value) ? value.trim() : null;
|
return isValidEncString(value) ? value.trim() : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function shouldAcceptCipherKey(value: unknown): boolean {
|
||||||
|
if (LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled) return true;
|
||||||
|
return optionalEncString(value) === null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeCipherKeyForStorage(value: unknown): string | null {
|
||||||
|
if (!LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled) return null;
|
||||||
|
return optionalEncString(value);
|
||||||
|
}
|
||||||
|
|
||||||
function sanitizeEncryptedObject<T extends Record<string, any>>(
|
function sanitizeEncryptedObject<T extends Record<string, any>>(
|
||||||
source: T | null | undefined,
|
source: T | null | undefined,
|
||||||
encryptedKeys: readonly string[]
|
encryptedKeys: readonly string[]
|
||||||
@@ -161,20 +172,57 @@ export function normalizeCipherLoginForStorage(login: any): any {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeCipherLoginForCompatibility(login: any): any {
|
export function normalizeCipherLoginForCompatibility(login: any, requiresUriChecksum: boolean = false): any {
|
||||||
const normalized = normalizeCipherLoginForStorage(login);
|
const normalized = normalizeCipherLoginForStorage(login);
|
||||||
if (!normalized || typeof normalized !== 'object') return normalized ?? null;
|
if (!normalized || typeof normalized !== 'object') return normalized ?? null;
|
||||||
const next = sanitizeEncryptedObject(normalized, ['username', 'password', 'totp', 'uri']);
|
const next = sanitizeEncryptedObject(normalized, ['username', 'password', 'totp', 'uri']);
|
||||||
if (!next) return null;
|
if (!next) return null;
|
||||||
next.uris = Array.isArray(next.uris)
|
next.uris = normalizeCipherLoginUrisForCompatibility(next.uris, {
|
||||||
? next.uris
|
hasLegacyLoginUri: isValidEncString(next.uri),
|
||||||
.map((uri: any) => sanitizeEncryptedObject(uri, ['uri', 'uriChecksum']))
|
requiresUriChecksum,
|
||||||
.filter((uri: any) => !!uri && (uri.uri || uri.uriChecksum || uri.match != null))
|
});
|
||||||
: null;
|
|
||||||
next.fido2Credentials = normalizeFido2CredentialsForCompatibility(next.fido2Credentials);
|
next.fido2Credentials = normalizeFido2CredentialsForCompatibility(next.fido2Credentials);
|
||||||
return next;
|
return next;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCipherLoginUrisForCompatibility(
|
||||||
|
uris: any,
|
||||||
|
options: { hasLegacyLoginUri?: boolean; requiresUriChecksum?: boolean } = {}
|
||||||
|
): any[] | null {
|
||||||
|
if (!Array.isArray(uris) || uris.length === 0) return null;
|
||||||
|
const out: any[] = [];
|
||||||
|
|
||||||
|
for (const uri of uris) {
|
||||||
|
if (!uri || typeof uri !== 'object') continue;
|
||||||
|
const next = sanitizeEncryptedObject(uri, ['uri', 'uriChecksum']);
|
||||||
|
if (!next) continue;
|
||||||
|
|
||||||
|
const hasUri = isValidEncString(next.uri);
|
||||||
|
const hasChecksum = isValidEncString(next.uriChecksum);
|
||||||
|
const hasMatch = next.match != null;
|
||||||
|
|
||||||
|
if (hasUri && hasChecksum) {
|
||||||
|
out.push(next);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasUri && !hasChecksum) {
|
||||||
|
// Bitwarden browser clients using the SDK can fail the whole vault load
|
||||||
|
// when an item-key encrypted URI has no encrypted checksum. The server
|
||||||
|
// cannot derive the checksum, so expose the item without the bad URI.
|
||||||
|
if (options.requiresUriChecksum || options.hasLegacyLoginUri) continue;
|
||||||
|
out.push({ ...next, uri: null, uriChecksum: null });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasChecksum || hasMatch) {
|
||||||
|
out.push(next);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return out.length ? out : null;
|
||||||
|
}
|
||||||
|
|
||||||
function hasMissingLoginUriChecksum(cipher: Cipher): boolean {
|
function hasMissingLoginUriChecksum(cipher: Cipher): boolean {
|
||||||
if (!cipher.key || !cipher.login || typeof cipher.login !== 'object') return false;
|
if (!cipher.key || !cipher.login || typeof cipher.login !== 'object') return false;
|
||||||
const uris = (cipher.login as any).uris;
|
const uris = (cipher.login as any).uris;
|
||||||
@@ -255,6 +303,14 @@ export function normalizeCipherSshKeyForCompatibility(sshKey: any): any {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function normalizeCipherSecureNoteForCompatibility(secureNote: any): CipherSecureNote | null {
|
||||||
|
if (!secureNote || typeof secureNote !== 'object') return null;
|
||||||
|
const type = Number(secureNote?.type ?? secureNote?.Type ?? 0);
|
||||||
|
return {
|
||||||
|
type: Number.isFinite(type) ? type : 0,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Format attachments for API response
|
// Format attachments for API response
|
||||||
export function formatAttachments(attachments: Attachment[]): any[] | null {
|
export function formatAttachments(attachments: Attachment[]): any[] | null {
|
||||||
if (attachments.length === 0) return null;
|
if (attachments.length === 0) return null;
|
||||||
@@ -506,7 +562,10 @@ export function cipherToResponse(
|
|||||||
): CipherResponse {
|
): CipherResponse {
|
||||||
// Strip internal-only fields that must not appear in the API response
|
// Strip internal-only fields that must not appear in the API response
|
||||||
const { userId, createdAt, updatedAt, archivedAt, deletedAt, ...passthrough } = cipher;
|
const { userId, createdAt, updatedAt, archivedAt, deletedAt, ...passthrough } = cipher;
|
||||||
const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null);
|
const responseCipherKey = LIMITS.compatibility.cipherKeyEncryptionFeatureEnabled
|
||||||
|
? optionalEncString(cipher.key)
|
||||||
|
: null;
|
||||||
|
const normalizedLogin = normalizeCipherLoginForCompatibility((passthrough as any).login ?? null, !!responseCipherKey);
|
||||||
const normalizedCard = sanitizeEncryptedObject((passthrough as any).card ?? null, ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code']);
|
const normalizedCard = sanitizeEncryptedObject((passthrough as any).card ?? null, ['cardholderName', 'brand', 'number', 'expMonth', 'expYear', 'code']);
|
||||||
const normalizedIdentity = sanitizeEncryptedObject((passthrough as any).identity ?? null, [
|
const normalizedIdentity = sanitizeEncryptedObject((passthrough as any).identity ?? null, [
|
||||||
'title',
|
'title',
|
||||||
@@ -529,6 +588,9 @@ export function cipherToResponse(
|
|||||||
'licenseNumber',
|
'licenseNumber',
|
||||||
]);
|
]);
|
||||||
const normalizedSshKey = normalizeCipherSshKeyForCompatibility((passthrough as any).sshKey ?? null);
|
const normalizedSshKey = normalizeCipherSshKeyForCompatibility((passthrough as any).sshKey ?? null);
|
||||||
|
const normalizedSecureNote = Number(cipher.type) === 2
|
||||||
|
? normalizeCipherSecureNoteForCompatibility((passthrough as any).secureNote ?? null) ?? { type: 0 }
|
||||||
|
: null;
|
||||||
const responseAttachments = applyCipherEmbeddedAttachmentMetadata(cipher, attachments);
|
const responseAttachments = applyCipherEmbeddedAttachmentMetadata(cipher, attachments);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -557,10 +619,11 @@ export function cipherToResponse(
|
|||||||
login: normalizedLogin,
|
login: normalizedLogin,
|
||||||
card: normalizedCard,
|
card: normalizedCard,
|
||||||
identity: normalizedIdentity,
|
identity: normalizedIdentity,
|
||||||
|
secureNote: normalizedSecureNote,
|
||||||
fields: normalizeCipherFieldsForCompatibility((passthrough as any).fields),
|
fields: normalizeCipherFieldsForCompatibility((passthrough as any).fields),
|
||||||
passwordHistory: normalizePasswordHistoryForCompatibility((passthrough as any).passwordHistory),
|
passwordHistory: normalizePasswordHistoryForCompatibility((passthrough as any).passwordHistory),
|
||||||
sshKey: normalizedSshKey,
|
sshKey: normalizedSshKey,
|
||||||
key: optionalEncString(cipher.key),
|
key: responseCipherKey,
|
||||||
encryptedFor: (passthrough as any).encryptedFor ?? null,
|
encryptedFor: (passthrough as any).encryptedFor ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -653,6 +716,10 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
const createSshKey = readCipherProp<CipherSshKey | null>(cipherData, ['sshKey', 'SshKey']);
|
const createSshKey = readCipherProp<CipherSshKey | null>(cipherData, ['sshKey', 'SshKey']);
|
||||||
const createPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']);
|
const createPasswordHistory = readCipherProp<PasswordHistory[] | null>(cipherData, ['passwordHistory', 'PasswordHistory']);
|
||||||
|
|
||||||
|
if (createKey.present && !shouldAcceptCipherKey(createKey.value)) {
|
||||||
|
return errorResponse('Cipher key encryption is not supported by this server. Resync the client and try again.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
const now = new Date().toISOString();
|
const now = new Date().toISOString();
|
||||||
// Opaque passthrough: spread ALL client fields to preserve unknown/future ones,
|
// Opaque passthrough: spread ALL client fields to preserve unknown/future ones,
|
||||||
// then override only server-controlled fields.
|
// then override only server-controlled fields.
|
||||||
@@ -670,7 +737,7 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
|||||||
deletedAt: null,
|
deletedAt: null,
|
||||||
};
|
};
|
||||||
cipher.folderId = createFolderId.present ? normalizeOptionalId(createFolderId.value) : normalizeOptionalId(cipher.folderId);
|
cipher.folderId = createFolderId.present ? normalizeOptionalId(createFolderId.value) : normalizeOptionalId(cipher.folderId);
|
||||||
cipher.key = createKey.present ? (createKey.value ?? null) : (cipher.key ?? null);
|
cipher.key = normalizeCipherKeyForStorage(createKey.present ? createKey.value : cipher.key);
|
||||||
cipher.login = createLogin.present ? (createLogin.value ?? null) : (cipher.login ?? null);
|
cipher.login = createLogin.present ? (createLogin.value ?? null) : (cipher.login ?? null);
|
||||||
cipher.card = createCard.present ? (createCard.value ?? null) : (cipher.card ?? null);
|
cipher.card = createCard.present ? (createCard.value ?? null) : (cipher.card ?? null);
|
||||||
cipher.identity = createIdentity.present ? (createIdentity.value ?? null) : (cipher.identity ?? null);
|
cipher.identity = createIdentity.present ? (createIdentity.value ?? null) : (cipher.identity ?? null);
|
||||||
@@ -731,6 +798,10 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
const incomingRevisionDate = readCipherRevisionDate(cipherData);
|
const incomingRevisionDate = readCipherRevisionDate(cipherData);
|
||||||
const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData);
|
const hasAttachmentMigrationMetadata = hasIncomingAttachmentMetadata(cipherData);
|
||||||
|
|
||||||
|
if (incomingKey.present && !shouldAcceptCipherKey(incomingKey.value)) {
|
||||||
|
return errorResponse('Cipher key encryption is not supported by this server. Resync the client and try again.', 400);
|
||||||
|
}
|
||||||
|
|
||||||
if (!hasAttachmentMigrationMetadata && isStaleCipherUpdate(existingCipher.updatedAt, incomingRevisionDate)) {
|
if (!hasAttachmentMigrationMetadata && isStaleCipherUpdate(existingCipher.updatedAt, incomingRevisionDate)) {
|
||||||
return errorResponse('The client copy of this cipher is out of date. Resync the client and try again.', 400);
|
return errorResponse('The client copy of this cipher is out of date. Resync the client and try again.', 400);
|
||||||
}
|
}
|
||||||
@@ -756,9 +827,7 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
|||||||
if (incomingFolderId.present) {
|
if (incomingFolderId.present) {
|
||||||
cipher.folderId = normalizeOptionalId(incomingFolderId.value);
|
cipher.folderId = normalizeOptionalId(incomingFolderId.value);
|
||||||
}
|
}
|
||||||
if (incomingKey.present) {
|
cipher.key = normalizeCipherKeyForStorage(incomingKey.present ? incomingKey.value : existingCipher.key);
|
||||||
cipher.key = incomingKey.value ?? null;
|
|
||||||
}
|
|
||||||
cipher.login = nextType === 1 ? (incomingLogin.present ? (incomingLogin.value ?? null) : (existingCipher.login ?? null)) : null;
|
cipher.login = nextType === 1 ? (incomingLogin.present ? (incomingLogin.value ?? null) : (existingCipher.login ?? null)) : null;
|
||||||
cipher.secureNote = nextType === 2 ? (incomingSecureNote.present ? (incomingSecureNote.value ?? null) : (existingCipher.secureNote ?? null)) : null;
|
cipher.secureNote = nextType === 2 ? (incomingSecureNote.present ? (incomingSecureNote.value ?? null) : (existingCipher.secureNote ?? null)) : null;
|
||||||
cipher.card = nextType === 3 ? (incomingCard.present ? (incomingCard.value ?? null) : (existingCipher.card ?? null)) : null;
|
cipher.card = nextType === 3 ? (incomingCard.present ? (incomingCard.value ?? null) : (existingCipher.card ?? null)) : null;
|
||||||
|
|||||||
@@ -11,14 +11,15 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.input {
|
.input {
|
||||||
@apply h-12 w-full rounded-xl border px-3.5 py-2.5 text-base text-ink outline-none transition;
|
@apply h-12 w-full rounded-xl border px-3.5 py-2.5 text-base leading-normal text-ink outline-none transition;
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
border-color: rgba(74, 103, 150, 0.34);
|
border-color: rgba(74, 103, 150, 0.34);
|
||||||
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
|
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.82);
|
||||||
}
|
}
|
||||||
|
|
||||||
select.input {
|
select.input {
|
||||||
@apply pr-[42px];
|
@apply py-0 pr-[42px];
|
||||||
|
line-height: 1.5;
|
||||||
appearance: none;
|
appearance: none;
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-appearance: none;
|
-moz-appearance: none;
|
||||||
|
|||||||
@@ -898,6 +898,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-module select.input {
|
.settings-module select.input {
|
||||||
|
padding-top: 0;
|
||||||
|
padding-bottom: 0;
|
||||||
padding-right: 30px;
|
padding-right: 30px;
|
||||||
background-position:
|
background-position:
|
||||||
calc(100% - 15px) calc(50% - 3px),
|
calc(100% - 15px) calc(50% - 3px),
|
||||||
|
|||||||
Reference in New Issue
Block a user