aboutsummaryrefslogtreecommitdiffhomepage
path: root/server
diff options
context:
space:
mode:
Diffstat (limited to 'server')
-rw-r--r--server/controllers/api/users/two-factor.ts14
-rw-r--r--server/helpers/core-utils.ts14
-rw-r--r--server/helpers/otp.ts10
-rw-r--r--server/helpers/peertube-crypto.ts47
-rw-r--r--server/initializers/checker-after-init.ts7
-rw-r--r--server/initializers/checker-before-init.ts1
-rw-r--r--server/initializers/config.ts3
-rw-r--r--server/initializers/constants.ts10
-rw-r--r--server/lib/auth/oauth.ts4
-rw-r--r--server/tests/helpers/crypto.ts33
-rw-r--r--server/tests/helpers/index.ts3
11 files changed, 129 insertions, 17 deletions
diff --git a/server/controllers/api/users/two-factor.ts b/server/controllers/api/users/two-factor.ts
index 79f63a62d..e6ae9e4dd 100644
--- a/server/controllers/api/users/two-factor.ts
+++ b/server/controllers/api/users/two-factor.ts
@@ -1,5 +1,7 @@
1import express from 'express' 1import express from 'express'
2import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' 2import { generateOTPSecret, isOTPValid } from '@server/helpers/otp'
3import { encrypt } from '@server/helpers/peertube-crypto'
4import { CONFIG } from '@server/initializers/config'
3import { Redis } from '@server/lib/redis' 5import { Redis } from '@server/lib/redis'
4import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares' 6import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares'
5import { 7import {
@@ -44,7 +46,9 @@ async function requestTwoFactor (req: express.Request, res: express.Response) {
44 const user = res.locals.user 46 const user = res.locals.user
45 47
46 const { secret, uri } = generateOTPSecret(user.email) 48 const { secret, uri } = generateOTPSecret(user.email)
47 const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, secret) 49
50 const encryptedSecret = await encrypt(secret, CONFIG.SECRETS.PEERTUBE)
51 const requestToken = await Redis.Instance.setTwoFactorRequest(user.id, encryptedSecret)
48 52
49 return res.json({ 53 return res.json({
50 otpRequest: { 54 otpRequest: {
@@ -60,22 +64,22 @@ async function confirmRequestTwoFactor (req: express.Request, res: express.Respo
60 const otpToken = req.body.otpToken 64 const otpToken = req.body.otpToken
61 const user = res.locals.user 65 const user = res.locals.user
62 66
63 const secret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken) 67 const encryptedSecret = await Redis.Instance.getTwoFactorRequestToken(user.id, requestToken)
64 if (!secret) { 68 if (!encryptedSecret) {
65 return res.fail({ 69 return res.fail({
66 message: 'Invalid request token', 70 message: 'Invalid request token',
67 status: HttpStatusCode.FORBIDDEN_403 71 status: HttpStatusCode.FORBIDDEN_403
68 }) 72 })
69 } 73 }
70 74
71 if (isOTPValid({ secret, token: otpToken }) !== true) { 75 if (await isOTPValid({ encryptedSecret, token: otpToken }) !== true) {
72 return res.fail({ 76 return res.fail({
73 message: 'Invalid OTP token', 77 message: 'Invalid OTP token',
74 status: HttpStatusCode.FORBIDDEN_403 78 status: HttpStatusCode.FORBIDDEN_403
75 }) 79 })
76 } 80 }
77 81
78 user.otpSecret = secret 82 user.otpSecret = encryptedSecret
79 await user.save() 83 await user.save()
80 84
81 return res.sendStatus(HttpStatusCode.NO_CONTENT_204) 85 return res.sendStatus(HttpStatusCode.NO_CONTENT_204)
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 @@
6*/ 6*/
7 7
8import { exec, ExecOptions } from 'child_process' 8import { exec, ExecOptions } from 'child_process'
9import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions } from 'crypto' 9import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto'
10import { truncate } from 'lodash' 10import { truncate } from 'lodash'
11import { pipeline } from 'stream' 11import { pipeline } from 'stream'
12import { URL } from 'url' 12import { URL } from 'url'
@@ -311,7 +311,17 @@ function promisify2<T, U, A> (func: (arg1: T, arg2: U, cb: (err: any, result: A)
311 } 311 }
312} 312}
313 313
314// eslint-disable-next-line max-len
315function 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> {
316 return function promisified (arg1: T, arg2: U, arg3: V): Promise<A> {
317 return new Promise<A>((resolve: (arg: A) => void, reject: (err: any) => void) => {
318 func.apply(null, [ arg1, arg2, arg3, (err: any, res: A) => err ? reject(err) : resolve(res) ])
319 })
320 }
321}
322
314const randomBytesPromise = promisify1<number, Buffer>(randomBytes) 323const randomBytesPromise = promisify1<number, Buffer>(randomBytes)
324const scryptPromise = promisify3<string, string, number, Buffer>(scrypt)
315const execPromise2 = promisify2<string, any, string>(exec) 325const execPromise2 = promisify2<string, any, string>(exec)
316const execPromise = promisify1<string, string>(exec) 326const execPromise = promisify1<string, string>(exec)
317const pipelinePromise = promisify(pipeline) 327const pipelinePromise = promisify(pipeline)
@@ -339,6 +349,8 @@ export {
339 promisify1, 349 promisify1,
340 promisify2, 350 promisify2,
341 351
352 scryptPromise,
353
342 randomBytesPromise, 354 randomBytesPromise,
343 355
344 generateRSAKeyPairPromise, 356 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 @@
1import { Secret, TOTP } from 'otpauth' 1import { Secret, TOTP } from 'otpauth'
2import { CONFIG } from '@server/initializers/config'
2import { WEBSERVER } from '@server/initializers/constants' 3import { WEBSERVER } from '@server/initializers/constants'
4import { decrypt } from './peertube-crypto'
3 5
4function isOTPValid (options: { 6async function isOTPValid (options: {
5 secret: string 7 encryptedSecret: string
6 token: string 8 token: string
7}) { 9}) {
8 const { token, secret } = options 10 const { token, encryptedSecret } = options
11
12 const secret = await decrypt(encryptedSecret, CONFIG.SECRETS.PEERTUBE)
9 13
10 const totp = new TOTP({ 14 const totp = new TOTP({
11 ...baseOTPOptions(), 15 ...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 @@
1import { compare, genSalt, hash } from 'bcrypt' 1import { compare, genSalt, hash } from 'bcrypt'
2import { createSign, createVerify } from 'crypto' 2import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto'
3import { Request } from 'express' 3import { Request } from 'express'
4import { cloneDeep } from 'lodash' 4import { cloneDeep } from 'lodash'
5import { sha256 } from '@shared/extra-utils' 5import { sha256 } from '@shared/extra-utils'
6import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' 6import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
7import { MActor } from '../types/models' 7import { MActor } from '../types/models'
8import { generateRSAKeyPairPromise, promisify1, promisify2 } from './core-utils' 8import { generateRSAKeyPairPromise, promisify1, promisify2, randomBytesPromise, scryptPromise } from './core-utils'
9import { jsonld } from './custom-jsonld-signature' 9import { jsonld } from './custom-jsonld-signature'
10import { logger } from './logger' 10import { logger } from './logger'
11 11
@@ -21,7 +21,9 @@ function createPrivateAndPublicKeys () {
21 return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE) 21 return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE)
22} 22}
23 23
24// ---------------------------------------------------------------------------
24// User password checks 25// User password checks
26// ---------------------------------------------------------------------------
25 27
26function comparePassword (plainPassword: string, hashPassword: string) { 28function comparePassword (plainPassword: string, hashPassword: string) {
27 if (!plainPassword) return Promise.resolve(false) 29 if (!plainPassword) return Promise.resolve(false)
@@ -35,7 +37,9 @@ async function cryptPassword (password: string) {
35 return bcryptHashPromise(password, salt) 37 return bcryptHashPromise(password, salt)
36} 38}
37 39
40// ---------------------------------------------------------------------------
38// HTTP Signature 41// HTTP Signature
42// ---------------------------------------------------------------------------
39 43
40function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { 44function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
41 if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { 45 if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
@@ -64,7 +68,9 @@ function parseHTTPSignature (req: Request, clockSkew?: number) {
64 return parsed 68 return parsed
65} 69}
66 70
71// ---------------------------------------------------------------------------
67// JSONLD 72// JSONLD
73// ---------------------------------------------------------------------------
68 74
69function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> { 75function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
70 if (signedDocument.signature.type === 'RsaSignature2017') { 76 if (signedDocument.signature.type === 'RsaSignature2017') {
@@ -114,6 +120,8 @@ async function signJsonLDObject <T> (byActor: MActor, data: T) {
114 return Object.assign(data, { signature }) 120 return Object.assign(data, { signature })
115} 121}
116 122
123// ---------------------------------------------------------------------------
124
117function buildDigest (body: any) { 125function buildDigest (body: any) {
118 const rawBody = typeof body === 'string' ? body : JSON.stringify(body) 126 const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
119 127
@@ -121,6 +129,34 @@ function buildDigest (body: any) {
121} 129}
122 130
123// --------------------------------------------------------------------------- 131// ---------------------------------------------------------------------------
132// Encryption
133// ---------------------------------------------------------------------------
134
135async function encrypt (str: string, secret: string) {
136 const iv = await randomBytesPromise(ENCRYPTION.IV)
137
138 const key = await scryptPromise(secret, ENCRYPTION.SALT, 32)
139 const cipher = createCipheriv(ENCRYPTION.ALGORITHM, key, iv)
140
141 let encrypted = iv.toString(ENCRYPTION.ENCODING) + ':'
142 encrypted += cipher.update(str, 'utf8', ENCRYPTION.ENCODING)
143 encrypted += cipher.final(ENCRYPTION.ENCODING)
144
145 return encrypted
146}
147
148async function decrypt (encryptedArg: string, secret: string) {
149 const [ ivStr, encryptedStr ] = encryptedArg.split(':')
150
151 const iv = Buffer.from(ivStr, 'hex')
152 const key = await scryptPromise(secret, ENCRYPTION.SALT, 32)
153
154 const decipher = createDecipheriv(ENCRYPTION.ALGORITHM, key, iv)
155
156 return decipher.update(encryptedStr, ENCRYPTION.ENCODING, 'utf8') + decipher.final('utf8')
157}
158
159// ---------------------------------------------------------------------------
124 160
125export { 161export {
126 isHTTPSignatureDigestValid, 162 isHTTPSignatureDigestValid,
@@ -131,7 +167,10 @@ export {
131 comparePassword, 167 comparePassword,
132 createPrivateAndPublicKeys, 168 createPrivateAndPublicKeys,
133 cryptPassword, 169 cryptPassword,
134 signJsonLDObject 170 signJsonLDObject,
171
172 encrypt,
173 decrypt
135} 174}
136 175
137// --------------------------------------------------------------------------- 176// ---------------------------------------------------------------------------
diff --git a/server/initializers/checker-after-init.ts b/server/initializers/checker-after-init.ts
index 42839d1c9..c83fef425 100644
--- a/server/initializers/checker-after-init.ts
+++ b/server/initializers/checker-after-init.ts
@@ -42,6 +42,7 @@ function checkConfig () {
42 logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.') 42 logger.warn('services.csp-logger configuration has been renamed to csp.report_uri. Please update your configuration file.')
43 } 43 }
44 44
45 checkSecretsConfig()
45 checkEmailConfig() 46 checkEmailConfig()
46 checkNSFWPolicyConfig() 47 checkNSFWPolicyConfig()
47 checkLocalRedundancyConfig() 48 checkLocalRedundancyConfig()
@@ -103,6 +104,12 @@ export {
103 104
104// --------------------------------------------------------------------------- 105// ---------------------------------------------------------------------------
105 106
107function checkSecretsConfig () {
108 if (!CONFIG.SECRETS.PEERTUBE) {
109 throw new Error('secrets.peertube is missing in config. Generate one using `openssl rand -hex 32`')
110 }
111}
112
106function checkEmailConfig () { 113function checkEmailConfig () {
107 if (!isEmailEnabled()) { 114 if (!isEmailEnabled()) {
108 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) { 115 if (CONFIG.SIGNUP.ENABLED && CONFIG.SIGNUP.REQUIRES_EMAIL_VERIFICATION) {
diff --git a/server/initializers/checker-before-init.ts b/server/initializers/checker-before-init.ts
index 1fd4ba248..c9268b156 100644
--- a/server/initializers/checker-before-init.ts
+++ b/server/initializers/checker-before-init.ts
@@ -11,6 +11,7 @@ const config: IConfig = require('config')
11function checkMissedConfig () { 11function checkMissedConfig () {
12 const required = [ 'listen.port', 'listen.hostname', 12 const required = [ 'listen.port', 'listen.hostname',
13 'webserver.https', 'webserver.hostname', 'webserver.port', 13 'webserver.https', 'webserver.hostname', 'webserver.port',
14 'secrets.peertube',
14 'trust_proxy', 15 'trust_proxy',
15 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max', 16 'database.hostname', 'database.port', 'database.username', 'database.password', 'database.pool.max',
16 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address', 17 'smtp.hostname', 'smtp.port', 'smtp.username', 'smtp.password', 'smtp.tls', 'smtp.from_address',
diff --git a/server/initializers/config.ts b/server/initializers/config.ts
index 287bf6f6d..a5a0d4e46 100644
--- a/server/initializers/config.ts
+++ b/server/initializers/config.ts
@@ -20,6 +20,9 @@ const CONFIG = {
20 PORT: config.get<number>('listen.port'), 20 PORT: config.get<number>('listen.port'),
21 HOSTNAME: config.get<string>('listen.hostname') 21 HOSTNAME: config.get<string>('listen.hostname')
22 }, 22 },
23 SECRETS: {
24 PEERTUBE: config.get<string>('secrets.peertube')
25 },
23 DATABASE: { 26 DATABASE: {
24 DBNAME: config.has('database.name') ? config.get<string>('database.name') : 'peertube' + config.get<string>('database.suffix'), 27 DBNAME: config.has('database.name') ? config.get<string>('database.name') : 'peertube' + config.get<string>('database.suffix'),
25 HOSTNAME: config.get<string>('database.hostname'), 28 HOSTNAME: config.get<string>('database.hostname'),
diff --git a/server/initializers/constants.ts b/server/initializers/constants.ts
index 9d6087867..cab61948a 100644
--- a/server/initializers/constants.ts
+++ b/server/initializers/constants.ts
@@ -1,5 +1,5 @@
1import { RepeatOptions } from 'bullmq' 1import { RepeatOptions } from 'bullmq'
2import { randomBytes } from 'crypto' 2import { Encoding, randomBytes } from 'crypto'
3import { invert } from 'lodash' 3import { invert } from 'lodash'
4import { join } from 'path' 4import { join } from 'path'
5import { randomInt, root } from '@shared/core-utils' 5import { randomInt, root } from '@shared/core-utils'
@@ -637,6 +637,13 @@ let PRIVATE_RSA_KEY_SIZE = 2048
637// Password encryption 637// Password encryption
638const BCRYPT_SALT_SIZE = 10 638const BCRYPT_SALT_SIZE = 10
639 639
640const ENCRYPTION = {
641 ALGORITHM: 'aes-256-cbc',
642 IV: 16,
643 SALT: 'peertube',
644 ENCODING: 'hex' as Encoding
645}
646
640const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes 647const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes
641const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days 648const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days
642 649
@@ -959,6 +966,7 @@ const VIDEO_FILTERS = {
959export { 966export {
960 WEBSERVER, 967 WEBSERVER,
961 API_VERSION, 968 API_VERSION,
969 ENCRYPTION,
962 VIDEO_LIVE, 970 VIDEO_LIVE,
963 PEERTUBE_VERSION, 971 PEERTUBE_VERSION,
964 LAZY_STATIC_PATHS, 972 LAZY_STATIC_PATHS,
diff --git a/server/lib/auth/oauth.ts b/server/lib/auth/oauth.ts
index b541142a5..35b05ec5a 100644
--- a/server/lib/auth/oauth.ts
+++ b/server/lib/auth/oauth.ts
@@ -9,12 +9,12 @@ import OAuth2Server, {
9 UnsupportedGrantTypeError 9 UnsupportedGrantTypeError
10} from '@node-oauth/oauth2-server' 10} from '@node-oauth/oauth2-server'
11import { randomBytesPromise } from '@server/helpers/core-utils' 11import { randomBytesPromise } from '@server/helpers/core-utils'
12import { isOTPValid } from '@server/helpers/otp'
12import { MOAuthClient } from '@server/types/models' 13import { MOAuthClient } from '@server/types/models'
13import { sha1 } from '@shared/extra-utils' 14import { sha1 } from '@shared/extra-utils'
14import { HttpStatusCode } from '@shared/models' 15import { HttpStatusCode } from '@shared/models'
15import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' 16import { OAUTH_LIFETIME, OTP } from '../../initializers/constants'
16import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' 17import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model'
17import { isOTPValid } from '@server/helpers/otp'
18 18
19class MissingTwoFactorError extends Error { 19class MissingTwoFactorError extends Error {
20 code = HttpStatusCode.UNAUTHORIZED_401 20 code = HttpStatusCode.UNAUTHORIZED_401
@@ -138,7 +138,7 @@ async function handlePasswordGrant (options: {
138 throw new MissingTwoFactorError('Missing two factor header') 138 throw new MissingTwoFactorError('Missing two factor header')
139 } 139 }
140 140
141 if (isOTPValid({ secret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) { 141 if (await isOTPValid({ encryptedSecret: user.otpSecret, token: request.headers[OTP.HEADER_NAME] }) !== true) {
142 throw new InvalidTwoFactorError('Invalid two factor header') 142 throw new InvalidTwoFactorError('Invalid two factor header')
143 } 143 }
144 } 144 }
diff --git a/server/tests/helpers/crypto.ts b/server/tests/helpers/crypto.ts
new file mode 100644
index 000000000..b508c715b
--- /dev/null
+++ b/server/tests/helpers/crypto.ts
@@ -0,0 +1,33 @@
1/* eslint-disable @typescript-eslint/no-unused-expressions,@typescript-eslint/require-await */
2
3import { expect } from 'chai'
4import { decrypt, encrypt } from '@server/helpers/peertube-crypto'
5
6describe('Encrypt/Descrypt', function () {
7
8 it('Should encrypt and decrypt the string', async function () {
9 const secret = 'my_secret'
10 const str = 'my super string'
11
12 const encrypted = await encrypt(str, secret)
13 const decrypted = await decrypt(encrypted, secret)
14
15 expect(str).to.equal(decrypted)
16 })
17
18 it('Should not decrypt without the same secret', async function () {
19 const str = 'my super string'
20
21 const encrypted = await encrypt(str, 'my_secret')
22
23 let error = false
24
25 try {
26 await decrypt(encrypted, 'my_sicret')
27 } catch (err) {
28 error = true
29 }
30
31 expect(error).to.be.true
32 })
33})
diff --git a/server/tests/helpers/index.ts b/server/tests/helpers/index.ts
index 951208842..42d644c40 100644
--- a/server/tests/helpers/index.ts
+++ b/server/tests/helpers/index.ts
@@ -1,6 +1,7 @@
1import './image' 1import './crypto'
2import './core-utils' 2import './core-utils'
3import './dns' 3import './dns'
4import './dns'
4import './comment-model' 5import './comment-model'
5import './markdown' 6import './markdown'
6import './request' 7import './request'