peertube-crypto.ts 4.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. import { compare, genSalt, hash } from 'bcrypt'
  2. import { createSign, createVerify } from 'crypto'
  3. import { Request } from 'express'
  4. import { cloneDeep } from 'lodash'
  5. import { sha256 } from '@shared/extra-utils'
  6. import { BCRYPT_SALT_SIZE, HTTP_SIGNATURE, PRIVATE_RSA_KEY_SIZE } from '../initializers/constants'
  7. import { MActor } from '../types/models'
  8. import { createPrivateKey, getPublicKey, promisify1, promisify2 } from './core-utils'
  9. import { jsonld } from './custom-jsonld-signature'
  10. import { logger } from './logger'
  11. const bcryptComparePromise = promisify2<any, string, boolean>(compare)
  12. const bcryptGenSaltPromise = promisify1<number, string>(genSalt)
  13. const bcryptHashPromise = promisify2<any, string | number, string>(hash)
  14. const httpSignature = require('@peertube/http-signature')
  15. async function createPrivateAndPublicKeys () {
  16. logger.info('Generating a RSA key...')
  17. const { key } = await createPrivateKey(PRIVATE_RSA_KEY_SIZE)
  18. const { publicKey } = await getPublicKey(key)
  19. return { privateKey: key, publicKey }
  20. }
  21. // User password checks
  22. function comparePassword (plainPassword: string, hashPassword: string) {
  23. return bcryptComparePromise(plainPassword, hashPassword)
  24. }
  25. async function cryptPassword (password: string) {
  26. const salt = await bcryptGenSaltPromise(BCRYPT_SALT_SIZE)
  27. return bcryptHashPromise(password, salt)
  28. }
  29. // HTTP Signature
  30. function isHTTPSignatureDigestValid (rawBody: Buffer, req: Request): boolean {
  31. if (req.headers[HTTP_SIGNATURE.HEADER_NAME] && req.headers['digest']) {
  32. return buildDigest(rawBody.toString()) === req.headers['digest']
  33. }
  34. return true
  35. }
  36. function isHTTPSignatureVerified (httpSignatureParsed: any, actor: MActor): boolean {
  37. return httpSignature.verifySignature(httpSignatureParsed, actor.publicKey) === true
  38. }
  39. function parseHTTPSignature (req: Request, clockSkew?: number) {
  40. const headers = req.method === 'POST'
  41. ? HTTP_SIGNATURE.REQUIRED_HEADERS.POST
  42. : HTTP_SIGNATURE.REQUIRED_HEADERS.ALL
  43. return httpSignature.parse(req, { clockSkew, headers })
  44. }
  45. // JSONLD
  46. function isJsonLDSignatureVerified (fromActor: MActor, signedDocument: any): Promise<boolean> {
  47. if (signedDocument.signature.type === 'RsaSignature2017') {
  48. return isJsonLDRSA2017Verified(fromActor, signedDocument)
  49. }
  50. logger.warn('Unknown JSON LD signature %s.', signedDocument.signature.type, signedDocument)
  51. return Promise.resolve(false)
  52. }
  53. // Backward compatibility with "other" implementations
  54. async function isJsonLDRSA2017Verified (fromActor: MActor, signedDocument: any) {
  55. const [ documentHash, optionsHash ] = await Promise.all([
  56. createDocWithoutSignatureHash(signedDocument),
  57. createSignatureHash(signedDocument.signature)
  58. ])
  59. const toVerify = optionsHash + documentHash
  60. const verify = createVerify('RSA-SHA256')
  61. verify.update(toVerify, 'utf8')
  62. return verify.verify(fromActor.publicKey, signedDocument.signature.signatureValue, 'base64')
  63. }
  64. async function signJsonLDObject <T> (byActor: MActor, data: T) {
  65. const signature = {
  66. type: 'RsaSignature2017',
  67. creator: byActor.url,
  68. created: new Date().toISOString()
  69. }
  70. const [ documentHash, optionsHash ] = await Promise.all([
  71. createDocWithoutSignatureHash(data),
  72. createSignatureHash(signature)
  73. ])
  74. const toSign = optionsHash + documentHash
  75. const sign = createSign('RSA-SHA256')
  76. sign.update(toSign, 'utf8')
  77. const signatureValue = sign.sign(byActor.privateKey, 'base64')
  78. Object.assign(signature, { signatureValue })
  79. return Object.assign(data, { signature })
  80. }
  81. function buildDigest (body: any) {
  82. const rawBody = typeof body === 'string' ? body : JSON.stringify(body)
  83. return 'SHA-256=' + sha256(rawBody, 'base64')
  84. }
  85. // ---------------------------------------------------------------------------
  86. export {
  87. isHTTPSignatureDigestValid,
  88. parseHTTPSignature,
  89. isHTTPSignatureVerified,
  90. buildDigest,
  91. isJsonLDSignatureVerified,
  92. comparePassword,
  93. createPrivateAndPublicKeys,
  94. cryptPassword,
  95. signJsonLDObject
  96. }
  97. // ---------------------------------------------------------------------------
  98. function hashObject (obj: any): Promise<any> {
  99. return jsonld.promises
  100. .normalize(obj, {
  101. algorithm: 'URDNA2015',
  102. format: 'application/n-quads'
  103. })
  104. .then(res => sha256(res))
  105. }
  106. function createSignatureHash (signature: any) {
  107. const signatureCopy = cloneDeep(signature)
  108. Object.assign(signatureCopy, {
  109. '@context': [
  110. 'https://w3id.org/security/v1',
  111. { RsaSignature2017: 'https://w3id.org/security#RsaSignature2017' }
  112. ]
  113. })
  114. delete signatureCopy.type
  115. delete signatureCopy.id
  116. delete signatureCopy.signatureValue
  117. return hashObject(signatureCopy)
  118. }
  119. function createDocWithoutSignatureHash (doc: any) {
  120. const docWithoutSignature = cloneDeep(doc)
  121. delete docWithoutSignature.signature
  122. return hashObject(docWithoutSignature)
  123. }