feat: add server-side encryption for address and phone

This commit is contained in:
openclaw 2026-03-04 18:02:42 +00:00
parent 40042eb76c
commit d08e6f8a17
6 changed files with 65 additions and 2 deletions

View file

@ -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

View file

@ -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

View file

@ -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({

View file

@ -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;

View file

@ -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, () => {

View file

@ -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');
};