diff --git a/README.md b/README.md index 960be80..e1c611a 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ Erster funktionaler Backend-Stand für die Vision: - Datenmodell inkl. postalischer Adress-Verifikation (`backend/sql/schema.sql`) - Address-Change-Flow mit Briefcode (`/addresses/change-request`, `/addresses/verify`) - Kontaktdatenaustausch nach Deal (`/contacts/request`, `/contacts/respond`, `/contacts/deal/:dealId`) +- Serverseitige AES-256-GCM-Verschlüsselung für Adresse/Telefon (`DATA_ENCRYPTION_KEY`) ## Start diff --git a/backend/.env.example b/backend/.env.example index b73887f..f01b507 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -5,3 +5,4 @@ DB_NAME=helpyourneighbour DB_USER=helpyourneighbour DB_PASSWORD=change-me JWT_SECRET=change-me-super-secret +DATA_ENCRYPTION_KEY=base64-32-byte-key diff --git a/backend/src/routes/addresses.js b/backend/src/routes/addresses.js index 2c50115..dfca5dc 100644 --- a/backend/src/routes/addresses.js +++ b/backend/src/routes/addresses.js @@ -3,13 +3,14 @@ import { createHash, randomInt } from 'crypto'; import { z } from 'zod'; import { pool } from '../db/connection.js'; import { requireAuth } from '../middleware/auth.js'; +import { encryptText } from '../services/encryption.js'; const router = Router(); const hashCode = (code) => createHash('sha256').update(code).digest('hex'); router.post('/change-request', requireAuth, async (req, res) => { - const parsed = z.object({ newAddressEncrypted: z.string().min(10) }).safeParse(req.body); + const parsed = z.object({ newAddress: z.string().min(10) }).safeParse(req.body); if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); const verificationCode = String(randomInt(100000, 999999)); @@ -18,7 +19,7 @@ router.post('/change-request', requireAuth, async (req, res) => { const [result] = await pool.query( `INSERT INTO address_change_requests (user_id, new_address_encrypted, verification_code_hash) VALUES (?, ?, ?)`, - [req.user.userId, parsed.data.newAddressEncrypted, verificationCodeHash] + [req.user.userId, encryptText(parsed.data.newAddress), verificationCodeHash] ); res.status(201).json({ diff --git a/backend/src/routes/profile.js b/backend/src/routes/profile.js new file mode 100644 index 0000000..caa8dad --- /dev/null +++ b/backend/src/routes/profile.js @@ -0,0 +1,19 @@ +import { Router } from 'express'; +import { z } from 'zod'; +import { pool } from '../db/connection.js'; +import { requireAuth } from '../middleware/auth.js'; +import { encryptText } from '../services/encryption.js'; + +const router = Router(); + +router.post('/phone', requireAuth, async (req, res) => { + const parsed = z.object({ phone: z.string().min(6).max(40) }).safeParse(req.body); + if (!parsed.success) return res.status(400).json({ error: parsed.error.flatten() }); + + const encryptedPhone = encryptText(parsed.data.phone); + await pool.query('UPDATE users SET phone_encrypted = ? WHERE id = ?', [encryptedPhone, req.user.userId]); + + res.json({ status: 'updated' }); +}); + +export default router; diff --git a/backend/src/server.js b/backend/src/server.js index 4667534..8e91e75 100644 --- a/backend/src/server.js +++ b/backend/src/server.js @@ -6,6 +6,7 @@ import offerRoutes from './routes/offers.js'; import reviewRoutes from './routes/reviews.js'; import addressRoutes from './routes/addresses.js'; import contactRoutes from './routes/contacts.js'; +import profileRoutes from './routes/profile.js'; dotenv.config(); @@ -20,6 +21,7 @@ app.use('/offers', offerRoutes); app.use('/reviews', reviewRoutes); app.use('/addresses', addressRoutes); app.use('/contacts', contactRoutes); +app.use('/profile', profileRoutes); const port = Number(process.env.PORT || 3000); app.listen(port, () => { diff --git a/backend/src/services/encryption.js b/backend/src/services/encryption.js new file mode 100644 index 0000000..07b67fb --- /dev/null +++ b/backend/src/services/encryption.js @@ -0,0 +1,39 @@ +import { createCipheriv, createDecipheriv, randomBytes } from 'crypto'; + +const ALGO = 'aes-256-gcm'; + +const getKey = () => { + const key = process.env.DATA_ENCRYPTION_KEY; + if (!key) throw new Error('DATA_ENCRYPTION_KEY is not set'); + + const keyBuf = Buffer.from(key, 'base64'); + if (keyBuf.length !== 32) throw new Error('DATA_ENCRYPTION_KEY must be base64-encoded 32 bytes'); + return keyBuf; +}; + +export const encryptText = (plainText) => { + const iv = randomBytes(12); + const key = getKey(); + const cipher = createCipheriv(ALGO, key, iv); + + const encrypted = Buffer.concat([cipher.update(plainText, 'utf8'), cipher.final()]); + const tag = cipher.getAuthTag(); + + return `${iv.toString('base64')}:${tag.toString('base64')}:${encrypted.toString('base64')}`; +}; + +export const decryptText = (payload) => { + const [ivB64, tagB64, dataB64] = payload.split(':'); + if (!ivB64 || !tagB64 || !dataB64) throw new Error('Invalid encrypted payload format'); + + const key = getKey(); + const decipher = createDecipheriv(ALGO, key, Buffer.from(ivB64, 'base64')); + decipher.setAuthTag(Buffer.from(tagB64, 'base64')); + + const decrypted = Buffer.concat([ + decipher.update(Buffer.from(dataB64, 'base64')), + decipher.final() + ]); + + return decrypted.toString('utf8'); +};