UNPKG

oracle-nosqldb

Version:

Node.js driver for Oracle NoSQL Database

262 lines (229 loc) 8.62 kB
/*- * Copyright (c) 2018, 2024 Oracle and/or its affiliates. All rights reserved. * * Licensed under the Universal Permissive License v 1.0 as shown at * https://oss.oracle.com/licenses/upl/ */ const util = require('util'); const fs = require('fs'); const crypto = require('crypto'); const tls = require('tls'); const net = require('net'); const NoSQLAuthorizationError = require('../../error').NoSQLAuthorizationError; const promisified = require('../../utils').promisified; const clearData = require('../../utils').clearData; /* Signing algorithm only rsa-sha256 is allowed */ const ALG_RSA = 'rsa-sha256'; /* Signature algorithm */ const ALG_SIGN = 'sha256WithRSAEncryption'; /* OCI signature version only version 1 is allowed*/ const SIGNATURE_VERSION = 1; /* * <ocid>.<resource-type>.<realm>. <region>(.future-extensibility) * .<resource-type-specific-id> * pattern is relaxed other than the required <ocid> and * <resource-type-specific-id> */ const OCID_PATTERN = /^([0-9a-zA-Z-_]+[.:])([0-9a-zA-Z-_]*[.:]){3,}([0-9a-zA-Z-_]+)$/; class Utils { static get isInBrowser() { return false; } static async _sign(signingContent, privateKey) { const sign = crypto.createSign(ALG_SIGN); sign.update(signingContent); return sign.sign(privateKey, 'base64'); } static signatureHeader(signingHeaders, keyId, signature) { return `Signature headers="${signingHeaders}",keyId="${keyId}",\ algorithm="${ALG_RSA}",signature="${signature}",\ version="${SIGNATURE_VERSION}"`; } static async sign(signingContent, privateKey, desc) { try { return this._sign(signingContent, privateKey); } catch(err) { throw NoSQLAuthorizationError.illegalState( `Error signing ${desc}: ${err.message}`, err); } } static isValidOcid(ocid) { return typeof ocid === 'string' && ocid.match(OCID_PATTERN); } static privateKeyFromPEM(key, passphrase, fileName) { try { return crypto.createPrivateKey({ key, format: 'pem', passphrase: passphrase ? passphrase : undefined }); } catch(err) { throw NoSQLAuthorizationError.invalidArg('Error creating \ private key' + (fileName ? ` from file ${fileName}` : ''), err); } } static async privateKeyFromPEMFile(keyFile, pass, passIsFile) { let key; try { key = await promisified(null, fs.readFile, keyFile); } catch(err) { throw NoSQLAuthorizationError.invalidArg(`Error reading private \ key from file ${keyFile}: ${err.message}`, err); } let pkPass; if (pass != null) { if (passIsFile) { try { pkPass = await promisified(null, fs.readFile, pass); } catch(err) { clearData(key); throw NoSQLAuthorizationError.invalidArg(`Error reading \ private key passphrase from file ${pass}: ${err.message}`, err); } } else { pkPass = pass; } } try { return Utils.privateKeyFromPEM(key, pkPass, keyFile); } finally { clearData(key); if (pkPass != null && passIsFile) { clearData(pass); } } } static parseSecurityToken(value, fileName, tokenName = 'security token') { const token = { value }; const parts = value.split('.'); if (parts.length < 3) { throw NoSQLAuthorizationError.invalidArg('Invalid ' + tokenName + ' value' + (fileName ? ` from file ${fileName}` : '') + `, number of parts: \ ${parts.length} (should be >= 3)`); } try { const claimsStr = Buffer.from(parts[1], 'base64').toString(); token.claims = JSON.parse(claimsStr); return token; } catch(err) { throw NoSQLAuthorizationError.invalidArg('Error parsing ' + tokenName + (fileName ? ` from file ${fileName}` : ''), err); } } //expireBeforeMs is to account for token acquisition time and possibly //clock difference between client and server. static getSecurityTokenExpiration(token, expireBeforeMs = 0) { return Number(token.claims.exp) * 1000 - expireBeforeMs; } //We don't currently do the check done in OCI SDK SecurityTokenAdapter //that compares public key in jwt's jwk with current public key because //no easy way to create public key from jwk in Node.js currently. If //such mismatch occurs, the driver request will fail with //INVALID_AUTHORIZATION and we will retry it after refreshing the token. static isSecurityTokenValid(token, expireBeforeMs) { const exp = this.getSecurityTokenExpiration(token, expireBeforeMs); const now = Date.now(); return Number.isFinite(exp) && now < exp; } static parseCert(pemCert) { const secureContext = tls.createSecureContext({ cert: pemCert }); const sock = new tls.TLSSocket(new net.Socket(), { secureContext }); const cert = sock.getCertificate(); sock.destroy(); return cert; } static getSubjRDNValue(rdn, key) { if (rdn == null) { return null; } const prefix = key + ':'; if (!Array.isArray(rdn)) { rdn = [ rdn ]; } for(let kv of rdn) { if (typeof kv !== 'string') { throw NoSQLAuthorizationError.illegalState('Invalid RDN \ value in instance certificate subject name: ' + util.inspect(kv)); } if (kv.startsWith(prefix)) { return kv.substring(prefix.length); } } return null; } static getTenantIdFromInstanceCert(cert) { if (cert.subject == null) { throw NoSQLAuthorizationError.illegalState('Invalid instance \ certificate, missing subject'); } let tenantId = this.getSubjRDNValue(cert.subject.OU, 'opc-tenant'); if (tenantId == null) { tenantId = this.getSubjRDNValue(cert.subject.O, 'opc-identity'); } if (tenantId == null) { throw NoSQLAuthorizationError.illegalState('Instance certificate \ does not contain tenant id'); } return tenantId; } static generateRSAKeyPair() { return new Promise((resolve, reject) => { crypto.generateKeyPair('rsa', { modulusLength: 2048 }, (err, publicKey, privateKey) => { if (err) { return reject(err); } return resolve({ publicKey, privateKey }); }); }); } static pemCert2derB64(pem) { //remove header and footer return pem.replace('-----BEGIN CERTIFICATE-----', '') .replace('-----END CERTIFICATE-----', ''); } static pemCert2der(pem) { return Buffer.from(Utils.pemCert2derB64(pem), 'base64'); } static sha256digest(data) { const hash = crypto.createHash('sha256'); hash.update(data); return hash.digest('base64'); } static fingerprintFromPemCert(pemCert) { const derCert = Utils.pemCert2der(pemCert); const hash = crypto.createHash('sha1'); hash.update(derCert); const raw = hash.digest('hex'); return raw.match(/.{2}/g).join(':'); } static async generateOpcRequestId() { //To avoid blocking event loop, recommended using async version of //crypto.randomBytes. const buf = await promisified(null, crypto.randomBytes, 16); return buf.toString('hex').toUpperCase(); } static publicKey2B64(publicKey) { return publicKey.export({ type: 'spki', format: 'der', }).toString('base64'); } //Parse JSON response that only has one field "token": { "token": "...."} static parseTokenResponse(res, source = 'authorization server') { try { res = JSON.parse(res); } catch(err) { throw NoSQLAuthorizationError.badProto(`Error parsing security \ token response "${res}" from ${source}: ${err.message}`, err); } if (typeof res.token !== 'string') { throw NoSQLAuthorizationError.badProto(`Missing or invalid \ security token in ${source} response: "${res}"`); } return res.token; } } module.exports = Utils;