hostname: 'localhost'
port: 9000
+# Secrets you need to generate the first time you run PeerTube
+secrets:
+ peertube: ''
+
rates_limit:
api:
# 50 attempts in 10 seconds
webserver:
https: false
+secrets:
+ peertube: 'my super dev secret'
+
database:
hostname: 'localhost'
port: 5432
hostname: 'example.com'
port: 443
+# Secrets you need to generate the first time you run PeerTube
+secret:
+ peertube: ''
+
rates_limit:
api:
# 50 attempts in 10 seconds
webserver:
https: false
+secrets:
+ peertube: 'my super secret'
+
rates_limit:
signup:
window: 10 minutes
import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init'
-checkConfig()
+try {
+ checkConfig()
+} catch (err) {
+ logger.error('Config error.', { err })
+ process.exit(-1)
+}
// Trust our proxy (IP forwarding...)
app.set('trust proxy', CONFIG.TRUST_PROXY)
import express from 'express'
import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
+import { encrypt } from '@server/helpers/peertube-crypto'
+import { CONFIG } from '@server/initializers/config'
import { Redis } from '@server/lib/redis'
import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
import {
const user = res.locals.user
const { secret, uri } = generateOTPSecret(user.email)
- const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, secret)
+
+ const encryptedSecret = await encrypt(secret, CONFIG.SECRETS.PEERTUBE)
+ const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, encryptedSecret)
return res.json({
otpRequest: {
const otpToken = req.body.otpToken
const user = res.locals.user
- const secret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken)
- if (!secret) {
+ const encryptedSecret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken)
+ if (!encryptedSecret) {
return res.fail({
message: 'Invalid request token',
status: HttpStatusCode.FORBIDDEN_403
})
}
- if (isOTPValid({ secret, token: otpToken }) !== true) {
+ if (await isOTPValid({ encryptedSecret, token: otpToken }) !== true) {
return res.fail({
message: 'Invalid OTP token',
status: HttpStatusCode.FORBIDDEN_403
})
}
- user.otpSecret = secret
+ user.otpSecret = encryptedSecret
await user.save()
return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
*/
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'
}
}
+// eslint-disable-next-line max-len
+function promisify3<T, U, V, A> (func: (arg1: T, arg2: U, arg3: V, cb: (err: any, result: A) => void) => void): (arg1: T, arg2: U, arg3: V) => Promise<A> {
+ return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> {
+ return new Promise<A>((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<number, Buffer>(randomBytes)
+const scryptPromise = promisify3<string, string, number, Buffer>(scrypt)
const execPromise2 = promisify2<string, any, string>(exec)
const execPromise = promisify1<string, string>(exec)
const pipelinePromise = promisify(pipeline)
promisify1,
promisify2,
+ scryptPromise,
+
randomBytesPromise,
generateRSAKeyPairPromise,
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(),
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'
return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE)
}
+// ---------------------------------------------------------------------------
// User password checks
+// ---------------------------------------------------------------------------
function comparePassword (plainPassword: string, hashPassword: string) {
if (!plainPassword) return Promise.resolve(false)
return bcryptHashPromise(password, salt)
}
+// ---------------------------------------------------------------------------
// HTTP Signature
+// ---------------------------------------------------------------------------
function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
return parsed
}
+// ---------------------------------------------------------------------------
// JSONLD
+// ---------------------------------------------------------------------------
function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
if (signedDocument.signature.type === 'RsaSignature2017') {
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 {
comparePassword,
createPrivateAndPublicKeys,
cryptPassword,
- signJsonLDObject
+ signJsonLDObject,
+
+ encrypt,
+ decrypt
}
// ---------------------------------------------------------------------------
logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.')
}
+ checkSecretsConfig()
checkEmailConfig()
checkNSFWPolicyConfig()
checkLocalRedundancyConfig()
// ---------------------------------------------------------------------------
+function checkSecretsConfig () {
+ if (!CONFIG.SECRETS.PEERTUBE) {
+ throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`')
+ }
+}
+
function checkEmailConfig () {
if (!isEmailEnabled()) {
if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
function checkMissedConfig () {
const required = [ 'listen.port', 'listen.hostname',
'webserver.https', 'webserver.hostname', 'webserver.port',
+ 'secrets.peertube',
'trust_proxy',
'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max',
'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
PORT: config.get<number>('listen.port'),
HOSTNAME: config.get<string>('listen.hostname')
},
+ SECRETS: {
+ PEERTUBE: config.get<string>('secrets.peertube')
+ },
DATABASE: {
DBNAME: config.has('database.name') ? config.get<string>('database.name') : 'peertube' + config.get<string>('database.suffix'),
HOSTNAME: config.get<string>('database.hostname'),
import { RepeatOptions } from 'bullmq'
-import { randomBytes } from 'crypto'
+import { Encoding, randomBytes } from 'crypto'
import { invert } from 'lodash'
import { join } from 'path'
import { randomInt, root } from '@shared/core-utils'
// Password encryption
const BCRYPT_SALT_SIZE = 10
+const ENCRYPTION = {
+ ALGORITHM: 'aes-256-cbc',
+ IV: 16,
+ SALT: 'peertube',
+ ENCODING: 'hex' as Encoding
+}
+
const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
export {
WEBSERVER,
API_VERSION,
+ ENCRYPTION,
VIDEO_LIVE,
PEERTUBE_VERSION,
LAZY_STATIC_PATHS,
UnsupportedGrantTypeError
} from '@node-oauth/oauth2-server'
import { randomBytesPromise } from '@server/helpers/core-utils'
+import { isOTPValid } from '@server/helpers/otp'
import { MOAuthClient } from '@server/types/models'
import { sha1 } from '@shared/extra-utils'
import { HttpStatusCode } from '@shared/models'
import { OAUTH_LIFETIME, OTP } from '../../initializers/constants'
import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
-import { isOTPValid } from '@server/helpers/otp'
class MissingTwoFactorError extends Error {
code = HttpStatusCode.UNAUTHORIZED_401
throw new MissingTwoFactorError('Missing two factor header')
}
- if (isOTPValid({ secret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) {
+ if (await isOTPValid({ encryptedSecret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) {
throw new InvalidTwoFactorError('Invalid two factor header')
}
}
--- /dev/null
+/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
+
+import { expect } from 'chai'
+import { decrypt, encrypt } from '@server/helpers/peertube-crypto'
+
+describe('Encrypt/Descrypt', function () {
+
+ it('Should encrypt and decrypt the string', async function () {
+ const secret = 'my_secret'
+ const str = 'my super string'
+
+ const encrypted = await encrypt(str, secret)
+ const decrypted = await decrypt(encrypted, secret)
+
+ expect(str).to.equal(decrypted)
+ })
+
+ it('Should not decrypt without the same secret', async function () {
+ const str = 'my super string'
+
+ const encrypted = await encrypt(str, 'my_secret')
+
+ let error = false
+
+ try {
+ await decrypt(encrypted, 'my_sicret')
+ } catch (err) {
+ error = true
+ }
+
+ expect(error).to.be.true
+ })
+})
-import './image'
+import './crypto'
import './core-utils'
import './dns'
+import './dns'
import './comment-model'
import './markdown'
import './request'