From 56f47830758ff8e92abcfcc5f35d474ab12fe215 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Wed, 5 Oct 2022 15:37:15 +0200 Subject: Support two factor authentication in backend --- server/helpers/otp.ts | 54 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 server/helpers/otp.ts (limited to 'server/helpers') diff --git a/server/helpers/otp.ts b/server/helpers/otp.ts new file mode 100644 index 000000000..a13edc5e2 --- /dev/null +++ b/server/helpers/otp.ts @@ -0,0 +1,54 @@ +import { Secret, TOTP } from 'otpauth' +import { WEBSERVER } from '@server/initializers/constants' + +function isOTPValid (options: { + secret: string + token: string +}) { + const { token, secret } = options + + const totp = new TOTP({ + ...baseOTPOptions(), + + secret + }) + + const delta = totp.validate({ + token, + window: 1 + }) + + if (delta === null) return false + + return true +} + +function generateOTPSecret (email: string) { + const totp = new TOTP({ + ...baseOTPOptions(), + + label: email, + secret: new Secret() + }) + + return { + secret: totp.secret.base32, + uri: totp.toString() + } +} + +export { + isOTPValid, + generateOTPSecret +} + +// --------------------------------------------------------------------------- + +function baseOTPOptions () { + return { + issuer: WEBSERVER.HOST, + algorithm: 'SHA1', + digits: 6, + period: 30 + } +} -- cgit v1.2.3 From 2166c058f34dff6f91566930d12448805d829de7 Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Fri, 7 Oct 2022 14:23:42 +0200 Subject: Allow admins to disable two factor auth --- server/helpers/peertube-crypto.ts | 2 ++ 1 file changed, 2 insertions(+) (limited to 'server/helpers') diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 8aca50900..dcf47ce76 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -24,6 +24,8 @@ function createPrivateAndPublicKeys () { // User password checks function comparePassword (plainPassword: string, hashPassword: string) { + if (!plainPassword) return Promise.resolve(false) + return bcryptComparePromise(plainPassword, hashPassword) } -- cgit v1.2.3 From a3e5f804ad821f6979e8735b0569b1209986fedc Mon Sep 17 00:00:00 2001 From: Chocobozzz Date: Mon, 10 Oct 2022 11:12:23 +0200 Subject: Encrypt OTP secret --- server/helpers/core-utils.ts | 14 +++++++++++- server/helpers/otp.ts | 10 ++++++--- server/helpers/peertube-crypto.ts | 47 +++++++++++++++++++++++++++++++++++---- 3 files changed, 63 insertions(+), 8 deletions(-) (limited to 'server/helpers') diff --git a/server/helpers/core-utils.ts b/server/helpers/core-utils.ts index c762f6a29..73bd994c1 100644 --- a/server/helpers/core-utils.ts +++ b/server/helpers/core-utils.ts @@ -6,7 +6,7 @@ */ import { exec, ExecOptions } from 'child_process' -import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions } from 'crypto' +import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto' import { truncate } from 'lodash' import { pipeline } from 'stream' import { URL } from 'url' @@ -311,7 +311,17 @@ function promisify2 (func: (arg1: T, arg2: U, cb: (err: any, result: A) } } +// eslint-disable-next-line max-len +function promisify3 (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise { + return function promisified (arg1: T, arg2: U, arg3: V): Promise { + return new Promise((resolve: (arg: A) => void, reject: (err: any) => void) => { + func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ]) + }) + } +} + const randomBytesPromise = promisify1(randomBytes) +const scryptPromise = promisify3(scrypt) const execPromise2 = promisify2(exec) const execPromise = promisify1(exec) const pipelinePromise = promisify(pipeline) @@ -339,6 +349,8 @@ export { promisify1, promisify2, + scryptPromise, + randomBytesPromise, generateRSAKeyPairPromise, diff --git a/server/helpers/otp.ts b/server/helpers/otp.ts index a13edc5e2..a32cc9621 100644 --- a/server/helpers/otp.ts +++ b/server/helpers/otp.ts @@ -1,11 +1,15 @@ import { Secret, TOTP } from 'otpauth' +import { CONFIG } from '@server/initializers/config' import { WEBSERVER } from '@server/initializers/constants' +import { decrypt } from './peertube-crypto' -function isOTPValid (options: { - secret: string +async function isOTPValid (options: { + encryptedSecret: string token: string }) { - const { token, secret } = options + const { token, encryptedSecret } = options + + const secret = await decrypt(encryptedSecret, CONFIG.SECRETS.PEERTUBE) const totp = new TOTP({ ...baseOTPOptions(), diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index dcf47ce76..ae7d11800 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,11 +1,11 @@ import { compare, genSalt, hash } from 'bcrypt' -import { createSign, createVerify } from 'crypto' +import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto' import { Request } from 'express' import { cloneDeep } from 'lodash' import { sha256 } from '@shared/extra-utils' -import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' +import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' import { MActor } from '../types/models' -import { generateRSAKeyPairPromise, promisify1, promisify2 } from './core-utils' +import { generateRSAKeyPairPromise, promisify1, promisify2, randomBytesPromise, scryptPromise } from './core-utils' import { jsonld } from './custom-jsonld-signature' import { logger } from './logger' @@ -21,7 +21,9 @@ function createPrivateAndPublicKeys () { return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE) } +// --------------------------------------------------------------------------- // User password checks +// --------------------------------------------------------------------------- function comparePassword (plainPassword: string, hashPassword: string) { if (!plainPassword) return Promise.resolve(false) @@ -35,7 +37,9 @@ async function cryptPassword (password: string) { return bcryptHashPromise(password, salt) } +// --------------------------------------------------------------------------- // HTTP Signature +// --------------------------------------------------------------------------- function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { @@ -64,7 +68,9 @@ function parseHTTPSignature (req: Request, clockSkew?: number) { return parsed } +// --------------------------------------------------------------------------- // JSONLD +// --------------------------------------------------------------------------- function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise { if (signedDocument.signature.type === 'RsaSignature2017') { @@ -114,12 +120,42 @@ async function signJsonLDObject (byActor: MActor, data: T) { return Object.assign(data, { signature }) } +// --------------------------------------------------------------------------- + function buildDigest (body: any) { const rawBody = typeof body === 'string' ? body : JSON.stringify(body) return 'SHA-256=' + sha256(rawBody, 'base64') } +// --------------------------------------------------------------------------- +// Encryption +// --------------------------------------------------------------------------- + +async function encrypt (str: string, secret: string) { + const iv = await randomBytesPromise(ENCRYPTION.IV) + + const key = await scryptPromise(secret, ENCRYPTION.SALT, 32) + const cipher = createCipheriv(ENCRYPTION.ALGORITHM, key, iv) + + let encrypted = iv.toString(ENCRYPTION.ENCODING) + ':' + encrypted += cipher.update(str, 'utf8', ENCRYPTION.ENCODING) + encrypted += cipher.final(ENCRYPTION.ENCODING) + + return encrypted +} + +async function decrypt (encryptedArg: string, secret: string) { + const [ ivStr, encryptedStr ] = encryptedArg.split(':') + + const iv = Buffer.from(ivStr, 'hex') + const key = await scryptPromise(secret, ENCRYPTION.SALT, 32) + + const decipher = createDecipheriv(ENCRYPTION.ALGORITHM, key, iv) + + return decipher.update(encryptedStr, ENCRYPTION.ENCODING, 'utf8') + decipher.final('utf8') +} + // --------------------------------------------------------------------------- export { @@ -131,7 +167,10 @@ export { comparePassword, createPrivateAndPublicKeys, cryptPassword, - signJsonLDObject + signJsonLDObject, + + encrypt, + decrypt } // --------------------------------------------------------------------------- -- cgit v1.2.3