Files
oxmc ecc340e7b1
Release Please / release-please (push) Has been cancelled
Docker Build and Push (Development) / build-and-push-dev (push) Has been cancelled
feat: add dark mode UI and SQLite as alternate database backend
Dark mode: CSS custom properties + .dark class toggled via Redux/localStorage,
with a theme toggle in the Header and Settings page. Foundation didn't exist
yet despite prior assumption, so it was built from scratch.

SQLite: new DB_ENGINE=mongo|sqlite env var selects the metadata backend at
runtime (Mongo stays default, no behavior change unless opted in). File/thumbnail
bytes continue to use the existing fs/S3 storage path. Implemented via a shared
DB interface (dbTypes.ts) with parallel mongoDB/ and sqliteDB/ implementations,
selected through dbFactory.ts, with JWT/encryption logic extracted into
userCrypto.ts so both engines share it.
2026-07-05 04:18:33 -07:00

224 lines
6.1 KiB
TypeScript

import crypto from "crypto";
import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken";
import env from "../enviroment/env";
const maxAgeAccess = 60 * 1000 * 20 + 1000 * 60;
const maxAgeRefresh = 60 * 1000 * 60 * 24 * 30 + 1000 * 60;
const maxAgeAccessStreamVideo = 60 * 1000 * 60 * 24;
export interface CryptoUser {
_id: any;
email: string;
password: string;
privateKey?: string;
publicKey?: string;
emailVerified?: boolean;
}
export const encryptToken = (token: string, key: any, iv: any): string => {
iv = Buffer.from(iv, "hex");
const TOKEN_CIPHER_KEY = crypto.createHash("sha256").update(key).digest();
const cipher = crypto.createCipheriv("aes-256-cbc", TOKEN_CIPHER_KEY, iv);
const encryptedText = cipher.update(token);
return Buffer.concat([encryptedText, cipher.final()]).toString("hex");
};
export const decryptToken = (
encryptedToken: any,
key: string,
iv: any
): string => {
encryptedToken = Buffer.from(encryptedToken, "hex");
iv = Buffer.from(iv, "hex");
const TOKEN_CIPHER_KEY = crypto.createHash("sha256").update(key).digest();
const decipher = crypto.createDecipheriv("aes-256-cbc", TOKEN_CIPHER_KEY, iv);
const tokenDecrypted = decipher.update(encryptedToken);
return Buffer.concat([tokenDecrypted, decipher.final()]).toString();
};
export const getEncryptionKey = (user: any): Buffer | undefined => {
try {
const userPassword = user.password;
const masterEncryptedText = user.privateKey as string;
const masterPassword = env.key!;
const iv = Buffer.from(user.publicKey as string, "hex");
const USER_CIPHER_KEY = crypto
.createHash("sha256")
.update(userPassword)
.digest();
const MASTER_CIPHER_KEY = crypto
.createHash("sha256")
.update(masterPassword)
.digest();
const unhexMasterText = Buffer.from(masterEncryptedText, "hex");
const masterDecipher = crypto.createDecipheriv(
"aes-256-cbc",
MASTER_CIPHER_KEY,
iv
);
let masterDecrypted = masterDecipher.update(unhexMasterText);
masterDecrypted = Buffer.concat([masterDecrypted, masterDecipher.final()]);
const decipher = crypto.createDecipheriv("aes-256-cbc", USER_CIPHER_KEY, iv);
let decrypted = decipher.update(masterDecrypted);
decrypted = Buffer.concat([decrypted, decipher.final()]);
return decrypted;
} catch (e) {
console.log("Get Encryption Key Error", e);
return undefined;
}
};
export const generateEncryptionKeyMaterial = (
userPassword: string,
randomKey: Buffer
): { privateKey: string; publicKey: string } => {
const masterPassword = env.key!;
const iv = crypto.randomBytes(16);
const USER_CIPHER_KEY = crypto
.createHash("sha256")
.update(userPassword)
.digest();
const cipher = crypto.createCipheriv("aes-256-cbc", USER_CIPHER_KEY, iv);
let encryptedText = cipher.update(randomKey);
encryptedText = Buffer.concat([encryptedText, cipher.final()]);
const MASTER_CIPHER_KEY = crypto
.createHash("sha256")
.update(masterPassword)
.digest();
const masterCipher = crypto.createCipheriv(
"aes-256-cbc",
MASTER_CIPHER_KEY,
iv
);
const masterEncryptedText = masterCipher.update(encryptedText);
const privateKey = Buffer.concat([
masterEncryptedText,
masterCipher.final(),
]).toString("hex");
const publicKey = iv.toString("hex");
return { privateKey, publicKey };
};
export const hashPassword = async (password: string): Promise<string> => {
return bcrypt.hash(password, 8);
};
export const comparePassword = async (
password: string,
hash: string
): Promise<boolean> => {
return bcrypt.compare(password, hash);
};
export const signAuthTokens = (
user: any
): {
accessToken: string;
refreshToken: string;
encryptedRefreshToken: string;
iv: string;
time: number;
} => {
const iv = crypto.randomBytes(16);
const time = new Date().getTime();
const userObj = {
_id: user._id,
emailVerified: user.emailVerified,
email: user.email,
};
const accessToken = jwt.sign({ user: userObj, iv }, env.passwordAccess!, {
expiresIn: maxAgeAccess.toString(),
});
const refreshToken = jwt.sign(
{ _id: user._id.toString(), iv, time },
env.passwordRefresh!,
{ expiresIn: maxAgeRefresh.toString() }
);
const encryptionKey = getEncryptionKey(user);
const encryptedRefreshToken = encryptToken(refreshToken, encryptionKey, iv);
return {
accessToken,
refreshToken,
encryptedRefreshToken,
iv: iv.toString("hex"),
time,
};
};
export const signStreamVideoToken = (
user: any
): { token: string; encryptedToken: string; iv: string; time: number } => {
const iv = crypto.randomBytes(16);
const time = new Date().getTime();
const token = jwt.sign(
{ _id: user._id.toString(), iv, time },
env.passwordAccess!,
{ expiresIn: maxAgeAccessStreamVideo.toString() }
);
const encryptionKey = getEncryptionKey(user);
const encryptedToken = encryptToken(token, encryptionKey, iv);
return { token, encryptedToken, iv: iv.toString("hex"), time };
};
export const signTempAuthToken = (
user: any
): { token: string; encryptedToken: string } => {
const iv = crypto.randomBytes(16);
const token = jwt.sign({ _id: user._id.toString(), iv }, env.passwordAccess!, {
expiresIn: "3000ms",
});
const encryptionKey = getEncryptionKey(user);
const encryptedToken = encryptToken(token, encryptionKey, iv);
return { token, encryptedToken };
};
export const signEmailVerifyToken = (
user: any
): { token: string; encryptedToken: string } => {
const iv = crypto.randomBytes(16);
const token = jwt.sign({ _id: user._id.toString(), iv }, env.passwordAccess!, {
expiresIn: "1d",
});
const encryptionKey = getEncryptionKey(user);
const encryptedToken = encryptToken(token, encryptionKey, iv);
return { token, encryptedToken };
};
export const signPasswordResetToken = (
user: any
): { token: string; encryptedToken: string } => {
const iv = crypto.randomBytes(16);
const token = jwt.sign({ _id: user._id.toString(), iv }, env.passwordAccess!, {
expiresIn: "1h",
});
const encryptionKey = getEncryptionKey(user);
const encryptedToken = encryptToken(token, encryptionKey, iv);
return { token, encryptedToken };
};