ecc340e7b1
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.
224 lines
6.1 KiB
TypeScript
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 };
|
|
};
|