X-Git-Url: https://git.immae.eu/?a=blobdiff_plain;f=server%2Fhelpers%2Fpeertube-crypto.ts;h=95e78a9048ec34a0f31d6cb86ea0be579417f635;hb=cffef25313bdf7a6c435f56ac6715fdd91acf7b3;hp=313c12e26fe07f78b1bca2db159602037fdcb05d;hpb=ce33ee01cd3806201b676c318e9aa930032921b2;p=github%2FChocobozzz%2FPeerTube.git diff --git a/server/helpers/peertube-crypto.ts b/server/helpers/peertube-crypto.ts index 313c12e26..95e78a904 100644 --- a/server/helpers/peertube-crypto.ts +++ b/server/helpers/peertube-crypto.ts @@ -1,71 +1,208 @@ -import { BCRYPT_SALT_SIZE, PRIVATE_RSA_KEY_SIZE } from '../initializers' -import { ActorModel } from '../models/activitypub/actor' -import { bcryptComparePromise, bcryptGenSaltPromise, bcryptHashPromise, createPrivateKey, getPublicKey } from './core-utils' -import { jsig } from './custom-jsonld-signature' +import { compare, genSalt, hash } from 'bcrypt' +import { createCipheriv, createDecipheriv, createSign, createVerify } from 'crypto' +import { Request } from 'express' +import { cloneDeep } from 'lodash' +import { promisify1, promisify2 } from '@shared/core-utils' +import { sha256 } from '@shared/extra-utils' +import { BCRYPT_SALT_SIZE, ENCRYPTION, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants' +import { MActor } from '../types/models' +import { generateRSAKeyPairPromise, randomBytesPromise, scryptPromise } from './core-utils' +import { jsonld } from './custom-jsonld-signature' import { logger } from './logger' -async function createPrivateAndPublicKeys () { +const bcryptComparePromise = promisify2(compare) +const bcryptGenSaltPromise = promisify1(genSalt) +const bcryptHashPromise = promisify2(hash) + +const httpSignature = require('@peertube/http-signature') + +function createPrivateAndPublicKeys () { logger.info('Generating a RSA key...') - const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE) - const { publicKey } = await getPublicKey(key) + return generateRSAKeyPairPromise(PRIVATE_RSA_KEY_SIZE) +} + +// --------------------------------------------------------------------------- +// User password checks +// --------------------------------------------------------------------------- + +function comparePassword (plainPassword: string, hashPassword: string) { + if (!plainPassword) return Promise.resolve(false) + + return bcryptComparePromise(plainPassword, hashPassword) +} + +async function cryptPassword (password: string) { + const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) - return { privateKey: key, publicKey } + return bcryptHashPromise(password, salt) } -function isSignatureVerified (fromActor: ActorModel, signedDocument: object) { - const publicKeyObject = { - '@context': jsig.SECURITY_CONTEXT_URL, - '@id': fromActor.url, - '@type': 'CryptographicKey', - owner: fromActor.url, - publicKeyPem: fromActor.publicKey +// --------------------------------------------------------------------------- +// HTTP Signature +// --------------------------------------------------------------------------- + +function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean { + if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) { + return buildDigest(rawBody.toString()) === req.headers['digest'] } - const publicKeyOwnerObject = { - '@context': jsig.SECURITY_CONTEXT_URL, - '@id': fromActor.url, - publicKey: [ publicKeyObject ] + return true +} + +function isHTTPSignatureVerified (httpSignatureParsed: any, actor: MActor): boolean { + return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true +} + +function parseHTTPSignature (req: Request, clockSkew?: number) { + const requiredHeaders = req.method === 'POST' + ? [ '(request-target)', 'host', 'digest' ] + : [ '(request-target)', 'host' ] + + const parsed = httpSignature.parse(req, { clockSkew, headers: requiredHeaders }) + + const parsedHeaders = parsed.params.headers + if (!parsedHeaders.includes('date') && !parsedHeaders.includes('(created)')) { + throw new Error(`date or (created) must be included in signature`) } - const options = { - publicKey: publicKeyObject, - publicKeyOwner: publicKeyOwnerObject + return parsed +} + +// --------------------------------------------------------------------------- +// JSONLD +// --------------------------------------------------------------------------- + +function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise { + if (signedDocument.signature.type === 'RsaSignature2017') { + return isJsonLDRSA2017Verified(fromActor, signedDocument) } - return jsig.promises.verify(signedDocument, options) - .catch(err => { - logger.error('Cannot check signature.', err) - return false - }) + logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument) + + return Promise.resolve(false) +} + +// Backward compatibility with "other" implementations +async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) { + const [ documentHash, optionsHash ] = await Promise.all([ + createDocWithoutSignatureHash(signedDocument), + createSignatureHash(signedDocument.signature) + ]) + + const toVerify = optionsHash + documentHash + + const verify = createVerify('RSA-SHA256') + verify.update(toVerify, 'utf8') + + return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64') } -function signObject (byActor: ActorModel, data: any) { - const options = { - privateKeyPem: byActor.privateKey, +async function signJsonLDObject (byActor: MActor, data: T) { + const signature = { + type: 'RsaSignature2017', creator: byActor.url, - algorithm: 'RsaSignature2017' + created: new Date().toISOString() } - return jsig.promises.sign(data, options) + const [ documentHash, optionsHash ] = await Promise.all([ + createDocWithoutSignatureHash(data), + createSignatureHash(signature) + ]) + + const toSign = optionsHash + documentHash + + const sign = createSign('RSA-SHA256') + sign.update(toSign, 'utf8') + + const signatureValue = sign.sign(byActor.privateKey, 'base64') + Object.assign(signature, { signatureValue }) + + return Object.assign(data, { signature }) } -function comparePassword (plainPassword: string, hashPassword: string) { - return bcryptComparePromise(plainPassword, hashPassword) +// --------------------------------------------------------------------------- + +function buildDigest (body: any) { + const rawBody = typeof body === 'string' ? body : JSON.stringify(body) + + return 'SHA-256=' + sha256(rawBody, 'base64') } -async function cryptPassword (password: string) { - const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE) +// --------------------------------------------------------------------------- +// Encryption +// --------------------------------------------------------------------------- - return bcryptHashPromise(password, salt) +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 { - isSignatureVerified, + isHTTPSignatureDigestValid, + parseHTTPSignature, + isHTTPSignatureVerified, + buildDigest, + isJsonLDSignatureVerified, comparePassword, createPrivateAndPublicKeys, cryptPassword, - signObject + signJsonLDObject, + + encrypt, + decrypt +} + +// --------------------------------------------------------------------------- + +function hashObject (obj: any): Promise { + return jsonld.promises.normalize(obj, { + safe: false, + algorithm: 'URDNA2015', + format: 'application/n-quads' + }).then(res => sha256(res)) +} + +function createSignatureHash (signature: any) { + const signatureCopy = cloneDeep(signature) + Object.assign(signatureCopy, { + '@context': [ + 'https://w3id.org/security/v1', + { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' } + ] + }) + + delete signatureCopy.type + delete signatureCopy.id + delete signatureCopy.signatureValue + + return hashObject(signatureCopy) +} + +function createDocWithoutSignatureHash (doc: any) { + const docWithoutSignature = cloneDeep(doc) + delete docWithoutSignature.signature + + return hashObject(docWithoutSignature) }