Hoppa till huvudinnehåll

Modul 13: Kryptografi och Lösenordssäkerhet

Kryptografi och Lösenordssäkerhet 🔐

Ditt uppdrag 🎯

Förbättra säkerheten i ditt autentiseringssystem:

  • Förstå varför klartext-lösenord är farligt
  • Implementera säker password hashing med Node.js inbyggda crypto
  • Lära dig om salting och varför det behövs
  • Skydda mot vanliga lösenordsattacker

Kritiskt: Detta handlar om att skydda användarnas mest känsliga data. Ett misstag kan exponera tusentals lösenord!

Varning: Säkerhet är INTE valfritt! 🚨

Att spara lösenord i klartext är en av de värsta säkerhetsfelen en utvecklare kan göra. Det är olagligt i många länder och kan förstöra företag och liv! Hittills har du sparat lösenorden i "plaintext" i databasen. Nu ska vi kryptera dem ordentligt!

Fas 1: Förstå problemet med klartext 🧠

Steg 1: Varför är klartext farligt?

Scenario: Din databas läcker (det händer OFTA)

Med klartext-lösenord:

SELECT * FROM users;
-- anna | anna123
-- erik | password
-- lisa | erik456

Vad kan en hacker göra med denna data?

  1. Logga in som vilken användare som helst
  2. Testa samma lösenord på andra sajter
  3. Sälja lösenorden på dark web
  4. Utpressa användarna

Verkliga exempel:

  • LinkedIn (2012): 6.5 miljoner lösenord läckta
  • Adobe (2013): 150 miljoner lösenord i klartext
  • Yahoo (2014): 3 miljarder konton komprometterade

Steg 2: Vad är hashing?

Grundkonceptet:

Lösenord → Hash Function → Hash
"hello123" → PBKDF2 → "2c70e12b7a0646f92279f427c7b38e7334d8e5389cff167a1dc30e73f826b683"

Viktiga egenskaper:

  • Enkelriktat: Omöjligt att få tillbaka originalet
  • Deterministiskt: Samma input ger alltid samma output
  • Långsam: Designad för att ta tid (skyddar mot brute force)
  • Lawine-effekt: Liten ändring ger helt olika hash

Steg 3: Problemet med enkla hashes

Även hashade lösenord har problem:

Problem 1: Rainbow Tables

Förberäknade hash-tabeller:
"password" → "5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8"
"123456" → "ef92b778bafe771e89245b89ecbc08a44a4e166c06659911881f383d4473e94f"

Problem 2: Identiska lösenord

-- Två användare med samma lösenord får samma hash
anna | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
erik | 5e884898da28047151d0e56f8dc6292773603d0d6aabbdd62a11ef721d1542d8
-- En hacker ser att de har samma lösenord!

Detta är varför vi behöver salt!

Fas 2: Introduktion till Salt 🧂

Steg 1: Vad är salt?

Salt = slumpmässig data som läggs till lösenordet före hashing

Utan salt:
"password" → hash → "5e884898da28..."

Med salt:
"password" + "randomsalt123" → hash → "7d4f9a2b8c1e..."
"password" + "anothersalt456" → hash → "9a3b7c4d5e2f..."

Resultat: Samma lösenord får olika hashes!

Så fungerar det: Ett "salt" är en slumpmässig sekvens som läggs till lösenordet innan det hashas. Detta förhindrar att två identiska lösenord ger samma hash och skyddar mot så kallade "rainbow table"-attacker.

Registrering:

  1. Användaren skapar lösenord: "mypassword"
  2. Systemet genererar unikt salt: "abc123xyz"
  3. Kombinera: "mypassword" + "abc123xyz"
  4. Hasha kombinationen: "9f4e2d..."
  5. Spara i databas: salt="abc123xyz", hash="9f4e2d..."

Inloggning:

  1. Användaren skriver lösenord: "mypassword"
  2. Systemet hämtar sparad salt: "abc123xyz"
  3. Kombinera igen: "mypassword" + "abc123xyz"
  4. Hasha: "9f4e2d..."
  5. Jämför med sparad hash

Steg 2: Exempel med statistik

Ett exempel är att med samma krypteringsteknik, utan salt, kommer lösenordet Anders65 se likadant ut i databasen även om det är krypterat. En hacker kan då använda statistik som att 25% av alla som heter Anders och är födda 65 använder Anders65 som lösenord. Vidare kanske 20% av användarna heter Anders och är födda 65. Då vet hackern att om drygt 5% av lösenorden i sin krypterade form ser likadana ut är det rimligt att testa Anders65 som lösenord för de användarna.

Detta undviker vi med hjälp av ett "salt" som ser till att alla som använder lösenordet Anders65 får unika "hashar" när lösenorden krypterats.

Steg 3: Designa din datamodell

Uppdatera din User-modell:

model User {
id String @id @default(uuid())
username String @unique
salt String // Unikt för varje användare
hash String // Lösenord + salt, hashat
createdAt DateTime @default(now())
}

Migrera din databas:

npx prisma db push

Fas 3: Implementera säker hashing med Node.js crypto 🔧

Steg 1: Skapa hash-funktioner

Använd Node.js inbyggda crypto-bibliotek:

import * as crypto from "node:crypto";

// Function to generate a new salt and hash a password
function hashPassword(password: string): { salt: string; hash: string } {
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return { salt, hash };
}

// Function to validate a password against a stored salt and hash
function validatePassword(inputPassword: string, storedSalt: string, storedHash: string): boolean {
const hash = crypto.pbkdf2Sync(inputPassword, storedSalt, 10000, 64, 'sha512').toString('hex');
return storedHash === hash;
}

Viktiga parametrar att förstå:

  • crypto.randomBytes(16): Genererar 16 bytes slumpmässig data för salt
  • pbkdf2Sync: Password-Based Key Derivation Function 2
  • 10000 iterationer: Hur många gånger hashing körs (högre = säkrare men långsammare)
  • 64 bytes: Längden på den resulterande hashen
  • 'sha512': Hash-algoritmen som används

Steg 2: Uppdatera registrering

Uppdatera din register-action i src/routes/login/+page.server.ts:

import * as crypto from "node:crypto";

// Lägg till hash-funktionerna
function hashPassword(password: string): { salt: string; hash: string } {
const salt = crypto.randomBytes(16).toString('hex');
const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512').toString('hex');
return { salt, hash };
}

function validatePassword(inputPassword: string, storedSalt: string, storedHash: string): boolean {
const hash = crypto.pbkdf2Sync(inputPassword, storedSalt, 10000, 64, 'sha512').toString('hex');
return storedHash === hash;
}

export const actions: Actions = {
register: async ({ request, cookies }) => {
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();

// Validering
if (!username || !password) {
return fail(400, { error: 'Användarnamn och lösenord krävs' });
}

if (username.length < 3) {
return fail(400, { error: 'Användarnamn måste vara minst 3 tecken' });
}

if (password.length < 6) {
return fail(400, { error: 'Lösenord måste vara minst 6 tecken' });
}

// Kontrollera om användaren redan finns
const existingUser = await prisma.user.findUnique({
where: { username }
});

if (existingUser) {
return fail(400, { error: 'Användarnamnet är redan taget' });
}

// Hasha lösenordet säkert
const { salt, hash } = hashPassword(password);

// Skapa användare med säker lagring
const newUser = await prisma.user.create({
data: {
username,
salt: salt,
hash: hash
}
});

// Logga in användaren
cookies.set('userId', newUser.id, {
path: '/',
maxAge: 60 * 60 * 24 * 7,
secure: false, // true i production
httpOnly: true
});

throw redirect(307, '/dashboard');
}
// ... andra actions
};

Steg 3: Uppdatera login-logik

Säker inloggning:

export const actions: Actions = {
// ... register action ovan

login: async ({ request, cookies }) => {
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();

// Validering
if (!username || !password) {
return fail(400, { error: 'Användarnamn och lösenord krävs' });
}

try {
// Hitta användare
const user = await prisma.user.findUnique({
where: { username }
});

if (!user) {
return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' });
}

// Verifiera lösenord med salt och hash
const isValidPassword = validatePassword(password, user.salt, user.hash);

if (!isValidPassword) {
return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' });
}

// Sätt session cookie
cookies.set('userId', user.id, {
path: '/',
maxAge: 60 * 60 * 24 * 7,
secure: false, // true i production
httpOnly: true
});

throw redirect(307, '/dashboard');

} catch (error) {
console.error('Login error:', error);
return fail(500, { error: 'Ett fel uppstod. Försök igen.' });
}
}
};

Steg 4: Testa den säkra implementationen

Verifiera att allt fungerar:

// Din uppgift: Testa registrering och inloggning
// 1. Registrera en ny användare
// 2. Kontrollera i Prisma Studio att lösenordet är hashat
// 3. Logga ut och logga in igen
// 4. Kontrollera att det fungerar

console.log("Testing hash functions:");
const testPassword = "testPassword123";
const { salt, hash } = hashPassword(testPassword);
console.log("Original password:", testPassword);
console.log("Salt:", salt);
console.log("Hash:", hash);

const isValid = validatePassword(testPassword, salt, hash);
console.log("Password validation:", isValid); // Should be true

const isInvalid = validatePassword("wrongPassword", salt, hash);
console.log("Wrong password validation:", isInvalid); // Should be false

Fas 4: Säkerhetsförbättringar 🛡️

Steg 1: Timing-attacker

Problem: Olika svarstider kan avslöja information

// DÅLIGT - avslöjar om användaren finns
const user = await prisma.user.findUnique({ where: { username } });
if (!user) {
return fail(400, { error: 'User not found' }); // Snabbt svar
}

const isValid = validatePassword(password, user.salt, user.hash); // Långsamt
if (!isValid) {
return fail(400, { error: 'Wrong password' }); // Långsamt svar
}

BÄTTRE - konstant tid:

export const actions: Actions = {
login: async ({ request, cookies }) => {
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();

if (!username || !password) {
return fail(400, { error: 'Alla fält måste fyllas i' });
}

const user = await prisma.user.findUnique({ where: { username } });

// Kör alltid hash-beräkning, även för icke-existerande användare
const dummySalt = 'dummysalt123456789abcdef123456789abcdef';
const dummyHash = 'dummyhash123456789abcdef123456789abcdef123456789abcdef123456789abcdef';

const isValidPassword = user
? validatePassword(password, user.salt, user.hash)
: validatePassword(password, dummySalt, dummyHash); // Tar samma tid

if (!user || !isValidPassword) {
return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' }); // Samma felmeddelande
}

// Fortsätt med lyckad inloggning...
cookies.set('userId', user.id, {
path: '/',
maxAge: 60 * 60 * 24 * 7,
secure: false,
httpOnly: true
});

throw redirect(307, '/dashboard');
}
};

Steg 2: Lösenordsstyrka

Implementera lösenordskrav:

function validatePasswordStrength(password: string): string[] {
const errors: string[] = [];

if (password.length < 8) {
errors.push('Lösenordet måste vara minst 8 tecken');
}

if (!/[A-Z]/.test(password)) {
errors.push('Lösenordet måste innehålla minst en stor bokstav');
}

if (!/[a-z]/.test(password)) {
errors.push('Lösenordet måste innehålla minst en liten bokstav');
}

if (!/[0-9]/.test(password)) {
errors.push('Lösenordet måste innehålla minst en siffra');
}

if (!/[!@#$%^&*(),.?":{}|<>]/.test(password)) {
errors.push('Lösenordet måste innehålla minst ett specialtecken');
}

// Vanliga lösenord att undvika
const commonPasswords = ['password', '123456', 'qwerty', 'abc123', 'password123'];
if (commonPasswords.includes(password.toLowerCase())) {
errors.push('Detta lösenord är för vanligt och osäkert');
}

return errors;
}

// Använd i register-action:
export const actions: Actions = {
register: async ({ request, cookies }) => {
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();

if (!username || !password) {
return fail(400, { error: 'Alla fält måste fyllas i' });
}

// Validera lösenordsstyrka
const passwordErrors = validatePasswordStrength(password);
if (passwordErrors.length > 0) {
return fail(400, { error: passwordErrors.join('. ') });
}

// Fortsätt med resten av registreringen...
}
};

Steg 3: Rate limiting

Skydda mot brute force-attacker:

// Enkel in-memory rate limiting (för production: använd databas eller Redis)
const failedAttempts = new Map<string, { count: number, lastAttempt: Date }>();

export const actions: Actions = {
login: async ({ request, getClientAddress }) => {
const clientIP = getClientAddress();

// Kontrollera rate limit
const attempts = failedAttempts.get(clientIP);
if (attempts && attempts.count >= 5) {
const timeSinceLastAttempt = Date.now() - attempts.lastAttempt.getTime();
if (timeSinceLastAttempt < 15 * 60 * 1000) { // 15 minuter
return fail(429, { error: 'För många inloggningsförsök. Försök igen om 15 minuter.' });
} else {
// Reset efter timeout
failedAttempts.delete(clientIP);
}
}

const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();

// ... vanlig login-logik ...

// Om inloggning misslyckas, räkna försök
if (!user || !isValidPassword) {
const current = failedAttempts.get(clientIP) || { count: 0, lastAttempt: new Date() };
failedAttempts.set(clientIP, {
count: current.count + 1,
lastAttempt: new Date()
});

return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' });
} else {
// Nollställ vid lyckad inloggning
failedAttempts.delete(clientIP);

// Fortsätt med lyckad inloggning...
}
}
};

Fas 5: Migration från klartext 🔄

Steg 1: Hantera befintliga användare

Om du redan har användare med klartext-lösenord:

// Migrationsstrategi - håll båda under övergång
model User {
id String @id @default(uuid())
username String @unique

// Gamla systemet (kommer tas bort)
password String?

// Nya systemet
salt String?
hash String?

createdAt DateTime @default(now())
}

Steg 2: Gradvis migration

Konvertera användare vid nästa inloggning:

export const actions: Actions = {
login: async ({ request, cookies }) => {
const data = await request.formData();
const username = data.get('username')?.toString();
const password = data.get('password')?.toString();

const user = await prisma.user.findUnique({ where: { username } });

if (!user) {
return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' });
}

// Kontrollera om användaren fortfarande har klartext-lösenord
if (user.password && !user.hash) {
// Gammal användare med klartext-lösenord
if (user.password === password) {
// Konvertera till säkert format
const { salt, hash } = hashPassword(password);

await prisma.user.update({
where: { id: user.id },
data: {
salt: salt,
hash: hash,
password: null // Ta bort klartext
}
});

// Logga in användaren
cookies.set('userId', user.id, {
path: '/',
maxAge: 60 * 60 * 24 * 7,
secure: false,
httpOnly: true
});

throw redirect(307, '/dashboard');
} else {
return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' });
}
} else if (user.hash && user.salt) {
// Ny användare med säkert lösenord
const isValid = validatePassword(password, user.salt, user.hash);
if (isValid) {
cookies.set('userId', user.id, {
path: '/',
maxAge: 60 * 60 * 24 * 7,
secure: false,
httpOnly: true
});

throw redirect(307, '/dashboard');
} else {
return fail(400, { error: 'Ogiltigt användarnamn eller lösenord' });
}
} else {
return fail(400, { error: 'Kontoproblem. Kontakta support.' });
}
}
};

Utmaningar att lösa själv 🎯

Grundnivå:

  1. Implementera salt + hash för alla lösenord ✓
  2. Migrera befintliga användare från klartext ✓
  3. Lösenordsstyrka-validering

Mellannivå:

  1. Rate limiting för inloggningsförsök ✓
  2. Säker password reset via console.log "email"
  3. Timing attack protection

Expertnivå:

  1. Password compromise detection (kolla mot lista över läckta lösenord)
  2. Advanced threat protection (geolocation checks)
  3. Security audit logging (logga alla säkerhetshändelser)

Säkerhetschecklista ✅

✅ Lösenordshantering:

  • Aldrig spara lösenord i klartext
  • Unikt salt för varje användare
  • Minst 10,000 PBKDF2-iterationer
  • Säker hash-algoritm (SHA-512)

✅ Attacker-skydd:

  • Rate limiting mot brute force
  • Konstant tid för alla operationer
  • Samma felmeddelanden för olika fel
  • Lösenordsstyrka-krav

✅ Migration och kompatibilitet:

  • Säker migration från klartext
  • Bakåtkompatibilitet under övergång
  • Automatic upgrade vid inloggning

Vanliga misstag att undvika ⚠️

För få iterationer

// DÅLIGT - för snabbt för moderna datorer
crypto.pbkdf2Sync(password, salt, 100, 64, 'sha512');

// BÄTTRE - tar längre tid att knäcka
crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512');

Förutsägbara salts

// DÅLIGT - samma salt för alla
const salt = 'mysalt123';

// DÅLIGT - förutsägbart salt baserat på username
const salt = username + 'salt';

// BRA - verkligt slumpmässigt
const salt = crypto.randomBytes(16).toString('hex');

Information leakage

// DÅLIGT - avslöjar om användare finns
if (!user) return { error: 'User not found' };
if (!validPassword) return { error: 'Wrong password' };

// BÄTTRE - samma meddelande
return { error: 'Ogiltigt användarnamn eller lösenord' };

Synkron crypto i production

// OKEJ för utveckling
const hash = crypto.pbkdf2Sync(password, salt, 10000, 64, 'sha512');

// BÄTTRE för production - blockar inte event loop
const hash = await new Promise((resolve, reject) => {
crypto.pbkdf2(password, salt, 10000, 64, 'sha512', (err, derivedKey) => {
if (err) reject(err);
else resolve(derivedKey.toString('hex'));
});
});

Reflektion och lärande 🤔

Efter implementation, reflektera:

Säkerhetsförståelse:

  • Varför är salting så kritiskt för säkerhet?
  • Vad händer om din hash-algoritm blir komprometterad?
  • Hur balanserar du säkerhet mot prestanda?

Implementation-reflektion:

  • Vilka var de svåraste delarna att implementera?
  • Hur testade du att din säkra implementation fungerar?
  • Vad skulle du göra annorlunda nästa gång?

Hotmodell-reflektion:

  • Vilka attacker skyddar ditt system mot?
  • Vilka attacker är du fortfarande sårbar för?
  • Hur skulle du upptäcka om systemet blir komprometterat?

Grattis! 🎉

Du har nu implementerat:

  • Säker lösenordshantering med salt och hash
  • Skydd mot vanliga attacker (rainbow tables, timing, brute force)
  • Industristandard kryptografi med Node.js crypto
  • Graceful migration från osäker till säker lagring
  • Production-ready säkerhet för riktiga applikationer

Dessa skills är direkt överförbara till:

  • Enterprise-applikationer
  • Finansiella system
  • Hälsovård och medicin
  • Alla system som hanterar användardata
Du förstår kryptografi! 🔐

Säker lösenordshantering är en av de viktigaste färdigheterna en webbutvecklare kan ha. Du kan nu skydda användardata på samma sätt som stora företag gör!

Nästa steg 🚀

Nu när du har implementerat säker kryptografi är nästa modul om layout groups och advanced routing - att organisera din app för både inloggade och utloggade användare med professionell struktur!

Resurser för fördjupning 📚