first commit

This commit is contained in:
bryan
2025-12-30 13:53:13 -06:00
commit f0e8d9400a
31 changed files with 12464 additions and 0 deletions

10
Backend/db.js Normal file
View File

@@ -0,0 +1,10 @@
const { Pool } = require('pg');
require('dotenv').config();
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
module.exports = {
query: (text, params) => pool.query(text, params),
};

1313
Backend/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

20
Backend/package.json Normal file
View File

@@ -0,0 +1,20 @@
{
"name": "backend",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"cors": "^2.8.5",
"dotenv": "^17.2.3",
"express": "^5.2.1",
"pg": "^8.16.3",
"redis": "^5.10.0",
"socket.io": "^4.8.3"
}
}

15
Backend/redis.js Normal file
View File

@@ -0,0 +1,15 @@
const { createClient } = require('redis');
require('dotenv').config();
const client = createClient({
url: process.env.REDIS_URL
});
client.on('error', (err) => console.log('Redis Client Error', err));
(async () => {
await client.connect();
console.log('Redis connected');
})();
module.exports = client;

80
Backend/routes/auth.js Normal file
View File

@@ -0,0 +1,80 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
const crypto = require('crypto');
// Helper to generate fake salt for user privacy
function generateFakeSalt(username) {
return crypto.createHmac('sha256', 'SERVER_SECRET_KEY') // In prod, use env var
.update(username)
.digest('hex');
}
router.post('/register', async (req, res) => {
const { username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys } = req.body;
try {
const result = await db.query(
`INSERT INTO users (username, client_salt, encrypted_master_key, hashed_auth_key, public_identity_key, public_signing_key, encrypted_private_keys)
VALUES ($1, $2, $3, $4, $5, $6, $7) RETURNING id`,
[username, salt, encryptedMK, hak, publicKey, signingKey, encryptedPrivateKeys]
);
res.json({ success: true, userId: result.rows[0].id });
} catch (err) {
console.error(err);
if (err.code === '23505') { // Unique violation
res.status(400).json({ error: 'Username taken' });
} else {
res.status(500).json({ error: 'Server error' });
}
}
});
router.post('/login/salt', async (req, res) => {
const { username } = req.body;
try {
const result = await db.query('SELECT client_salt FROM users WHERE username = $1', [username]);
if (result.rows.length > 0) {
res.json({ salt: result.rows[0].client_salt });
} else {
// Return fake salt to prevent enumeration
res.json({ salt: generateFakeSalt(username) });
}
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
router.post('/login/verify', async (req, res) => {
const { username, dak } = req.body;
try {
const result = await db.query(
'SELECT hashed_auth_key, encrypted_master_key, encrypted_private_keys FROM users WHERE username = $1',
[username]
);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const user = result.rows[0];
const hashedDAK = crypto.createHash('sha256').update(dak).digest('hex');
if (hashedDAK === user.hashed_auth_key) {
res.json({
success: true,
userId: user.id,
encryptedMK: user.encrypted_master_key,
encryptedPrivateKeys: user.encrypted_private_keys
});
} else {
res.status(401).json({ error: 'Invalid credentials' });
}
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

View File

@@ -0,0 +1,15 @@
const express = require('express');
const router = express.Router();
const db = require('../db');
router.get('/', async (req, res) => {
try {
const result = await db.query('SELECT * FROM channels ORDER BY name ASC');
res.json(result.rows);
} catch (err) {
console.error(err);
res.status(500).json({ error: 'Server error' });
}
});
module.exports = router;

46
Backend/schema.sql Normal file
View File

@@ -0,0 +1,46 @@
CREATE EXTENSION IF NOT EXISTS "pgcrypto";
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
username TEXT UNIQUE NOT NULL,
client_salt TEXT NOT NULL,
encrypted_master_key TEXT NOT NULL,
hashed_auth_key TEXT NOT NULL,
public_identity_key TEXT NOT NULL,
public_signing_key TEXT NOT NULL,
encrypted_private_keys TEXT NOT NULL, -- Added this column
is_admin BOOLEAN DEFAULT FALSE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS channels (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name TEXT UNIQUE NOT NULL,
type TEXT DEFAULT 'text',
created_at TIMESTAMP DEFAULT NOW()
);
CREATE TABLE IF NOT EXISTS roles (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
permissions JSONB
);
CREATE TABLE IF NOT EXISTS channel_keys (
channel_id UUID NOT NULL,
user_id UUID NOT NULL,
encrypted_key_bundle TEXT NOT NULL,
key_version INTEGER DEFAULT 1,
PRIMARY KEY (channel_id, user_id)
);
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
channel_id UUID NOT NULL,
sender_id UUID NOT NULL,
ciphertext TEXT NOT NULL,
nonce TEXT NOT NULL,
signature TEXT NOT NULL,
key_version INTEGER NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);

View File

@@ -0,0 +1,31 @@
const fs = require('fs');
const path = require('path');
const db = require('../db');
async function initDb() {
try {
const schemaPath = path.join(__dirname, '../schema.sql');
const schemaSql = fs.readFileSync(schemaPath, 'utf8');
console.log('Applying schema...');
await db.query(schemaSql);
// Seed Channels
const channels = ['general', 'random'];
for (const name of channels) {
await db.query(
`INSERT INTO channels (name) VALUES ($1) ON CONFLICT (name) DO NOTHING`,
[name]
);
}
console.log('Channels seeded.');
console.log('Schema applied successfully.');
process.exit(0);
} catch (err) {
console.error('Error applying schema:', err);
process.exit(1);
}
}
initDb();

97
Backend/server.js Normal file
View File

@@ -0,0 +1,97 @@
const express = require('express');
const http = require('http');
const { Server } = require('socket.io');
const cors = require('cors');
require('dotenv').config();
const app = express();
const server = http.createServer(app);
const io = new Server(server, {
cors: {
origin: '*',
methods: ['GET', 'POST']
}
});
const authRoutes = require('./routes/auth');
const channelRoutes = require('./routes/channels');
app.use(cors());
app.use(express.json());
app.use('/api/auth', authRoutes);
app.use('/api/channels', channelRoutes);
app.get('/', (req, res) => {
res.send('Secure Chat Backend Running');
});
const redisClient = require('./redis');
const db = require('./db');
io.on('connection', (socket) => {
console.log('User connected:', socket.id);
socket.on('join_channel', async (channelId) => {
socket.join(channelId);
console.log(`User ${socket.id} joined channel ${channelId}`);
// Load recent messages
try {
const result = await db.query(
`SELECT m.*, u.username
FROM messages m
JOIN users u ON m.sender_id = u.id
WHERE m.channel_id = $1
ORDER BY m.created_at DESC LIMIT 50`,
[channelId]
);
socket.emit('recent_messages', result.rows.reverse());
} catch (err) {
console.error('Error fetching messages:', err);
}
});
socket.on('send_message', async (data) => {
// data: { channelId, senderId, ciphertext, nonce, signature, keyVersion }
const { channelId, senderId, ciphertext, nonce, signature, keyVersion } = data;
try {
// Store in DB
const result = await db.query(
`INSERT INTO messages (channel_id, sender_id, ciphertext, nonce, signature, key_version)
VALUES ($1, $2, $3, $4, $5, $6) RETURNING id, created_at`,
[channelId, senderId, ciphertext, nonce, signature, keyVersion]
);
const message = {
id: result.rows[0].id,
created_at: result.rows[0].created_at,
...data
};
// Get username for display
const userRes = await db.query('SELECT username FROM users WHERE id = $1', [senderId]);
if (userRes.rows.length > 0) {
message.username = userRes.rows[0].username;
}
// Broadcast to channel
io.to(channelId).emit('new_message', message);
} catch (err) {
console.error('Error saving message:', err);
}
});
socket.on('typing', (data) => {
socket.to(data.channelId).emit('user_typing', { username: data.username });
});
socket.on('disconnect', () => {
console.log('User disconnected:', socket.id);
});
});
const PORT = process.env.PORT || 3000;
server.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});