mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-12 23:35:58 +00:00
173 lines
5.0 KiB
JavaScript
173 lines
5.0 KiB
JavaScript
// import dotenv from 'dotenv'
|
|
// import { resolve } from 'path'
|
|
import crypto from 'crypto'
|
|
import request from 'request'
|
|
import CONFIG from './../../config'
|
|
const debug = require('debug')('ea:security')
|
|
|
|
// TODO Does this reference a local config? Why?
|
|
// dotenv.config({ path: resolve('src', 'activitypub', '.env') })
|
|
|
|
export function generateRsaKeyPair(options = {}) {
|
|
const { passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE } = options
|
|
return crypto.generateKeyPairSync('rsa', {
|
|
modulusLength: 4096,
|
|
publicKeyEncoding: {
|
|
type: 'spki',
|
|
format: 'pem',
|
|
},
|
|
privateKeyEncoding: {
|
|
type: 'pkcs8',
|
|
format: 'pem',
|
|
cipher: 'aes-256-cbc',
|
|
passphrase,
|
|
},
|
|
})
|
|
}
|
|
|
|
// signing
|
|
export function createSignature(options) {
|
|
const {
|
|
privateKey,
|
|
keyId,
|
|
url,
|
|
headers = {},
|
|
algorithm = 'rsa-sha256',
|
|
passphrase = CONFIG.PRIVATE_KEY_PASSPHRASE,
|
|
} = options
|
|
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
|
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
|
|
}
|
|
const signer = crypto.createSign(algorithm)
|
|
const signingString = constructSigningString(url, headers)
|
|
signer.update(signingString)
|
|
const signatureB64 = signer.sign({ key: privateKey, passphrase }, 'base64')
|
|
const headersString = Object.keys(headers).reduce((result, key) => {
|
|
return result + ' ' + key.toLowerCase()
|
|
}, '')
|
|
return `keyId="${keyId}",algorithm="${algorithm}",headers="(request-target)${headersString}",signature="${signatureB64}"`
|
|
}
|
|
|
|
// verifying
|
|
export function verifySignature(url, headers) {
|
|
return new Promise((resolve, reject) => {
|
|
const signatureHeader = headers['signature'] ? headers['signature'] : headers['Signature']
|
|
if (!signatureHeader) {
|
|
debug('No Signature header present!')
|
|
resolve(false)
|
|
}
|
|
debug(`Signature Header = ${signatureHeader}`)
|
|
const signature = extractKeyValueFromSignatureHeader(signatureHeader, 'signature')
|
|
const algorithm = extractKeyValueFromSignatureHeader(signatureHeader, 'algorithm')
|
|
const headersString = extractKeyValueFromSignatureHeader(signatureHeader, 'headers')
|
|
const keyId = extractKeyValueFromSignatureHeader(signatureHeader, 'keyId')
|
|
|
|
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
|
debug('Unsupported hash algorithm specified!')
|
|
resolve(false)
|
|
}
|
|
|
|
const usedHeaders = headersString.split(' ')
|
|
const verifyHeaders = {}
|
|
Object.keys(headers).forEach(key => {
|
|
if (usedHeaders.includes(key.toLowerCase())) {
|
|
verifyHeaders[key.toLowerCase()] = headers[key]
|
|
}
|
|
})
|
|
const signingString = constructSigningString(url, verifyHeaders)
|
|
debug(`keyId= ${keyId}`)
|
|
request(
|
|
{
|
|
url: keyId,
|
|
headers: {
|
|
Accept: 'application/json',
|
|
},
|
|
},
|
|
(err, response, body) => {
|
|
if (err) reject(err)
|
|
debug(`body = ${body}`)
|
|
const actor = JSON.parse(body)
|
|
const publicKeyPem = actor.publicKey.publicKeyPem
|
|
resolve(httpVerify(publicKeyPem, signature, signingString, algorithm))
|
|
},
|
|
)
|
|
})
|
|
}
|
|
|
|
// private: signing
|
|
function constructSigningString(url, headers) {
|
|
const urlObj = new URL(url)
|
|
let signingString = `(request-target): post ${urlObj.pathname}${
|
|
urlObj.search !== '' ? urlObj.search : ''
|
|
}`
|
|
return Object.keys(headers).reduce((result, key) => {
|
|
return result + `\n${key.toLowerCase()}: ${headers[key]}`
|
|
}, signingString)
|
|
}
|
|
|
|
// private: verifying
|
|
function httpVerify(pubKey, signature, signingString, algorithm) {
|
|
if (!SUPPORTED_HASH_ALGORITHMS.includes(algorithm)) {
|
|
throw Error(`SIGNING: Unsupported hashing algorithm = ${algorithm}`)
|
|
}
|
|
const verifier = crypto.createVerify(algorithm)
|
|
verifier.update(signingString)
|
|
return verifier.verify(pubKey, signature, 'base64')
|
|
}
|
|
|
|
// private: verifying
|
|
// This function can be used to extract the signature,headers,algorithm etc. out of the Signature Header.
|
|
// Just pass what you want as key
|
|
function extractKeyValueFromSignatureHeader(signatureHeader, key) {
|
|
const keyString = signatureHeader.split(',').filter(el => {
|
|
return !!el.startsWith(key)
|
|
})[0]
|
|
|
|
let firstEqualIndex = keyString.search('=')
|
|
// When headers are requested add 17 to the index to remove "(request-target) " from the string
|
|
if (key === 'headers') {
|
|
firstEqualIndex += 17
|
|
}
|
|
return keyString.substring(firstEqualIndex + 2, keyString.length - 1)
|
|
}
|
|
|
|
// Obtained from invoking crypto.getHashes()
|
|
export const SUPPORTED_HASH_ALGORITHMS = [
|
|
'rsa-md4',
|
|
'rsa-md5',
|
|
'rsa-mdC2',
|
|
'rsa-ripemd160',
|
|
'rsa-sha1',
|
|
'rsa-sha1-2',
|
|
'rsa-sha224',
|
|
'rsa-sha256',
|
|
'rsa-sha384',
|
|
'rsa-sha512',
|
|
'blake2b512',
|
|
'blake2s256',
|
|
'md4',
|
|
'md4WithRSAEncryption',
|
|
'md5',
|
|
'md5-sha1',
|
|
'md5WithRSAEncryption',
|
|
'mdc2',
|
|
'mdc2WithRSA',
|
|
'ripemd',
|
|
'ripemd160',
|
|
'ripemd160WithRSA',
|
|
'rmd160',
|
|
'sha1',
|
|
'sha1WithRSAEncryption',
|
|
'sha224',
|
|
'sha224WithRSAEncryption',
|
|
'sha256',
|
|
'sha256WithRSAEncryption',
|
|
'sha384',
|
|
'sha384WithRSAEncryption',
|
|
'sha512',
|
|
'sha512WithRSAEncryption',
|
|
'ssl3-md5',
|
|
'ssl3-sha1',
|
|
'whirlpool',
|
|
]
|