@evervault/sdk
Advanced tools
Comparing version 4.3.0 to 5.0.0
const { version } = require('../package.json'); | ||
const DEFAULT_API_URL = 'https://api.evervault.com'; | ||
const DEFAULT_FUNCTION_RUN_URL = 'https://run.evervault.com'; | ||
const DEFAULT_TUNNEL_HOSTNAME = 'https://relay.evervault.com:443'; | ||
const DEFAULT_CA_HOSTNAME = 'https://ca.evervault.com'; | ||
const DEFAULT_CAGES_CA_HOSTNAME = 'https://cages-ca.evervault.com'; | ||
const DEFAULT_CAGES_HOSTNAME = 'cages.evervault.com'; | ||
const DEFAULT_CAGES_HOSTNAME = 'cage.evervault.com'; | ||
const DEFAULT_POLL_INTERVAL = 5; | ||
const DEFAULT_MAX_FILE_SIZE_IN_MB = 25; | ||
const DEFAULT_ATTEST_POLL_INTERVAL = 7200; | ||
const DEFAULT_ATTEST_POLL_INTERVAL = 300; | ||
const DEFAULT_PCR_PROVIDER_POLL_INTERVAL = 300; | ||
@@ -16,3 +16,2 @@ module.exports = () => ({ | ||
baseUrl: process.env.EV_API_URL || DEFAULT_API_URL, | ||
functionRunUrl: process.env.EV_CAGE_RUN_URL || DEFAULT_FUNCTION_RUN_URL, | ||
userAgent: `evervault-node/${version}`, | ||
@@ -27,2 +26,5 @@ tunnelHostname: process.env.EV_TUNNEL_HOSTNAME || DEFAULT_TUNNEL_HOSTNAME, | ||
process.env.EV_ATTEST_POLL_INTERVAL || DEFAULT_ATTEST_POLL_INTERVAL, | ||
pcrProviderPollInterval: | ||
process.env.EV_PCR_PROVIDER_POLL_INTERVAL || | ||
DEFAULT_PCR_PROVIDER_POLL_INTERVAL, | ||
}, | ||
@@ -38,2 +40,3 @@ encryption: { | ||
evVersion: 'DUB', | ||
evVersionWithMetadata: 'BRU', | ||
header: { | ||
@@ -54,2 +57,3 @@ iss: 'evervault', | ||
evVersion: 'NOC', | ||
evVersionWithMetadata: 'LCY', | ||
header: { | ||
@@ -56,0 +60,0 @@ iss: 'evervault', |
@@ -39,7 +39,6 @@ const RepeatedTimer = require('./repeatedTimer'); | ||
get = async (cageName) => { | ||
get = (cageName) => { | ||
const doc = this.attestationDocCache[cageName]; | ||
if (!doc) { | ||
await this.loadCageDoc(cageName); | ||
return this.attestationDocCache[cageName]; | ||
console.warn(`No attestation doc found for ${cageName}`); | ||
} | ||
@@ -65,5 +64,5 @@ return doc; | ||
await Promise.all( | ||
this.cages.map(async (cageName) => | ||
this.loadCageDoc(cageName, this.appUuid) | ||
) | ||
this.cages.map(async (cageName) => { | ||
await this.loadCageDoc(cageName, this.appUuid); | ||
}) | ||
); | ||
@@ -70,0 +69,0 @@ }; |
const crypto = require('crypto'); | ||
const { P256 } = require('../curves'); | ||
const { Encoding } = require('../curves'); | ||
const Datatypes = require('../utils/datatypes'); | ||
@@ -8,2 +8,3 @@ const { errors } = require('../utils'); | ||
const PRIME256V1 = 'prime256v1'; | ||
const SECP256K1 = 'secp256k1'; | ||
@@ -30,3 +31,4 @@ const generateBytes = (byteLength) => { | ||
derivedSecret, | ||
data | ||
data, | ||
role | ||
) => { | ||
@@ -38,3 +40,4 @@ return await _traverseObject( | ||
derivedSecret, | ||
{ ...data } | ||
{ ...data }, | ||
role | ||
); | ||
@@ -47,3 +50,4 @@ }; | ||
derivedSecret, | ||
data | ||
data, | ||
role | ||
) => { | ||
@@ -57,3 +61,4 @@ if (Datatypes.isEncryptable(data)) { | ||
Datatypes.ensureString(data), | ||
Datatypes.getHeaderType(data) | ||
Datatypes.getHeaderType(data), | ||
role | ||
); | ||
@@ -68,3 +73,4 @@ } else if (Datatypes.isObjectStrict(data)) { | ||
derivedSecret, | ||
value | ||
value, | ||
role | ||
); | ||
@@ -81,3 +87,4 @@ } | ||
derivedSecret, | ||
value | ||
value, | ||
role | ||
); | ||
@@ -95,7 +102,7 @@ } | ||
const getSharedSecret = (ecdh, publicKey, ephemeralPublicKey) => { | ||
const getSharedSecret = (ecdh, publicKey, ephemeralPublicKey, curveName) => { | ||
const secret = ecdh.computeSecret(Buffer.from(publicKey, 'base64')); | ||
const uncompressedKey = crypto.ECDH.convertKey( | ||
ephemeralPublicKey, | ||
'prime256v1', | ||
curveName, | ||
'base64', | ||
@@ -108,3 +115,3 @@ 'base64', | ||
Buffer.from([0x00, 0x00, 0x00, 0x01]), | ||
P256.encodePublicKey(uncompressedKey), | ||
Encoding.encodePublicKey(curveName, uncompressedKey), | ||
]); | ||
@@ -123,6 +130,6 @@ | ||
str, | ||
datatype | ||
datatype, | ||
role | ||
) => { | ||
const keyIv = await generateBytes(config.ivLength); | ||
const cipher = crypto.createCipheriv( | ||
@@ -136,9 +143,10 @@ config.cipherAlgorithm, | ||
); | ||
if (curve === PRIME256V1) { | ||
if (curve === PRIME256V1 || (curve === SECP256K1 && role)) { | ||
cipher.setAAD(Buffer.from(ecdhTeamKey, 'base64')); | ||
} | ||
const result = buildCipherBuffer(str, role); | ||
const encryptedBuffer = Buffer.concat([ | ||
cipher.update(str, 'utf8'), | ||
cipher.update(result), | ||
cipher.final(), | ||
@@ -152,14 +160,80 @@ cipher.getAuthTag(), | ||
ecdhPublicKey, | ||
encryptedBuffer.toString('base64') | ||
encryptedBuffer.toString('base64'), | ||
role | ||
); | ||
}; | ||
const _evVersionPrefix = base64RemovePadding( | ||
Buffer.from(config.evVersion).toString('base64') | ||
); | ||
const buildEncodedMetadata = (role, encryptionTimestamp) => { | ||
let buffer = []; | ||
const _evEncryptedFileVersion = () => { | ||
// Binary representation of a fixed map with 2 or 3 items, followed by the key-value pairs. | ||
buffer.push(0x80 | (!role ? 2 : 3)); | ||
if (role) { | ||
// `dr` (data role) => role_name | ||
// Binary representation for a fixed string of length 2, followed by `dr` | ||
buffer.push(0xa2); | ||
buffer.push(...'dr'.split('').map((c) => c.charCodeAt(0))); | ||
// Binary representation for a fixed string of role name length, followed by the role name itself. | ||
buffer.push(0xa0 | role.length); | ||
buffer.push(...role.split('').map((c) => c.charCodeAt(0))); | ||
} | ||
// "eo" (encryption origin) => 5 (Node SDK) | ||
// Binary representation for a fixed string of length 2, followed by `eo` | ||
buffer.push(0xa2); | ||
buffer.push(...'eo'.split('').map((c) => c.charCodeAt(0))); | ||
// Binary representation for the integer 5 | ||
buffer.push(5); | ||
// "et" (encryption timestamp) => current time | ||
// Binary representation for a fixed string of length 2, followed by `et` | ||
buffer.push(0xa2); | ||
buffer.push(...'et'.split('').map((c) => c.charCodeAt(0))); | ||
// Binary representation for a 4-byte unsigned integer (uint 32), followed by the epoch time | ||
buffer.push(0xce); | ||
buffer.push((encryptionTimestamp >> 24) & 0xff); | ||
buffer.push((encryptionTimestamp >> 16) & 0xff); | ||
buffer.push((encryptionTimestamp >> 8) & 0xff); | ||
buffer.push(encryptionTimestamp & 0xff); | ||
return Buffer.from(buffer); | ||
}; | ||
const buildCipherBuffer = (data, role) => { | ||
let result; | ||
if (role) { | ||
const metadataBytes = buildEncodedMetadata( | ||
role, | ||
Math.floor(new Date().getTime() / 1000) | ||
); | ||
let offsetBuffer = Buffer.allocUnsafe(2); | ||
offsetBuffer.writeUInt16LE(metadataBytes.length); | ||
result = Buffer.concat([offsetBuffer, metadataBytes, Buffer.from(data)]); | ||
} else { | ||
result = Buffer.from(data); | ||
} | ||
return result; | ||
}; | ||
const _evVersionPrefix = (role) => | ||
base64RemovePadding( | ||
Buffer.from( | ||
role ? config.evVersionWithMetadata : config.evVersion | ||
).toString('base64') | ||
); | ||
const _evEncryptedFileVersion = (hasMetadata) => { | ||
if (config.ecdhCurve == 'secp256k1') { | ||
if (hasMetadata) { | ||
return Buffer.from([0x04]); | ||
} | ||
return Buffer.from([0x02]); | ||
} else if (config.ecdhCurve === 'prime256v1') { | ||
if (hasMetadata) { | ||
return Buffer.from([0x05]); | ||
} | ||
return Buffer.from([0x03]); | ||
@@ -175,5 +249,6 @@ } else { | ||
ecdhPublicKey, | ||
encryptedData | ||
encryptedData, | ||
role | ||
) => { | ||
return `ev:${_evVersionPrefix}${ | ||
return `ev:${_evVersionPrefix(role)}${ | ||
datatype !== 'string' ? ':' + datatype : '' | ||
@@ -185,2 +260,29 @@ }:${base64RemovePadding(keyIv)}:${base64RemovePadding( | ||
const _encryptBytes = ( | ||
data, | ||
setAuthData, | ||
derivedSecret, | ||
ecdhTeamKey, | ||
keyIv | ||
) => { | ||
const cipher = crypto.createCipheriv( | ||
config.cipherAlgorithm, | ||
derivedSecret, | ||
keyIv, | ||
{ | ||
authTagLength: config.authTagLength, | ||
} | ||
); | ||
if (setAuthData) { | ||
cipher.setAAD(Buffer.from(ecdhTeamKey, 'base64')); | ||
} | ||
return Buffer.concat([ | ||
cipher.update(data), | ||
cipher.final(), | ||
cipher.getAuthTag(), | ||
]); | ||
}; | ||
const _encryptFile = async ( | ||
@@ -191,3 +293,4 @@ curve, | ||
derivedSecret, | ||
data | ||
data, | ||
role | ||
) => { | ||
@@ -202,33 +305,60 @@ const fileSizeInBytes = data.length; | ||
const cipher = crypto.createCipheriv( | ||
config.cipherAlgorithm, | ||
const setAuthData = curve === PRIME256V1 || role ? true : false; | ||
const encryptedBuffer = _encryptBytes( | ||
data, | ||
setAuthData, | ||
derivedSecret, | ||
keyIv, | ||
{ | ||
authTagLength: config.authTagLength, | ||
} | ||
ecdhTeamKey, | ||
keyIv | ||
); | ||
if (curve === PRIME256V1) { | ||
cipher.setAAD(Buffer.from(ecdhTeamKey, 'base64')); | ||
let encryptedMetadataBytes; | ||
if (role) { | ||
const metadataBytes = buildEncodedMetadata( | ||
role, | ||
Math.floor(new Date().getTime() / 1000) | ||
); | ||
encryptedMetadataBytes = _encryptBytes( | ||
metadataBytes, | ||
setAuthData, | ||
derivedSecret, | ||
ecdhTeamKey, | ||
keyIv | ||
); | ||
} | ||
const encryptedBuffer = Buffer.concat([ | ||
cipher.update(data), | ||
cipher.final(), | ||
cipher.getAuthTag(), | ||
]); | ||
return _formatFile( | ||
keyIv, | ||
ecdhPublicKey, | ||
encryptedBuffer, | ||
encryptedMetadataBytes | ||
); | ||
}; | ||
return _formatFile(keyIv, ecdhPublicKey, encryptedBuffer); | ||
const _calculateOffsetToData = (encryptedMetadataBytes) => { | ||
if (encryptedMetadataBytes) { | ||
let offsetBuffer = Buffer.allocUnsafe(2); | ||
offsetBuffer.writeUInt16LE(55 + 2 + encryptedMetadataBytes.length); // headers + metadata offset size + metadata offset | ||
return offsetBuffer; | ||
} else { | ||
return Buffer.from([0x37, 0x00]); // 55 bytes to starting byte of data if no metadtata | ||
} | ||
}; | ||
const _formatFile = async (keyIv, ecdhPublicKey, encryptedData) => { | ||
const _formatFile = async ( | ||
keyIv, | ||
ecdhPublicKey, | ||
encryptedData, | ||
encryptedMetadataBytes = undefined | ||
) => { | ||
const evEncryptedFileIdentifier = Buffer.from([ | ||
0x25, 0x45, 0x56, 0x45, 0x4e, 0x43, | ||
]); | ||
const versionNumber = _evEncryptedFileVersion(); | ||
const offsetToData = Buffer.from([0x37, 0x00]); | ||
const hasMetadata = encryptedMetadataBytes !== undefined; | ||
const versionNumber = _evEncryptedFileVersion(hasMetadata); | ||
const offsetToData = _calculateOffsetToData(encryptedMetadataBytes); | ||
const flags = Buffer.from([0x00]); | ||
const fileContents = Buffer.concat([ | ||
const fileHeaders = Buffer.concat([ | ||
evEncryptedFileIdentifier, | ||
@@ -240,5 +370,18 @@ versionNumber, | ||
flags, | ||
Buffer.from(encryptedData), | ||
]); | ||
let fileContents; | ||
if (encryptedMetadataBytes) { | ||
let metadataOffsetBuffer = Buffer.allocUnsafe(2); | ||
metadataOffsetBuffer.writeUInt16LE(encryptedMetadataBytes.length); | ||
fileContents = Buffer.concat([ | ||
fileHeaders, | ||
metadataOffsetBuffer, | ||
encryptedMetadataBytes, | ||
Buffer.from(encryptedData), | ||
]); | ||
} else { | ||
fileContents = Buffer.concat([fileHeaders, Buffer.from(encryptedData)]); | ||
} | ||
const crc32Hash = CRC32(fileContents); | ||
@@ -258,2 +401,3 @@ | ||
data, | ||
role = undefined, | ||
options = DEFAULT_ENCRYPT_OPTIONS | ||
@@ -271,3 +415,4 @@ ) => { | ||
derivedSecret, | ||
data | ||
data, | ||
role | ||
); | ||
@@ -281,2 +426,3 @@ } else if (Datatypes.isObjectStrict(data)) { | ||
data, | ||
role, | ||
options | ||
@@ -290,3 +436,4 @@ ); | ||
derivedSecret, | ||
[...data] | ||
[...data], | ||
role | ||
); | ||
@@ -300,3 +447,4 @@ } else if (Datatypes.isEncryptable(data)) { | ||
Datatypes.ensureString(data), | ||
Datatypes.getHeaderType(data) | ||
Datatypes.getHeaderType(data), | ||
role | ||
); | ||
@@ -308,3 +456,9 @@ } else { | ||
return { encrypt, getSharedSecret, generateBytes }; | ||
return { | ||
encrypt, | ||
getSharedSecret, | ||
generateBytes, | ||
buildCipherBuffer, | ||
buildEncodedMetadata, | ||
}; | ||
}; |
@@ -25,18 +25,9 @@ const { errors, Datatypes } = require('../utils'); | ||
} | ||
if (!additionalHeaders['x-async']) { | ||
return phin({ | ||
url: path.startsWith('https://') ? path : `${config.baseUrl}/${path}`, | ||
method, | ||
headers, | ||
data, | ||
parse, | ||
}); | ||
} else { | ||
return phin({ | ||
url: path.startsWith('https://') ? path : `${config.baseUrl}/${path}`, | ||
method, | ||
headers, | ||
data, | ||
}); | ||
} | ||
return phin({ | ||
url: path.startsWith('https://') ? path : `${config.baseUrl}/${path}`, | ||
method, | ||
headers, | ||
data, | ||
parse, | ||
}); | ||
}; | ||
@@ -57,3 +48,3 @@ | ||
return await get('cages/key', {}, true).catch((_e) => { | ||
throw new errors.CageKeyError( | ||
throw new errors.EvervaultError( | ||
"An error occurred while retrieving the cage's key" | ||
@@ -67,3 +58,3 @@ ); | ||
} | ||
throw errors.mapApiResponseToError(response); | ||
throw errors.mapResponseCodeToError(response); | ||
}; | ||
@@ -86,3 +77,3 @@ | ||
.catch((err) => { | ||
throw new errors.CertError( | ||
throw new errors.EvervaultError( | ||
`Unable to download cert from ${config.certHostname} (${err.message})` | ||
@@ -109,3 +100,3 @@ ); | ||
.catch((err) => { | ||
throw new errors.CertError( | ||
throw new errors.EvervaultError( | ||
`Unable to download cert from ${config.cagesCertHostname} (${err.message})` | ||
@@ -133,3 +124,3 @@ ); | ||
.catch((err) => { | ||
throw new errors.CertError( | ||
throw new errors.EvervaultError( | ||
`Unable to download attestation doc from ${url} (${err.message})` | ||
@@ -143,3 +134,3 @@ ); | ||
const response = await get('v2/relay-outbound').catch((e) => { | ||
throw new errors.RelayOutboundConfigError( | ||
throw new errors.EvervaultError( | ||
`An error occoured while retrieving the Relay Outbound configuration: ${e}` | ||
@@ -157,28 +148,27 @@ ); | ||
} | ||
throw errors.mapApiResponseToError(response); | ||
throw errors.mapResponseCodeToError(response); | ||
}; | ||
const buildRunHeaders = ({ version, async }) => { | ||
const headers = {}; | ||
if (version) { | ||
headers['x-version-id'] = version; | ||
} | ||
if (async) { | ||
headers['x-async'] = 'true'; | ||
} | ||
return headers; | ||
}; | ||
const runCage = (cageName, payload, options = {}) => { | ||
const optionalHeaders = buildRunHeaders(options); | ||
return post( | ||
`${config.functionRunUrl}/${cageName}`, | ||
const runFunction = async (functionName, payload) => { | ||
const response = await post( | ||
`${config.baseUrl}/functions/${functionName}/runs`, | ||
{ | ||
...payload, | ||
payload, | ||
}, | ||
{ | ||
'Content-Type': 'application/json', | ||
...optionalHeaders, | ||
}, | ||
true | ||
); | ||
if (response.statusCode >= 200 && response.statusCode < 300) { | ||
const responseBody = response.body; | ||
if (responseBody.status === 'success') { | ||
return response; | ||
} | ||
); | ||
throw errors.mapFunctionFailureResponseToError(responseBody); | ||
} | ||
const responseBody = response.body; | ||
throw errors.mapApiResponseToError(responseBody); | ||
}; | ||
@@ -210,3 +200,2 @@ | ||
} catch (e) { | ||
console.error(`Attempt ${retryCount + 1} failed: ${e.message}`); | ||
retryCount++; | ||
@@ -250,3 +239,6 @@ await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); | ||
} | ||
throw errors.mapApiResponseToError(response); | ||
const resBody = Buffer.isBuffer(response.body) | ||
? JSON.parse(response.body.toString()) | ||
: response.body; | ||
throw errors.mapApiResponseToError(resBody); | ||
}; | ||
@@ -295,3 +287,3 @@ | ||
} | ||
throw errors.mapApiResponseToError(response); | ||
throw errors.mapApiResponseToError(response.body); | ||
}; | ||
@@ -301,3 +293,3 @@ | ||
getCageKey, | ||
runCage, | ||
runFunction, | ||
getCert, | ||
@@ -304,0 +296,0 @@ getCagesCert, |
@@ -7,2 +7,3 @@ module.exports = { | ||
AttestationDoc: require('./cageAttestationDoc'), | ||
CagePcrManager: require('./cagePcrManager'), | ||
}; |
module.exports = ({ http, crypto, run }) => { | ||
return { | ||
fetch: async (url, options) => { | ||
const { body } = await http.runCage('proxy-cage', { | ||
const { body } = await http.runFunction('proxy-cage', { | ||
url, | ||
@@ -6,0 +6,0 @@ options, |
@@ -1,3 +0,1 @@ | ||
const { InitializationError } = require('../utils/errors'); | ||
module.exports = (defaultInterval, cb) => { | ||
@@ -4,0 +2,0 @@ const createInterval = () => { |
const crypto = require('crypto'); | ||
const ASN1 = require('uasn1'); | ||
const curveConstants = require('./constants'); | ||
@@ -10,8 +11,8 @@ /** | ||
*/ | ||
const createCurve = (curveName, curveValues) => { | ||
const asn1Encoder = buildEncoder(curveValues); | ||
return (compressedPubKey) => { | ||
const createCurve = () => { | ||
return (curveName, compressedPubKey) => { | ||
const asn1Encoder = buildEncoder(curveName); | ||
const decompressed = crypto.ECDH.convertKey( | ||
compressedPubKey, | ||
'prime256v1', | ||
curveName, | ||
'base64', | ||
@@ -25,3 +26,9 @@ 'hex', | ||
const buildEncoder = ({ p, a, b, seed, generator, n, h }) => { | ||
/** | ||
* The seed parameter is optional according to the X9.62 standard | ||
* for DER encoding public keys | ||
* https://www.bsi.bund.de/SharedDocs/Downloads/EN/BSI/Publications/TechGuidelines/TR03111/BSI-TR-03111_V-2-0_pdf.pdf?__blob=publicationFile&v=1 | ||
*/ | ||
const buildEncoder = (curveName) => { | ||
const curveParams = curveConstants[curveName]; | ||
return (decompressedKey) => { | ||
@@ -44,19 +51,27 @@ const hexEncodedKey = ASN1( | ||
// curve p value | ||
ASN1.UInt(p) | ||
ASN1.UInt(curveParams.p) | ||
), | ||
ASN1( | ||
'30', | ||
// curve a value | ||
ASN1('04', a), | ||
// curve b value | ||
ASN1('04', b), | ||
// curve seed value | ||
ASN1.BitStr(seed) | ||
), | ||
curveParams.seed | ||
? ASN1( | ||
'30', | ||
// curve a value | ||
ASN1('04', curveParams.a), | ||
// curve b value | ||
ASN1('04', curveParams.b), | ||
// curve seed value | ||
ASN1.BitStr(curveParams.seed) | ||
) | ||
: ASN1( | ||
'30', | ||
// curve a value | ||
ASN1('04', curveParams.a), | ||
// curve b value | ||
ASN1('04', curveParams.b) | ||
), | ||
// curve generate point in decompressed form | ||
ASN1('04', generator), | ||
ASN1('04', curveParams.generator), | ||
// curve n value | ||
ASN1.UInt(n), | ||
ASN1.UInt(curveParams.n), | ||
// curve h value | ||
ASN1.UInt(h) | ||
ASN1.UInt(curveParams.h) | ||
) | ||
@@ -73,3 +88,3 @@ ), | ||
module.exports = { | ||
createCurve, | ||
encodePublicKey: createCurve(), | ||
}; |
module.exports = { | ||
P256: require('./p256'), | ||
Encoding: require('./base'), | ||
}; |
143
lib/index.js
@@ -15,3 +15,9 @@ const crypto = require('crypto'); | ||
const Config = require('./config'); | ||
const { Crypto, Http, RelayOutboundConfig, AttestationDoc } = require('./core'); | ||
const { | ||
Crypto, | ||
Http, | ||
RelayOutboundConfig, | ||
AttestationDoc, | ||
CagePcrManager, | ||
} = require('./core'); | ||
const { TokenCreationError } = require('./utils/errors'); | ||
@@ -35,3 +41,3 @@ const console = require('console'); | ||
) { | ||
throw new errors.InitializationError( | ||
throw new errors.EvervaultError( | ||
'The provided App ID is invalid. The App ID can be retrieved in the Evervault dashboard (App Settings).' | ||
@@ -94,2 +100,3 @@ ); | ||
if (cageAttest.hasAttestationBindings()) { | ||
//Store attestation documents from cages in cache | ||
let attestationCache = new AttestationDoc( | ||
@@ -101,7 +108,17 @@ this.config, | ||
); | ||
await attestationCache.init(); | ||
//Store client PCR providers to periodically pull new PCRs | ||
const cagePcrManager = new CagePcrManager( | ||
this.config, | ||
cagesAttestationData | ||
); | ||
await cagePcrManager.init(); | ||
cageAttest.addAttestationListener( | ||
this.config.http, | ||
cagesAttestationData, | ||
attestationCache | ||
attestationCache, | ||
cagePcrManager | ||
); | ||
@@ -121,21 +138,2 @@ } else { | ||
async _shouldOverloadHttpModule(options, apiKey) { | ||
// DEPRECATED: Remove this method in next major version | ||
if (options.intercept || options.ignoreDomains) { | ||
console.warn( | ||
'\x1b[43m\x1b[30mWARN\x1b[0m The `intercept` and `ignoreDomains` config options in the Evervault Node.js SDK are deprecated and slated for removal.', | ||
'\n\x1b[43m\x1b[30mWARN\x1b[0m More details: https://docs.evervault.com/reference/nodejs-sdk#evervaultenableoutboundrelay' | ||
); | ||
} else if (options.intercept !== false && options.enableOutboundRelay) { | ||
// ^ preserves backwards compatibility with if relay is explictly turned off | ||
console.warn( | ||
'\x1b[43m\x1b[30mWARN\x1b[0m The `enableOutboundRelay` config option in the Evervault Node.js SDK is deprecated and slated for removal.', | ||
'\n\x1b[43m\x1b[30mWARN\x1b[0m You can now use the `enableOutboundRelay()` method to enable outbound relay.', | ||
'\n\x1b[43m\x1b[30mWARN\x1b[0m More details: https://docs.evervault.com/reference/nodejs-sdk#evervaultenableoutboundrelay' | ||
); | ||
await RelayOutboundConfig.init( | ||
this.config, | ||
this.http, | ||
Boolean(options.debugRequests) | ||
); | ||
} | ||
if (options.decryptionDomains && options.decryptionDomains.length > 0) { | ||
@@ -153,18 +151,2 @@ const decryptionDomainsFilter = this._decryptionDomainsFilter( | ||
); | ||
} else if ( | ||
options.intercept === true || | ||
options.relay === true || | ||
(options.ignoreDomains && options.ignoreDomains.length > 0) | ||
) { | ||
const ignoreDomainsFilter = this._ignoreDomainFilter( | ||
options.ignoreDomains | ||
); | ||
await this.httpsHelper.overloadHttpsModule( | ||
apiKey, | ||
this.config.http.tunnelHostname, | ||
ignoreDomainsFilter, | ||
Boolean(options.debugRequests), | ||
this.http, | ||
originalRequest | ||
); | ||
} else if (options.enableOutboundRelay) { | ||
@@ -185,3 +167,2 @@ await this.httpsHelper.overloadHttpsModule( | ||
_alwaysIgnoreDomains() { | ||
const functionsHost = new URL(this.config.http.functionRunUrl).host; | ||
const caHost = new URL(this.config.http.certHostname).host; | ||
@@ -192,19 +173,5 @@ const apiHost = new URL(this.config.http.baseUrl).host; | ||
return [functionsHost, cagesCaHost, caHost, apiHost, cagesHost]; | ||
return [cagesCaHost, caHost, apiHost, cagesHost]; | ||
} | ||
_parsedDomainsToIgnore(ignoreDomains) { | ||
ignoreDomains = ignoreDomains.concat(this._alwaysIgnoreDomains()); | ||
let ignoreExact = []; | ||
let ignoreEndsWith = []; | ||
ignoreDomains.forEach((domain) => { | ||
let exact = domain.startsWith('www.') ? domain.slice(4) : domain; | ||
ignoreExact.push(exact); | ||
ignoreEndsWith.push('.' + exact); | ||
ignoreEndsWith.push('@' + exact); | ||
}); | ||
return [ignoreExact, ignoreEndsWith]; | ||
} | ||
_decryptionDomainsFilter(decryptionDomains) { | ||
@@ -231,10 +198,2 @@ return (domain) => | ||
_ignoreDomainFilter(ignoreDomains = []) { | ||
const [ignoreExact, ignoreEndsWith] = | ||
this._parsedDomainsToIgnore(ignoreDomains); | ||
return (domain) => | ||
!this._isIgnoreRequest(domain, ignoreExact, ignoreEndsWith); | ||
} | ||
_relayOutboundConfigDomainFilter() { | ||
@@ -250,6 +209,2 @@ return (domain) => { | ||
_isIgnoreRequest(domain, ignoreExact, ignoreEndsWith) { | ||
return this._exactOrEndsWith(domain, ignoreExact, ignoreEndsWith); | ||
} | ||
_exactOrEndsWith(domain, exactDomains, endsWithDomains) { | ||
@@ -263,3 +218,3 @@ if (exactDomains.includes(domain)) return true; | ||
_refreshKeys() { | ||
_refreshKeys(role) { | ||
this._ecdh.generateKeys(); | ||
@@ -270,3 +225,6 @@ this.defineHiddenProperty( | ||
); | ||
if (this.curve === EvervaultClient.CURVES.PRIME256V1) { | ||
if ( | ||
this.curve === EvervaultClient.CURVES.PRIME256V1 || | ||
(this.curve === EvervaultClient.CURVES.SECP256K1 && role) | ||
) { | ||
this.defineHiddenProperty( | ||
@@ -277,3 +235,4 @@ '_derivedAesKey', | ||
this._ecdhTeamKey, | ||
this._ecdhPublicKey | ||
this._ecdhPublicKey, | ||
this.curve | ||
) | ||
@@ -291,5 +250,12 @@ ); | ||
* @param {Object || String} data | ||
* @param {String || undefined} role | ||
* @returns {Promise<Object || String>} | ||
*/ | ||
async encrypt(data) { | ||
async encrypt(data, role = null) { | ||
const dataRoleRegex = /^[a-z0-9-]{1,20}$/; | ||
if (role !== null && !dataRoleRegex.test(role)) { | ||
throw new Error( | ||
'The provided Data Role slug is invalid. The slug can be retrieved in the Evervault dashboard (Data Roles section).' | ||
); | ||
} | ||
if (!Datatypes.isDefined(this._derivedAesKey)) { | ||
@@ -307,3 +273,3 @@ if (!Datatypes.isDefined(this._ecdhTeamKey)) { | ||
} | ||
this._refreshKeys(); | ||
this._refreshKeys(role); | ||
} | ||
@@ -315,3 +281,3 @@ if (!Datatypes.isDefined(this._refreshInterval)) { | ||
(ref) => { | ||
ref._refreshKeys(); | ||
ref._refreshKeys(role); | ||
}, | ||
@@ -328,3 +294,4 @@ this.config.encryption[this.curve].keyCycleMinutes * 60 * 1000, | ||
this._derivedAesKey, | ||
data | ||
data, | ||
role | ||
); | ||
@@ -348,6 +315,5 @@ } | ||
*/ | ||
async run(functionName, payload, options = {}) { | ||
async run(functionName, payload) { | ||
validationHelper.validateFunctionName(functionName); | ||
validationHelper.validatePayload(payload); | ||
validationHelper.validateFunctionName(functionName); | ||
validationHelper.validateOptions(options); | ||
@@ -357,3 +323,3 @@ if (this.retry) { | ||
async () => { | ||
return await this.http.runCage(functionName, payload, options); | ||
return await this.http.runFunction(functionName, payload); | ||
}, | ||
@@ -364,3 +330,3 @@ { retries: 3 } | ||
} else { | ||
const response = await this.http.runCage(functionName, payload, options); | ||
const response = await this.http.runFunction(functionName, payload); | ||
return response.body; | ||
@@ -372,21 +338,2 @@ } | ||
* @param {String} functionName | ||
* @param {Object} data | ||
* @param {Object} options | ||
* @returns {Promise<*>} | ||
*/ | ||
async encryptAndRun(functionName, data, options) { | ||
console.warn( | ||
'\x1b[43m\x1b[30mWARN\x1b[0m The `encrypt_and_run` method is deprecated and slated for removal. Please use the `encrypt` and `run` methods instead.' | ||
); | ||
validationHelper.validatePayload(data); | ||
validationHelper.validateFunctionName(functionName); | ||
validationHelper.validateOptions(options); | ||
const payload = await this.encrypt(data); | ||
return await this.run(functionName, payload, options); | ||
} | ||
/** | ||
* @param {String} functionName | ||
* @param {Object} payload | ||
@@ -393,0 +340,0 @@ * @returns {Promise<*>} |
@@ -87,6 +87,6 @@ const certHelper = require('./certHelper'); | ||
async function attestCageConnection( | ||
function attestCageConnection( | ||
hostname, | ||
cert, | ||
cagesAttestationInfo = {}, | ||
cagePcrManager, | ||
attestationCache | ||
@@ -103,5 +103,6 @@ ) { | ||
// Pull cage name from cage hostname | ||
const { cageName, appUuid } = parseCageNameAndAppFromHost(hostname); | ||
const { cageName } = parseCageNameAndAppFromHost(hostname); | ||
// check if PCRs for this cage have been given | ||
const pcrs = cagesAttestationInfo[cageName]; | ||
const pcrs = cagePcrManager.get(cageName); | ||
var pcrsList = []; | ||
@@ -114,14 +115,4 @@ if (Array.isArray(pcrs)) { | ||
let attestationDoc = await attestationCache.get(cageName); | ||
if (!attestationDoc) { | ||
await attestationCache.loadCageDoc(cageName); | ||
attestationDoc = await attestationCache.get(cageName); | ||
if (!attestationDoc) { | ||
throw new CageAttestationError( | ||
"Couldn't find attestation doc in cache", | ||
hostname, | ||
cert | ||
); | ||
} | ||
} | ||
let attestationDoc = attestationCache.get(cageName); | ||
let attestationDocBytes = Buffer.from(attestationDoc, 'base64'); | ||
@@ -135,14 +126,3 @@ | ||
// Reload cache to check if deployment has happened between polling | ||
if (!isConnectionValid) { | ||
attestationCache.loadCageDoc(cageName); | ||
isConnectionValid = await getCageAttestationDoc( | ||
cageName, | ||
cert, | ||
pcrsList, | ||
attestationCache | ||
); | ||
} | ||
if (!isConnectionValid) { | ||
console.warn( | ||
@@ -201,7 +181,3 @@ `EVERVAULT WARN :: Connection to Cage ${cageName} failed attestation` | ||
function addAttestationListener( | ||
config, | ||
cagesAttestationInfo, | ||
attestationCache | ||
) { | ||
function addAttestationListener(config, attestationCache, cagePcrManager) { | ||
tls.checkServerIdentity = function (hostname, cert) { | ||
@@ -214,5 +190,6 @@ // only attempt attestation if the host is a cage | ||
cert.raw, | ||
cagesAttestationInfo, | ||
cagePcrManager, | ||
attestationCache | ||
); | ||
if (attestationResult != null) { | ||
@@ -219,0 +196,0 @@ return attestationResult; |
@@ -8,20 +8,14 @@ class EvervaultError extends Error { | ||
class InitializationError extends EvervaultError {} | ||
class FunctionTimeoutError extends EvervaultError {} | ||
class AccountError extends EvervaultError {} | ||
class FunctionNotReadyError extends EvervaultError {} | ||
class ApiKeyError extends EvervaultError {} | ||
class FunctionRuntimeError extends EvervaultError { | ||
constructor(message, stack, id) { | ||
super(message); | ||
this.stack = stack; | ||
this.id = id; | ||
} | ||
} | ||
class CageKeyError extends EvervaultError {} | ||
class RequestError extends EvervaultError {} | ||
class CertError extends EvervaultError {} | ||
class ForbiddenIPError extends EvervaultError {} | ||
class DecryptError extends EvervaultError {} | ||
class RelayOutboundConfigError extends EvervaultError {} | ||
class CageAttestationError extends EvervaultError { | ||
@@ -39,4 +33,22 @@ constructor(reason, host, cert) { | ||
const mapApiResponseToError = ({ statusCode, body, headers }) => { | ||
if (statusCode === 401) return new ApiKeyError('Invalid Api Key provided.'); | ||
const mapFunctionFailureResponseToError = ({ error, id }) => { | ||
if (error) { | ||
throw new FunctionRuntimeError(error.message, error.stack, id); | ||
} | ||
throw new EvervaultError('An unknown error occurred.'); | ||
}; | ||
const mapApiResponseToError = ({ code, detail }) => { | ||
if (code === 'functions/request-timeout') { | ||
throw new FunctionTimeoutError(detail); | ||
} | ||
if (code === 'functions/function-not-ready') { | ||
throw new FunctionNotReadyError(detail); | ||
} | ||
throw new EvervaultError(detail); | ||
}; | ||
const mapResponseCodeToError = ({ statusCode, body, headers }) => { | ||
if (statusCode === 401) | ||
return new EvervaultError('Invalid authorization provided.'); | ||
if ( | ||
@@ -46,3 +58,3 @@ statusCode === 403 && | ||
) { | ||
return new ForbiddenIPError( | ||
return new EvervaultError( | ||
body.message || "IP is not present on the invoked Cage's whitelist." | ||
@@ -52,3 +64,3 @@ ); | ||
if (statusCode === 403) { | ||
return new ApiKeyError( | ||
return new EvervaultError( | ||
'The API key provided does not have the required permissions.' | ||
@@ -58,15 +70,8 @@ ); | ||
if (statusCode === 422) { | ||
return new DecryptError(body.message || 'Unable to decrypt data.'); | ||
return new EvervaultError(body.message || 'Unable to decrypt data.'); | ||
} | ||
if (statusCode === 423) | ||
return new AccountError( | ||
body.message || | ||
'Your account is still being set up. Refer to the account status page on app.evervault.com' | ||
); | ||
if (statusCode === 424) | ||
return new AccountError( | ||
body.message || | ||
'An error occurred during account creation. Please contact evervault support.' | ||
); | ||
return new RequestError(`Request returned with status [${statusCode}]`); | ||
if (body.message) { | ||
return new EvervaultError(body.message); | ||
} | ||
return new EvervaultError(`Request returned with status [${statusCode}]`); | ||
}; | ||
@@ -76,15 +81,11 @@ | ||
EvervaultError, | ||
CageKeyError, | ||
ApiKeyError, | ||
AccountError, | ||
InitializationError, | ||
mapApiResponseToError, | ||
RequestError, | ||
CertError, | ||
DecryptError, | ||
ForbiddenIPError, | ||
RelayOutboundConfigError, | ||
mapResponseCodeToError, | ||
mapFunctionFailureResponseToError, | ||
CageAttestationError, | ||
ExceededMaxFileSizeError, | ||
TokenCreationError, | ||
FunctionTimeoutError, | ||
FunctionNotReadyError, | ||
FunctionRuntimeError, | ||
}; |
@@ -7,3 +7,3 @@ const crypto = require('crypto'); | ||
if (apiKey === '' || !Datatypes.isString(apiKey)) { | ||
throw new errors.InitializationError( | ||
throw new errors.EvervaultError( | ||
'The API key must be a string and cannot be empty.' | ||
@@ -21,3 +21,3 @@ ); | ||
if (appUuidHash !== appUuidHashFromApiKey) { | ||
throw new errors.InitializationError( | ||
throw new errors.EvervaultError( | ||
`The API key is not valid for app ${appUuid}. Make sure to use an API key belonging to the app ${appUuid}.` | ||
@@ -43,12 +43,2 @@ ); | ||
const validateOptions = (options = {}) => { | ||
if ( | ||
Datatypes.isObjectStrict(options) && | ||
Datatypes.isDefined(options.version) && | ||
!Datatypes.isNumber(options.version) | ||
) { | ||
throw new errors.EvervaultError('Function version must be a number'); | ||
} | ||
}; | ||
const validateRelayOutboundOptions = (options = {}) => { | ||
@@ -72,4 +62,3 @@ if ( | ||
validateFunctionName, | ||
validateOptions, | ||
validateRelayOutboundOptions, | ||
}; |
{ | ||
"name": "@evervault/sdk", | ||
"version": "4.3.0", | ||
"version": "5.0.0", | ||
"description": "Node.js SDK for Evervault", | ||
@@ -11,3 +11,3 @@ "main": "lib/index.js", | ||
"test": "mocha 'tests/**/*.test.js'", | ||
"test:e2e": "mocha 'e2e/**/*.test.js' --exit", | ||
"test:e2e": "mocha 'e2e/**/*.test.js' --timeout 5000 --exit", | ||
"test:filter": "mocha 'tests/**/*.test.js' --grep", | ||
@@ -46,5 +46,7 @@ "test:coverage": "nyc --reporter=text npm run test" | ||
"chai": "^4.2.0", | ||
"chai-as-promised": "^7.1.1", | ||
"husky": "^7.0.2", | ||
"lint-staged": "^11.1.2", | ||
"mocha": "^10.0.0", | ||
"msgpackr": "^1.9.9", | ||
"nock": "^12.0.3", | ||
@@ -51,0 +53,0 @@ "nyc": "^15.1.0", |
185
README.md
@@ -5,4 +5,2 @@ [![Evervault](https://evervault.com/evervault.svg)](https://evervault.com/) | ||
The [Evervault](https://evervault.com) Node.js SDK is a toolkit for encrypting data as it enters your server, and working with Functions. By default, initializing the SDK will result in all outbound HTTPS requests being intercepted and decrypted. | ||
## Getting Started | ||
@@ -16,185 +14,4 @@ | ||
See the Evervault [Node.js SDK documentation](https://docs.evervault.com/sdk/nodejs). | ||
See the Evervault [Node.js SDK documentation](https://docs.evervault.com/sdks/nodejs) to learn how to install, set up, and use the SDK. | ||
## Installation | ||
Our Node.js SDK is distributed via [npm](https://www.npmjs.com/package/@evervault/sdk), and can be installed using your preferred package manager. | ||
```sh | ||
npm install --save @evervault/sdk | ||
yarn add @evervault/sdk | ||
``` | ||
## Setup | ||
To make Evervault available for use in your app: | ||
```js | ||
const Evervault = require('@evervault/sdk'); | ||
// Initialize the client with your App ID and API Key | ||
const evervaultClient = new Evervault('<APP_ID>', '<API_KEY>'); | ||
// Encrypt your sensitive data | ||
const encrypted = await evervaultClient.encrypt({ ssn: '012-34-5678' }); | ||
// Process the encrypted data in a Function | ||
const result = await evervaultClient.run('<FUNCTION_NAME>', encrypted); | ||
// Send the decrypted data to a third-party API | ||
await evervaultClient.enableOutboundRelay(); | ||
const response = await axios.post('https://example.com', encrypted); | ||
// Use HTTPSProxyAgent to send data to a third-party | ||
const httpsAgent = evervault.createRelayHttpsAgent(); | ||
const response = await axios.get('https://example.com', { | ||
httpsAgent, | ||
}); | ||
// Decrypt the data | ||
const decrypted = await evervaultClient.decrypt(encrypted); | ||
// Enable the Cages client | ||
await evervaultClient.enableCages({ 'my-cage': { pcr8: '...' } }); | ||
const response = await axios.post( | ||
'https://my-cage.my-app.cages.evervault.com', | ||
encrypted | ||
); // This connection will be attested by the Cages client | ||
``` | ||
## Reference | ||
The Evervault Node.js SDK exposes six functions. | ||
### evervault.encrypt() | ||
`evervault.encrypt()` encrypts data. To encrypt data at the server, simply pass a string, boolean, number, array, object or buffer into the `evervault.encrypt()` function. Store the encrypted data in your database as normal. | ||
```javascript | ||
async evervault.encrypt(data: string | boolean | number | Array | Object | Buffer); | ||
``` | ||
| Parameter | Type | Description | | ||
| --------- | ------------------------------------------------ | --------------------- | | ||
| data | String, Boolean, Number, Array, Object or String | Data to be encrypted. | | ||
### evervault.decrypt() | ||
`evervault.decrypt()` decrypts data previously encrypted with the `encrypt()` function or through Evervault's Relay (Evervault's encryption proxy). | ||
An API Key with the `decrypt` permission must be used to perform this operation. | ||
```javascript | ||
async evervault.decrypt(encrypted: string | Array | Object | Buffer); | ||
``` | ||
| Parameter | Type | Description | | ||
| --------- | ------------------------------- | --------------------- | | ||
| encrypted | String, Array, Object or Buffer | Data to be decrypted. | | ||
### evervault.createClientSideDecryptToken() | ||
`evervault.createClientSideDecryptToken()` creates a token that can be used to authenticate a `decrypt()` request | ||
from a frontend/client application. | ||
An API Key with the `Create Token` permission must be used to perform this operation. | ||
```javascript | ||
async evervault.createClientSideDecryptToken(payload: string | Array | Object, expiry: Date); | ||
``` | ||
| Parameter | Type | Description | | ||
| --------- | ------------------------ | --------------------------------------------------------------------- | | ||
| payload | String, Array, or Object | Data that the token can decrypt. | | ||
| expiry | Date | The expiry of the token, must be < 10 mins from now. (Default 5 mins) | | ||
### evervault.run() | ||
`evervault.run()` invokes a Function with a given payload. | ||
An API Key with the `run function` permission must be used to perform this operation. | ||
```javascript | ||
async evervault.run(functionName: String, payload: Object[, options: Object]); | ||
``` | ||
| Parameter | Type | Description | | ||
| ------------ | ------ | ----------------------------------------------------- | | ||
| functionName | String | Name of the Function to be run | | ||
| data | Object | Payload for the Function | | ||
| options | Object | [Options for the Function run](#Function-Run-Options) | | ||
#### Function Run Options | ||
Options to control how your Function is run | ||
| Option | Type | Default | Description | | ||
| ------- | ------- | --------- | ---------------------------------------------------------------------------------------- | | ||
| async | Boolean | false | Run your Function in async mode. Async Function runs will be queued for processing. | | ||
| version | Number | undefined | Specify the version of your Function to run. By default, the latest version will be run. | | ||
### evervault.createRunToken() | ||
`evervault.createRunToken()` creates a single use, time bound token for invoking a Function. | ||
An API Key with the `create a run token` permission must be used to perform this operation. | ||
```javascript | ||
async evervault.createRunToken(functionName: String, payload: Object); | ||
``` | ||
| Parameter | Type | Description | | ||
| ------------ | ------ | -------------------------------------------------------- | | ||
| functionName | String | Name of the Function the run token should be created for | | ||
| data | Object | Payload that the token can be used with | | ||
### evervault.enableOutboundRelay() | ||
`evervault.enableOutboundRelay()` configures your application to proxy HTTP requests using Outbound Relay based on the configuration created in the Evervault dashboard. See [Outbound Relay](https://docs.evervault.com/concepts/outbound-relay/overview) to learn more. | ||
```javascript | ||
async evervault.enableOutboundRelay([options: Object]) | ||
``` | ||
| Option | Type | Default | Description | | ||
| ------------------- | --------- | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | ||
| `decryptionDomains` | `Array` | `undefined` | Requests sent to any of the domains listed will be proxied through Outbound Relay. This will override the configuration created in the Evervault dashboard. | | ||
| `debugRequests` | `Boolean` | `False` | Output request domains and whether they were sent through Outbound Relay. | | ||
### evervault.createRelayHttpsAgent() | ||
`evervault.createRelayHttpsAgent()` will return a `HttpsProxyAgent` configred to proxy traffic through Relay. | ||
```javascript | ||
evervault.createRelayHttpsAgent(); | ||
``` | ||
#### createRelayHttpsAgent axios example | ||
```javascript | ||
const httpsAgent = evervault.createRelayHttpsAgent(); | ||
const response = await axios.get('https://example.com', { | ||
httpsAgent, | ||
}); | ||
``` | ||
### evervault.enableCages() | ||
`evervault.enableCages()` configures your client to automatically attest any requests to Cages. See the [Cage attestation docs](https://docs.evervault.com/products/cages#how-does-attestation-work-with-cages) to learn more. | ||
```javascript | ||
async evervault.enableCages([cageAttestationData: Object]) | ||
``` | ||
| Key | Type | Default | Description | | ||
| ------------ | ---------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | ||
| `<CageName>` | `Object` `Array` | `undefined` | Requests to a Cage specified in this object will include a check to verify that the PCRs provided in the object are included in the attestation document. The provided data can be either a single Object, or an Array of Objects to allow roll-over between different sets of PCRs. | | ||
#### Cages Beta Example | ||
```javascript | ||
await evervault.enableCages({ | ||
'hello-cage': { | ||
pcr8: '97c5395a83c0d6a04d53ff962663c714c178c24500bf97f78456ed3721d922cf3f940614da4bb90107c439bc4a1443ca', | ||
}, | ||
}); | ||
``` | ||
## Contributing | ||
@@ -201,0 +18,0 @@ |
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
30
2385
74187
15
24