mirror of
https://github.com/shuaiplus/nodewarden.git
synced 2026-06-20 21:00:41 +00:00
enhance cipher and identity handling with new fields and rate limit adjustments
This commit is contained in:
@@ -97,6 +97,7 @@ export async function handleGetProfile(request: Request, env: Env, userId: strin
|
||||
twoFactorEnabled: false,
|
||||
key: user.key,
|
||||
privateKey: user.privateKey,
|
||||
accountKeys: null,
|
||||
securityStamp: user.securityStamp || user.id,
|
||||
organizations: [],
|
||||
providers: [],
|
||||
|
||||
@@ -304,7 +304,7 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
||||
id: cipher.id,
|
||||
organizationId: null,
|
||||
folderId: cipher.folderId,
|
||||
type: cipher.type,
|
||||
type: Number(cipher.type) || 1,
|
||||
name: cipher.name,
|
||||
notes: cipher.notes,
|
||||
favorite: cipher.favorite,
|
||||
@@ -312,6 +312,7 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
||||
card: cipher.card,
|
||||
identity: cipher.identity,
|
||||
secureNote: cipher.secureNote,
|
||||
sshKey: cipher.sshKey,
|
||||
fields: cipher.fields,
|
||||
passwordHistory: cipher.passwordHistory,
|
||||
reprompt: cipher.reprompt,
|
||||
@@ -319,9 +320,13 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
||||
creationDate: cipher.createdAt,
|
||||
revisionDate: cipher.updatedAt,
|
||||
deletedDate: cipher.deletedAt,
|
||||
archivedDate: null,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
permissions: null,
|
||||
permissions: {
|
||||
delete: true,
|
||||
restore: true,
|
||||
},
|
||||
object: 'cipher',
|
||||
collectionIds: [],
|
||||
attachments: attachments.length > 0 ? attachments.map(a => ({
|
||||
@@ -330,8 +335,11 @@ function formatCipherResponse(cipher: Cipher, attachments: Attachment[]): any {
|
||||
size: String(a.size),
|
||||
sizeName: a.sizeName,
|
||||
key: a.key,
|
||||
url: null,
|
||||
object: 'attachment',
|
||||
})) : null,
|
||||
key: cipher.key,
|
||||
encryptedFor: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+16
-6
@@ -13,6 +13,7 @@ function formatAttachments(attachments: Attachment[]): any[] | null {
|
||||
size: String(a.size),
|
||||
sizeName: a.sizeName,
|
||||
key: a.key,
|
||||
url: null,
|
||||
object: 'attachment',
|
||||
}));
|
||||
}
|
||||
@@ -23,7 +24,7 @@ function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): Ciphe
|
||||
id: cipher.id,
|
||||
organizationId: null,
|
||||
folderId: cipher.folderId,
|
||||
type: cipher.type,
|
||||
type: Number(cipher.type) || 1,
|
||||
name: cipher.name,
|
||||
notes: cipher.notes,
|
||||
favorite: cipher.favorite,
|
||||
@@ -31,6 +32,7 @@ function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): Ciphe
|
||||
card: cipher.card,
|
||||
identity: cipher.identity,
|
||||
secureNote: cipher.secureNote,
|
||||
sshKey: cipher.sshKey,
|
||||
fields: cipher.fields,
|
||||
passwordHistory: cipher.passwordHistory,
|
||||
reprompt: cipher.reprompt,
|
||||
@@ -38,16 +40,18 @@ function cipherToResponse(cipher: Cipher, attachments: Attachment[] = []): Ciphe
|
||||
creationDate: cipher.createdAt,
|
||||
revisionDate: cipher.updatedAt,
|
||||
deletedDate: cipher.deletedAt,
|
||||
archivedDate: null,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
permissions: {
|
||||
delete: true,
|
||||
restore: true,
|
||||
edit: true,
|
||||
},
|
||||
object: 'cipher',
|
||||
collectionIds: [],
|
||||
attachments: formatAttachments(attachments),
|
||||
key: cipher.key,
|
||||
encryptedFor: null,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -103,13 +107,14 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
||||
}
|
||||
|
||||
// Handle nested cipher object (from some clients)
|
||||
const cipherData = body.cipher || body;
|
||||
// Android client sends PascalCase "Cipher" for organization ciphers
|
||||
const cipherData = body.Cipher || body.cipher || body;
|
||||
|
||||
const now = new Date().toISOString();
|
||||
const cipher: Cipher = {
|
||||
id: generateUUID(),
|
||||
userId: userId,
|
||||
type: cipherData.type,
|
||||
type: Number(cipherData.type) || 1,
|
||||
folderId: cipherData.folderId || null,
|
||||
name: cipherData.name,
|
||||
notes: cipherData.notes || null,
|
||||
@@ -118,9 +123,11 @@ export async function handleCreateCipher(request: Request, env: Env, userId: str
|
||||
card: cipherData.card || null,
|
||||
identity: cipherData.identity || null,
|
||||
secureNote: cipherData.secureNote || null,
|
||||
sshKey: cipherData.sshKey || null,
|
||||
fields: cipherData.fields || null,
|
||||
passwordHistory: cipherData.passwordHistory || null,
|
||||
reprompt: cipherData.reprompt || 0,
|
||||
key: cipherData.key || null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
@@ -149,11 +156,12 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
}
|
||||
|
||||
// Handle nested cipher object
|
||||
const cipherData = body.cipher || body;
|
||||
// Android client sends PascalCase "Cipher" for organization ciphers
|
||||
const cipherData = body.Cipher || body.cipher || body;
|
||||
|
||||
const cipher: Cipher = {
|
||||
...existingCipher,
|
||||
type: cipherData.type ?? existingCipher.type,
|
||||
type: Number(cipherData.type) || existingCipher.type,
|
||||
folderId: cipherData.folderId !== undefined ? cipherData.folderId : existingCipher.folderId,
|
||||
name: cipherData.name ?? existingCipher.name,
|
||||
notes: cipherData.notes !== undefined ? cipherData.notes : existingCipher.notes,
|
||||
@@ -162,9 +170,11 @@ export async function handleUpdateCipher(request: Request, env: Env, userId: str
|
||||
card: cipherData.card !== undefined ? cipherData.card : existingCipher.card,
|
||||
identity: cipherData.identity !== undefined ? cipherData.identity : existingCipher.identity,
|
||||
secureNote: cipherData.secureNote !== undefined ? cipherData.secureNote : existingCipher.secureNote,
|
||||
sshKey: cipherData.sshKey !== undefined ? cipherData.sshKey : existingCipher.sshKey,
|
||||
fields: cipherData.fields !== undefined ? cipherData.fields : existingCipher.fields,
|
||||
passwordHistory: cipherData.passwordHistory !== undefined ? cipherData.passwordHistory : existingCipher.passwordHistory,
|
||||
reprompt: cipherData.reprompt ?? existingCipher.reprompt,
|
||||
key: cipherData.key !== undefined ? cipherData.key : existingCipher.key,
|
||||
updatedAt: new Date().toISOString(),
|
||||
};
|
||||
|
||||
|
||||
+20
-11
@@ -31,22 +31,21 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
return errorResponse('Email and password are required', 400);
|
||||
}
|
||||
|
||||
// Check if login is rate limited
|
||||
const loginCheck = await rateLimit.checkLoginAttempt(email);
|
||||
if (!loginCheck.allowed) {
|
||||
return errorResponse(
|
||||
`Too many failed login attempts. Try again in ${Math.ceil(loginCheck.retryAfterSeconds! / 60)} minutes.`,
|
||||
429
|
||||
);
|
||||
}
|
||||
|
||||
const user = await storage.getUser(email);
|
||||
if (!user) {
|
||||
// Record failed attempt even for non-existent user (prevent enumeration)
|
||||
await rateLimit.recordFailedLogin(email);
|
||||
return identityErrorResponse('Username or password is incorrect. Try again', 'invalid_grant', 400);
|
||||
}
|
||||
|
||||
// Check if login is rate limited (only after confirming user exists)
|
||||
const loginCheck = await rateLimit.checkLoginAttempt(email);
|
||||
if (!loginCheck.allowed) {
|
||||
return identityErrorResponse(
|
||||
`Too many failed login attempts. Try again in ${Math.ceil(loginCheck.retryAfterSeconds! / 60)} minutes.`,
|
||||
'TooManyRequests',
|
||||
429
|
||||
);
|
||||
}
|
||||
|
||||
const valid = await auth.verifyPassword(passwordHash, user.masterPasswordHash);
|
||||
if (!valid) {
|
||||
// Record failed login attempt
|
||||
@@ -136,6 +135,16 @@ export async function handleToken(request: Request, env: Env): Promise<Response>
|
||||
UserDecryptionOptions: {
|
||||
HasMasterPassword: true,
|
||||
Object: 'userDecryptionOptions',
|
||||
MasterPasswordUnlock: {
|
||||
Kdf: {
|
||||
KdfType: user.kdfType,
|
||||
Iterations: user.kdfIterations,
|
||||
Memory: user.kdfMemory || null,
|
||||
Parallelism: user.kdfParallelism || null,
|
||||
},
|
||||
MasterKeyEncryptedUserKey: user.key,
|
||||
Salt: user.email.toLowerCase(),
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -134,6 +134,8 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
||||
totp: c.login.totp || null,
|
||||
autofillOnPageLoad: null,
|
||||
fido2Credentials: null,
|
||||
uri: null,
|
||||
passwordRevisionDate: null,
|
||||
} : null,
|
||||
card: c.card ? {
|
||||
cardholderName: c.card.cardholderName || null,
|
||||
@@ -172,6 +174,8 @@ export async function handleCiphersImport(request: Request, env: Env, userId: st
|
||||
})) || null,
|
||||
passwordHistory: c.passwordHistory || null,
|
||||
reprompt: c.reprompt || 0,
|
||||
sshKey: null,
|
||||
key: null,
|
||||
createdAt: now,
|
||||
updatedAt: now,
|
||||
deletedAt: null,
|
||||
|
||||
+19
-2
@@ -11,6 +11,7 @@ function formatAttachments(attachments: Attachment[]): any[] | null {
|
||||
size: String(a.size),
|
||||
sizeName: a.sizeName,
|
||||
key: a.key,
|
||||
url: null,
|
||||
object: 'attachment',
|
||||
}));
|
||||
}
|
||||
@@ -41,6 +42,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
||||
twoFactorEnabled: false,
|
||||
key: user.key,
|
||||
privateKey: user.privateKey,
|
||||
accountKeys: null,
|
||||
securityStamp: user.securityStamp || user.id,
|
||||
organizations: [],
|
||||
providers: [],
|
||||
@@ -59,7 +61,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
||||
id: cipher.id,
|
||||
organizationId: null,
|
||||
folderId: cipher.folderId,
|
||||
type: cipher.type,
|
||||
type: Number(cipher.type) || 1,
|
||||
name: cipher.name,
|
||||
notes: cipher.notes,
|
||||
favorite: cipher.favorite,
|
||||
@@ -67,6 +69,7 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
||||
card: cipher.card,
|
||||
identity: cipher.identity,
|
||||
secureNote: cipher.secureNote,
|
||||
sshKey: cipher.sshKey,
|
||||
fields: cipher.fields,
|
||||
passwordHistory: cipher.passwordHistory,
|
||||
reprompt: cipher.reprompt,
|
||||
@@ -74,16 +77,18 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
||||
creationDate: cipher.createdAt,
|
||||
revisionDate: cipher.updatedAt,
|
||||
deletedDate: cipher.deletedAt,
|
||||
archivedDate: null,
|
||||
edit: true,
|
||||
viewPassword: true,
|
||||
permissions: {
|
||||
delete: true,
|
||||
restore: true,
|
||||
edit: true,
|
||||
},
|
||||
object: 'cipher',
|
||||
collectionIds: [],
|
||||
attachments: formatAttachments(attachments),
|
||||
key: cipher.key,
|
||||
encryptedFor: null,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -107,6 +112,18 @@ export async function handleSync(request: Request, env: Env, userId: string): Pr
|
||||
},
|
||||
policies: [],
|
||||
sends: [],
|
||||
userDecryption: {
|
||||
masterPasswordUnlock: {
|
||||
salt: user.email,
|
||||
kdf: {
|
||||
kdfType: user.kdfType,
|
||||
iterations: user.kdfIterations,
|
||||
memory: user.kdfMemory || null,
|
||||
parallelism: user.kdfParallelism || null,
|
||||
},
|
||||
masterKeyEncryptedUserKey: user.key,
|
||||
},
|
||||
},
|
||||
object: 'sync',
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user