Initial commit - realms platform

This commit is contained in:
doomtube 2026-01-05 22:54:27 -05:00
parent c590ab6d18
commit c717c3751c
234 changed files with 74103 additions and 15231 deletions

View file

@ -4,6 +4,12 @@ const DB_NAME = 'pgp_storage';
const DB_VERSION = 1;
const STORE_NAME = 'encrypted_keys';
// Rate limiting for unlock attempts
let unlockAttempts = 0;
let lockoutUntil = null;
const MAX_ATTEMPTS = 5;
const LOCKOUT_DURATION = 30 * 60 * 1000; // 30 minutes
// Initialize IndexedDB
async function initDB() {
return new Promise((resolve, reject) => {
@ -48,21 +54,23 @@ async function deriveKey(passphrase, salt) {
// Validate passphrase strength
export function validatePassphrase(passphrase) {
if (!passphrase || passphrase.length < 12) {
return 'Passphrase must be at least 12 characters';
if (!passphrase || passphrase.length < 16) {
return 'Passphrase must be at least 16 characters';
}
// Check for complexity
const hasUpper = /[A-Z]/.test(passphrase);
const hasLower = /[a-z]/.test(passphrase);
const hasNumber = /[0-9]/.test(passphrase);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>]/.test(passphrase);
const hasSpecial = /[!@#$%^&*(),.?":{}|<>\-_=+\[\]\\;'`~]/.test(passphrase);
const complexity = [hasUpper, hasLower, hasNumber, hasSpecial].filter(Boolean).length;
if (complexity < 3) {
return 'Passphrase must contain at least 3 of: uppercase, lowercase, numbers, special characters';
// Require all 4 types, OR 20+ characters with 3 types
if (complexity < 4 && !(passphrase.length >= 20 && complexity >= 3)) {
return 'Passphrase must contain uppercase, lowercase, numbers, AND special characters (or be 20+ chars with 3 types)';
}
return null; // Valid
}
@ -165,34 +173,40 @@ export async function unlockPrivateKey(passphrase) {
if (typeof window === 'undefined') {
throw new Error('Storage operations can only be performed in the browser');
}
// Check lockout
if (lockoutUntil && Date.now() < lockoutUntil) {
const remaining = Math.ceil((lockoutUntil - Date.now()) / 60000);
throw new Error(`Too many failed attempts. Try again in ${remaining} minute${remaining !== 1 ? 's' : ''}.`);
}
if (!passphrase) {
throw new Error('Passphrase required');
}
try {
const db = await initDB();
const transaction = db.transaction([STORE_NAME], 'readonly');
const store = transaction.objectStore(STORE_NAME);
const data = await new Promise((resolve, reject) => {
const request = store.get('primary_key');
request.onsuccess = () => resolve(request.result);
request.onerror = () => reject(request.error);
});
if (!data) {
throw new Error('No encrypted key found');
}
// Reconstruct typed arrays
const salt = new Uint8Array(data.salt);
const iv = new Uint8Array(data.iv);
const encrypted = new Uint8Array(data.encrypted);
// Derive decryption key
const key = await deriveKey(passphrase, salt);
// Decrypt
const decrypted = await crypto.subtle.decrypt(
{
@ -202,12 +216,25 @@ export async function unlockPrivateKey(passphrase) {
key,
encrypted
);
// Success - reset attempts
unlockAttempts = 0;
lockoutUntil = null;
const dec = new TextDecoder();
return dec.decode(decrypted);
} catch (error) {
// Track failed attempts
unlockAttempts++;
if (unlockAttempts >= MAX_ATTEMPTS) {
lockoutUntil = Date.now() + LOCKOUT_DURATION;
unlockAttempts = 0;
console.error('Too many failed unlock attempts. Locked out for 30 minutes.');
throw new Error('Too many failed attempts. Locked out for 30 minutes.');
}
console.error('Failed to unlock private key:', error);
throw new Error('Failed to unlock private key. Check your passphrase.');
throw new Error(`Failed to unlock private key. Check your passphrase. (${MAX_ATTEMPTS - unlockAttempts} attempts remaining)`);
}
}