Basic success

This commit is contained in:
shuaiplus
2026-02-03 22:56:42 +08:00
commit da307c79cd
27 changed files with 5639 additions and 0 deletions
+176
View File
@@ -0,0 +1,176 @@
import { JWTPayload } from '../types';
// Base64 URL encode
function base64UrlEncode(data: Uint8Array): string {
const base64 = btoa(String.fromCharCode(...data));
return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
// Base64 URL decode
function base64UrlDecode(str: string): Uint8Array {
str = str.replace(/-/g, '+').replace(/_/g, '/');
while (str.length % 4) str += '=';
const binary = atob(str);
const bytes = new Uint8Array(binary.length);
for (let i = 0; i < binary.length; i++) {
bytes[i] = binary.charCodeAt(i);
}
return bytes;
}
// Create JWT
export async function createJWT(payload: Omit<JWTPayload, 'iat' | 'exp' | 'iss' | 'premium' | 'email_verified' | 'amr'>, secret: string, expiresIn: number = 7200): Promise<string> {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const fullPayload: JWTPayload = {
...payload,
email_verified: true, // required by mobile client
amr: ['Application'], // authentication methods reference - required by mobile client
iat: now,
exp: now + expiresIn,
iss: 'nodewarden',
premium: true,
};
const encoder = new TextEncoder();
const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));
const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(fullPayload)));
const data = `${headerB64}.${payloadB64}`;
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
return `${data}.${signatureB64}`;
}
// Verify JWT
export async function verifyJWT(token: string, secret: string): Promise<JWTPayload | null> {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const [headerB64, payloadB64, signatureB64] = parts;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const data = `${headerB64}.${payloadB64}`;
const signature = base64UrlDecode(signatureB64);
const valid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
if (!valid) return null;
const payload: JWTPayload = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64)));
// Check expiration
const now = Math.floor(Date.now() / 1000);
if (payload.exp < now) return null;
return payload;
} catch {
return null;
}
}
// Create refresh token (simple random string)
export function createRefreshToken(): string {
const bytes = new Uint8Array(32);
crypto.getRandomValues(bytes);
return base64UrlEncode(bytes);
}
// File download token payload
export interface FileDownloadClaims {
cipherId: string;
attachmentId: string;
exp: number;
}
// Create file download token (short-lived, 5 minutes)
export async function createFileDownloadToken(
cipherId: string,
attachmentId: string,
secret: string
): Promise<string> {
const header = { alg: 'HS256', typ: 'JWT' };
const now = Math.floor(Date.now() / 1000);
const payload: FileDownloadClaims = {
cipherId,
attachmentId,
exp: now + 300, // 5 minutes
};
const encoder = new TextEncoder();
const headerB64 = base64UrlEncode(encoder.encode(JSON.stringify(header)));
const payloadB64 = base64UrlEncode(encoder.encode(JSON.stringify(payload)));
const data = `${headerB64}.${payloadB64}`;
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['sign']
);
const signature = await crypto.subtle.sign('HMAC', key, encoder.encode(data));
const signatureB64 = base64UrlEncode(new Uint8Array(signature));
return `${data}.${signatureB64}`;
}
// Verify file download token
export async function verifyFileDownloadToken(
token: string,
secret: string
): Promise<FileDownloadClaims | null> {
try {
const parts = token.split('.');
if (parts.length !== 3) return null;
const [headerB64, payloadB64, signatureB64] = parts;
const encoder = new TextEncoder();
const key = await crypto.subtle.importKey(
'raw',
encoder.encode(secret),
{ name: 'HMAC', hash: 'SHA-256' },
false,
['verify']
);
const data = `${headerB64}.${payloadB64}`;
const signature = base64UrlDecode(signatureB64);
const valid = await crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
if (!valid) return null;
const payload: FileDownloadClaims = JSON.parse(new TextDecoder().decode(base64UrlDecode(payloadB64)));
// Check expiration
const now = Math.floor(Date.now() / 1000);
if (payload.exp < now) return null;
return payload;
} catch {
return null;
}
}
+70
View File
@@ -0,0 +1,70 @@
// JSON response helper
export function jsonResponse(data: any, status: number = 200, headers: Record<string, string> = {}): Response {
return new Response(JSON.stringify(data), {
status,
headers: {
'Content-Type': 'application/json',
...getCorsHeaders(),
...headers,
},
});
}
// Error response helper
export function errorResponse(message: string, status: number = 400): Response {
return jsonResponse(
{
error: message,
error_description: message,
ErrorModel: {
Message: message,
Object: 'error',
},
},
status
);
}
// Identity endpoint error response (for /identity/connect/token)
export function identityErrorResponse(message: string, error: string = 'invalid_grant', status: number = 400): Response {
return jsonResponse(
{
error: error,
error_description: message,
ErrorModel: {
Message: message,
Object: 'error',
},
},
status
);
}
// CORS headers
export function getCorsHeaders(): Record<string, string> {
return {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization, Accept, Device-Type, Bitwarden-Client-Name, Bitwarden-Client-Version',
'Access-Control-Max-Age': '86400',
};
}
// Handle CORS preflight
export function handleCors(): Response {
return new Response(null, {
status: 204,
headers: getCorsHeaders(),
});
}
// HTML response helper
export function htmlResponse(html: string, status: number = 200): Response {
return new Response(html, {
status,
headers: {
'Content-Type': 'text/html; charset=utf-8',
...getCorsHeaders(),
},
});
}
+4
View File
@@ -0,0 +1,4 @@
// Generate UUID v4
export function generateUUID(): string {
return crypto.randomUUID();
}