diff options
author | Chocobozzz <me@florianbigard.com> | 2022-10-10 11:12:23 +0200 |
---|---|---|
committer | Chocobozzz <me@florianbigard.com> | 2022-10-10 11:12:23 +0200 |
commit | a3e5f804ad821f6979e8735b0569b1209986fedc (patch) | |
tree | 5b34a6bd6b3cb1c5e3eed32a72d02922100d53dc | |
parent | a0da6f90d16027b385a67da6a5691b163626a363 (diff) | |
download | PeerTube-a3e5f804ad821f6979e8735b0569b1209986fedc.tar.gz PeerTube-a3e5f804ad821f6979e8735b0569b1209986fedc.tar.zst PeerTube-a3e5f804ad821f6979e8735b0569b1209986fedc.zip |
Encrypt OTP secret
-rw-r--r-- | config/default.yaml | 4 | ||||
-rw-r--r-- | config/dev.yaml | 3 | ||||
-rw-r--r-- | config/production.yaml.example | 4 | ||||
-rw-r--r-- | config/test.yaml | 3 | ||||
-rw-r--r-- | server.ts | 7 | ||||
-rw-r--r-- | server/controllers/api/users/two-factor.ts | 14 | ||||
-rw-r--r-- | server/helpers/core-utils.ts | 14 | ||||
-rw-r--r-- | server/helpers/otp.ts | 10 | ||||
-rw-r--r-- | server/helpers/peertube-crypto.ts | 47 | ||||
-rw-r--r-- | server/initializers/checker-after-init.ts | 7 | ||||
-rw-r--r-- | server/initializers/checker-before-init.ts | 1 | ||||
-rw-r--r-- | server/initializers/config.ts | 3 | ||||
-rw-r--r-- | server/initializers/constants.ts | 10 | ||||
-rw-r--r-- | server/lib/auth/oauth.ts | 4 | ||||
-rw-r--r-- | server/tests/helpers/crypto.ts | 33 | ||||
-rw-r--r-- | server/tests/helpers/index.ts | 3 |
16 files changed, 149 insertions, 18 deletions
diff --git a/config/default.yaml b/config/default.yaml index 2d8aaf1ea..890d7acf9 100644 --- a/config/default.yaml +++ b/config/default.yaml | |||
@@ -10,6 +10,10 @@ webserver: | |||
10 | hostname: 'localhost' | 10 | hostname: 'localhost' |
11 | port: 9000 | 11 | port: 9000 |
12 | 12 | ||
13 | # Secrets you need to generate the first time you run PeerTube | ||
14 | secrets: | ||
15 | peertube: '' | ||
16 | |||
13 | rates_limit: | 17 | rates_limit: |
14 | api: | 18 | api: |
15 | # 50 attempts in 10 seconds | 19 | # 50 attempts in 10 seconds |
diff --git a/config/dev.yaml b/config/dev.yaml index ca93874d2..ef93afc19 100644 --- a/config/dev.yaml +++ b/config/dev.yaml | |||
@@ -5,6 +5,9 @@ listen: | |||
5 | webserver: | 5 | webserver: |
6 | https: false | 6 | https: false |
7 | 7 | ||
8 | secrets: | ||
9 | peertube: 'my super dev secret' | ||
10 | |||
8 | database: | 11 | database: |
9 | hostname: 'localhost' | 12 | hostname: 'localhost' |
10 | port: 5432 | 13 | port: 5432 |
diff --git a/config/production.yaml.example b/config/production.yaml.example index 46d574e42..399ac94f2 100644 --- a/config/production.yaml.example +++ b/config/production.yaml.example | |||
@@ -8,6 +8,10 @@ webserver: | |||
8 | hostname: 'example.com' | 8 | hostname: 'example.com' |
9 | port: 443 | 9 | port: 443 |
10 | 10 | ||
11 | # Secrets you need to generate the first time you run PeerTube | ||
12 | secret: | ||
13 | peertube: '' | ||
14 | |||
11 | rates_limit: | 15 | rates_limit: |
12 | api: | 16 | api: |
13 | # 50 attempts in 10 seconds | 17 | # 50 attempts in 10 seconds |
diff --git a/config/test.yaml b/config/test.yaml index a87642bd8..48cf0c0f6 100644 --- a/config/test.yaml +++ b/config/test.yaml | |||
@@ -5,6 +5,9 @@ listen: | |||
5 | webserver: | 5 | webserver: |
6 | https: false | 6 | https: false |
7 | 7 | ||
8 | secrets: | ||
9 | peertube: 'my super secret' | ||
10 | |||
8 | rates_limit: | 11 | rates_limit: |
9 | signup: | 12 | signup: |
10 | window: 10 minutes | 13 | window: 10 minutes |
@@ -45,7 +45,12 @@ try { | |||
45 | 45 | ||
46 | import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init' | 46 | import { checkConfig, checkActivityPubUrls, checkFFmpegVersion } from './server/initializers/checker-after-init' |
47 | 47 | ||
48 | checkConfig() | 48 | try { |
49 | checkConfig() | ||
50 | } catch (err) { | ||
51 | logger.error('Config error.', { err }) | ||
52 | process.exit(-1) | ||
53 | } | ||
49 | 54 | ||
50 | // Trust our proxy (IP forwarding...) | 55 | // Trust our proxy (IP forwarding...) |
51 | app.set('trust proxy', CONFIG.TRUST_PROXY) | 56 | app.set('trust proxy', CONFIG.TRUST_PROXY) |
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 @@ | |||
1 | import express from 'express' | 1 | import express from 'express' |
2 | import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' | 2 | import { generateOTPSecret, isOTPValid } from '@server/helpers/otp' |
3 | import { encrypt } from '@server/helpers/peertube-crypto' | ||
4 | import { CONFIG } from '@server/initializers/config' | ||
3 | import { Redis } from '@server/lib/redis' | 5 | import { Redis } from '@server/lib/redis' |
4 | import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares' | 6 | import { asyncMiddleware, authenticate, usersCheckCurrentPasswordFactory } from '@server/middlewares' |
5 | import { | 7 | import { |
@@ -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 | ||
8 | import { exec, ExecOptions } from 'child_process' | 8 | import { exec, ExecOptions } from 'child_process' |
9 | import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions } from 'crypto' | 9 | import { ED25519KeyPairOptions, generateKeyPair, randomBytes, RSAKeyPairOptions, scrypt } from 'crypto' |
10 | import { truncate } from 'lodash' | 10 | import { truncate } from 'lodash' |
11 | import { pipeline } from 'stream' | 11 | import { pipeline } from 'stream' |
12 | import { URL } from 'url' | 12 | import { 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 | ||
315 | 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> { | ||
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 | |||
314 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) | 323 | const randomBytesPromise = promisify1<number, Buffer>(randomBytes) |
324 | const scryptPromise = promisify3<string, string, number, Buffer>(scrypt) | ||
315 | const execPromise2 = promisify2<string, any, string>(exec) | 325 | const execPromise2 = promisify2<string, any, string>(exec) |
316 | const execPromise = promisify1<string, string>(exec) | 326 | const execPromise = promisify1<string, string>(exec) |
317 | const pipelinePromise = promisify(pipeline) | 327 | const 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 @@ | |||
1 | import { Secret, TOTP } from 'otpauth' | 1 | import { Secret, TOTP } from 'otpauth' |
2 | import { CONFIG } from '@server/initializers/config' | ||
2 | import { WEBSERVER } from '@server/initializers/constants' | 3 | import { WEBSERVER } from '@server/initializers/constants' |
4 | import { decrypt } from './peertube-crypto' | ||
3 | 5 | ||
4 | function isOTPValid (options: { | 6 | async 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 @@ | |||
1 | import { compare, genSalt, hash } from 'bcrypt' | 1 | import { compare, genSalt, hash } from 'bcrypt' |
2 | import { createSign, createVerify } from 'crypto' | 2 | import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto' |
3 | import { Request } from 'express' | 3 | import { Request } from 'express' |
4 | import { cloneDeep } from 'lodash' | 4 | import { cloneDeep } from 'lodash' |
5 | import { sha256 } from '@shared/extra-utils' | 5 | import { sha256 } from '@shared/extra-utils' |
6 | import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' | 6 | import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' |
7 | import { MActor } from '../types/models' | 7 | import { MActor } from '../types/models' |
8 | import { generateRSAKeyPairPromise, promisify1, promisify2 } from './core-utils' | 8 | import { generateRSAKeyPairPromise, promisify1, promisify2, randomBytesPromise, scryptPromise } from './core-utils' |
9 | import { jsonld } from './custom-jsonld-signature' | 9 | import { jsonld } from './custom-jsonld-signature' |
10 | import { logger } from './logger' | 10 | import { 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 | ||
26 | function comparePassword (plainPassword: string, hashPassword: string) { | 28 | function 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 | ||
40 | function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { | 44 | function 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 | ||
69 | function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> { | 75 | function 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 | |||
117 | function buildDigest (body: any) { | 125 | function 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 | |||
135 | async 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 | |||
148 | async 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 | ||
125 | export { | 161 | export { |
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 | ||
107 | function 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 | |||
106 | function checkEmailConfig () { | 113 | function 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') | |||
11 | function checkMissedConfig () { | 11 | function 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 @@ | |||
1 | import { RepeatOptions } from 'bullmq' | 1 | import { RepeatOptions } from 'bullmq' |
2 | import { randomBytes } from 'crypto' | 2 | import { Encoding, randomBytes } from 'crypto' |
3 | import { invert } from 'lodash' | 3 | import { invert } from 'lodash' |
4 | import { join } from 'path' | 4 | import { join } from 'path' |
5 | import { randomInt, root } from '@shared/core-utils' | 5 | import { randomInt, root } from '@shared/core-utils' |
@@ -637,6 +637,13 @@ let PRIVATE_RSA_KEY_SIZE = 2048 | |||
637 | // Password encryption | 637 | // Password encryption |
638 | const BCRYPT_SALT_SIZE = 10 | 638 | const BCRYPT_SALT_SIZE = 10 |
639 | 639 | ||
640 | const ENCRYPTION = { | ||
641 | ALGORITHM: 'aes-256-cbc', | ||
642 | IV: 16, | ||
643 | SALT: 'peertube', | ||
644 | ENCODING: 'hex' as Encoding | ||
645 | } | ||
646 | |||
640 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes | 647 | const USER_PASSWORD_RESET_LIFETIME = 60000 * 60 // 60 minutes |
641 | const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days | 648 | const USER_PASSWORD_CREATE_LIFETIME = 60000 * 60 * 24 * 7 // 7 days |
642 | 649 | ||
@@ -959,6 +966,7 @@ const VIDEO_FILTERS = { | |||
959 | export { | 966 | export { |
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' |
11 | import { randomBytesPromise } from '@server/helpers/core-utils' | 11 | import { randomBytesPromise } from '@server/helpers/core-utils' |
12 | import { isOTPValid } from '@server/helpers/otp' | ||
12 | import { MOAuthClient } from '@server/types/models' | 13 | import { MOAuthClient } from '@server/types/models' |
13 | import { sha1 } from '@shared/extra-utils' | 14 | import { sha1 } from '@shared/extra-utils' |
14 | import { HttpStatusCode } from '@shared/models' | 15 | import { HttpStatusCode } from '@shared/models' |
15 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' | 16 | import { OAUTH_LIFETIME, OTP } from '../../initializers/constants' |
16 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' | 17 | import { BypassLogin, getClient, getRefreshToken, getUser, revokeToken, saveToken } from './oauth-model' |
17 | import { isOTPValid } from '@server/helpers/otp' | ||
18 | 18 | ||
19 | class MissingTwoFactorError extends Error { | 19 | class 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 | |||
3 | import { expect } from 'chai' | ||
4 | import { decrypt, encrypt } from '@server/helpers/peertube-crypto' | ||
5 | |||
6 | describe('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 @@ | |||
1 | import './image' | 1 | import './crypto' |
2 | import './core-utils' | 2 | import './core-utils' |
3 | import './dns' | 3 | import './dns' |
4 | import './dns' | ||
4 | import './comment-model' | 5 | import './comment-model' |
5 | import './markdown' | 6 | import './markdown' |
6 | import './request' | 7 | import './request' |