forked from TritonDataCenter/node-manta
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
TritonDataCenter#296 add client encryption support
- Loading branch information
Showing
5 changed files
with
513 additions
and
5 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,273 @@ | ||
/* | ||
* Copyright 2017 Joyent, Inc. | ||
*/ | ||
|
||
var crypto = require('crypto'); | ||
|
||
var assert = require('assert-plus'); | ||
var PassThrough = require('stream').PassThrough; | ||
if (!PassThrough) | ||
PassThrough = require('readable-stream/passthrough.js'); | ||
|
||
var b64 = require('b64'); | ||
|
||
|
||
var VERSION = '1.0'; | ||
var CIPHERS = { | ||
'AES/CBC/PKCS5Padding': { | ||
string: 'aes-256-cbc', | ||
ivBytes: 16, | ||
keyBytes: 32 | ||
}, | ||
'AES/CTR/NoPadding': { | ||
string: 'aes-256-ctr', | ||
ivBytes: 16, | ||
keyBytes: 32 | ||
}, | ||
'AES/GCM/NoPadding': { | ||
string: 'aes-256-gcm', | ||
ivBytes: 12, | ||
keyBytes: 32 | ||
} | ||
}; | ||
|
||
|
||
exports.isSupportedVersion = function isSupportedVersion(version) { | ||
if (!version || version.indexOf('.') === -1) { | ||
return (false); | ||
} | ||
|
||
var versionParts = version.split('.'); | ||
var major = parseInt(versionParts[0], 10); | ||
|
||
return (major === 1); | ||
}; | ||
|
||
|
||
exports.decrypt = function decrypt(options, encrypted, res, cb) { | ||
assert.object(options, 'options are required and must be an object'); | ||
assert.stream(encrypted, 'encrypted is required and must be a stream'); | ||
assert.func(options.getKey, 'options.getKey is required'); | ||
|
||
var invalidHeaders = validateHeaders(res.headers); | ||
if (invalidHeaders) { | ||
return (cb(new Error('Headers are missing or invalid: ' + | ||
invalidHeaders), null, res)); | ||
} | ||
|
||
return options.getKey(res.headers['m-encrypt-key-id'], function (err, key) { | ||
if (err) { | ||
return (cb(err, null, res)); | ||
} | ||
|
||
var algorithm = getAlgorithm(res.headers['m-encrypt-cipher']); | ||
if (!algorithm) { | ||
return cb(new Error('Unsupported cipher algorithm: ' + | ||
res.headers['m-encrypt-cipher'])); | ||
} | ||
var decipher = crypto.createDecipheriv(algorithm.string, key, | ||
b64.decode(new Buffer(res.headers['m-encrypt-iv']))); | ||
var hmac = crypto.createHmac('sha256', key); | ||
var output = new PassThrough(); | ||
var byteLength = 0; | ||
|
||
encrypted.on('data', function (data) { | ||
hmac.update(data); | ||
}); | ||
|
||
encrypted.on('error', function (streamErr) { | ||
decipher.removeAllListeners(); | ||
encrypted.removeAllListeners(); | ||
cb(streamErr); | ||
}); | ||
|
||
decipher.on('data', function (data) { | ||
byteLength += Buffer.byteLength(data); | ||
}); | ||
|
||
decipher.once('error', function (decErr) { | ||
decipher.removeAllListeners(); | ||
cb(decErr); | ||
}); | ||
|
||
decipher.once('end', function (data) { | ||
var digest = hmac.digest('base64'); | ||
|
||
if (digest !== res.headers['m-encrypt-mac']) { | ||
return (cb(new Error('cipher hmac doesn\'t match stored' + | ||
' m-encrypt-mac value'), null, res)); | ||
} | ||
|
||
if (byteLength !== parseInt( | ||
res.headers['m-encrypt-original-content-length'], 10)) { | ||
return (cb(new Error( | ||
'decrypted file size doesn\'t match original copy'), | ||
null, res)); | ||
} | ||
|
||
return decryptMetadata(res.headers, key, function () { | ||
cb(null, output, res); | ||
}); | ||
}); | ||
|
||
|
||
return (encrypted.pipe(decipher).pipe(output)); | ||
}); | ||
}; | ||
|
||
|
||
var requiredHeaders = [ | ||
'm-encrypt-key-id', | ||
'm-encrypt-iv', | ||
'm-encrypt-cipher', | ||
'm-encrypt-mac', | ||
'm-encrypt-original-content-length' | ||
]; | ||
|
||
function validateHeaders(headers) { | ||
var missingHeaders = []; | ||
requiredHeaders.forEach(function (header) { | ||
if (headers[header] === undefined || headers[header] === null) { | ||
missingHeaders.push(header); | ||
} | ||
}); | ||
|
||
if ((headers['m-encrypt-metadata'] !== undefined && | ||
headers['m-encrypt-metadata'] !== null) && | ||
!headers['m-encrypt-metadata-cipher']) { | ||
|
||
missingHeaders.push('m-encrypt-metadata-cipher'); | ||
} | ||
|
||
return (missingHeaders.length ? missingHeaders : null); | ||
} | ||
|
||
|
||
function decryptMetadata(headers, key, cb) { | ||
if (!headers['m-encrypt-metadata']) { | ||
return (cb()); | ||
} | ||
|
||
var algorithm = getAlgorithm(headers['m-encrypt-metadata-cipher']); | ||
if (!algorithm) { | ||
return cb(new Error('Unsupported cipher algorithm: ' + | ||
headers['m-encrypt-metadata-cipher'])); | ||
} | ||
var decipher = crypto.createDecipheriv(algorithm.string, key, | ||
b64.decode(new Buffer(headers['m-encrypt-metadata-iv']))); | ||
var hmac = crypto.createHmac('sha256', key); | ||
|
||
var bufs = []; | ||
decipher.on('data', function (data) { | ||
bufs.push(data); | ||
}); | ||
|
||
decipher.once('finish', function () { | ||
hmac.update(b64.decode(new Buffer(headers['m-encrypt-metadata']))); | ||
headers['m-encrypt-metadata'] = Buffer.concat(bufs).toString(); | ||
|
||
if (headers['m-encrypt-metadata-mac'] !== hmac.digest('base64')) { | ||
return (cb(new Error('mac hash doesn\'t match'))); | ||
} | ||
|
||
return (cb()); | ||
}); | ||
|
||
decipher.write(b64.decode(new Buffer(headers['m-encrypt-metadata']))); | ||
return (decipher.end()); | ||
} | ||
|
||
exports.encrypt = function encrypt(options, input, cb) { | ||
assert.object(options, 'options are required and must be an object'); | ||
assert.stream(input, 'input is required and must be a stream'); | ||
assert.string(options.key, 'key is required when encrypting'); | ||
assert.string(options.cipher, 'cipher is required when encrypting'); | ||
|
||
var algorithm = getAlgorithm(options.cipher); | ||
if (!algorithm) { | ||
assert.ok(algorithm, 'Unsupported cipher algorithm: ' + options.cipher); | ||
} | ||
|
||
var iv = crypto.randomBytes(algorithm.ivBytes); | ||
var cipher = crypto.createCipheriv(algorithm.string, options.key, iv); | ||
var hmac = crypto.createHmac('sha256', options.key); | ||
var output = new PassThrough(); | ||
var byteLength = 0; | ||
|
||
cipher.on('data', function (data) { | ||
hmac.update(data); | ||
}); | ||
|
||
cipher.once('error', function (err) { | ||
cipher.removeAllListeners(); | ||
cb(err); | ||
}); | ||
|
||
input.on('data', function (data) { | ||
byteLength += Buffer.byteLength(data); | ||
}); | ||
|
||
cipher.once('end', function (data) { | ||
options.headers['m-encrypt-type'] = 'client/' + VERSION; | ||
options.headers['m-encrypt-key-id'] = options.keyId; | ||
options.headers['m-encrypt-iv'] = b64.encode(iv).toString(); | ||
options.headers['m-encrypt-cipher'] = options.cipher; | ||
options.headers['m-encrypt-mac'] = hmac.digest('base64'); | ||
options.headers['m-encrypt-original-content-length'] = byteLength; | ||
|
||
if (options.headers && options.headers['m-encrypt-metadata']) { | ||
return encryptMetadata(options.headers, options.key, | ||
function (err) { | ||
if (err) { | ||
return (cb(err)); | ||
} | ||
|
||
return (cb(null, output)); | ||
}); | ||
} | ||
|
||
return (cb(null, output)); | ||
}); | ||
|
||
input.pipe(cipher).pipe(output); | ||
}; | ||
|
||
|
||
function encryptMetadata(headers, key, cb) { | ||
var algorithm = getAlgorithm(headers['m-encrypt-metadata-cipher']); | ||
if (!algorithm) { | ||
return (cb(new Error('Unsupported cipher algorithm: ' + | ||
headers['m-encrypt-metadata-cipher']))); | ||
} | ||
|
||
var iv = crypto.randomBytes(algorithm.ivBytes); | ||
headers['m-encrypt-metadata-iv'] = b64.encode(iv).toString(); | ||
var cipher = crypto.createCipheriv(algorithm.string, key, iv); | ||
var hmac = crypto.createHmac('sha256', key); | ||
|
||
var bufs = []; | ||
cipher.on('data', function (data) { | ||
bufs.push(data); | ||
}); | ||
|
||
cipher.once('finish', function () { | ||
var encrypted = Buffer.concat(bufs); | ||
headers['m-encrypt-metadata'] = b64.encode(encrypted).toString(); | ||
hmac.update(encrypted); | ||
headers['m-encrypt-metadata-mac'] = hmac.digest('base64'); | ||
cb(); | ||
}); | ||
|
||
cipher.once('error', function (err) { | ||
cipher.removeAllListeners(); | ||
cb(err); | ||
}); | ||
|
||
cipher.write(headers['m-encrypt-metadata']); | ||
return (cipher.end()); | ||
} | ||
|
||
|
||
function getAlgorithm(cipher) { | ||
return (CIPHERS[cipher]); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.