Compare commits

...

1 Commits

Author SHA1 Message Date
Florent Beauchamp
f618fcdaf8 feat(vhd): implement encryption on vhd directory 2022-06-18 11:12:30 +02:00
9 changed files with 168 additions and 62 deletions

View File

@@ -75,11 +75,15 @@ const debounceResourceFactory = factory =>
}
class RemoteAdapter {
constructor(handler, { debounceResource = res => res, dirMode, vhdDirectoryCompression } = {}) {
constructor(
handler,
{ debounceResource = res => res, dirMode, vhdDirectoryCompression, vhdDirectoryEncryption } = {}
) {
this._debounceResource = debounceResource
this._dirMode = dirMode
this._handler = handler
this._vhdDirectoryCompression = vhdDirectoryCompression
this._vhdDirectoryEncryption = vhdDirectoryEncryption
this._readCacheListVmBackups = synchronized.withKey()(this._readCacheListVmBackups)
}
@@ -201,7 +205,9 @@ class RemoteAdapter {
const isVhdDirectory = vhd instanceof VhdDirectory
return isVhdDirectory
? this.#useVhdDirectory() && this.#getCompressionType() === vhd.compressionType
? this.#useVhdDirectory() &&
this.#getCompressionType() === vhd.compressionType &&
this.#getEncryption() === vhd.encryption
: !this.#useVhdDirectory()
})
}
@@ -291,6 +297,10 @@ class RemoteAdapter {
return this._vhdDirectoryCompression
}
#getEncryption() {
return this._vhdDirectoryEncryption
}
#useVhdDirectory() {
return this.handler.type === 's3'
}
@@ -576,6 +586,7 @@ class RemoteAdapter {
await createVhdDirectoryFromStream(handler, dataPath, input, {
concurrency: 16,
compression: this.#getCompressionType(),
encryption: this.#getEncryption(),
async validator() {
await input.task
return validator.apply(this, arguments)

View File

@@ -71,6 +71,7 @@ class BackupWorker {
debounceResource: this.debounceResource,
dirMode: this.#config.dirMode,
vhdDirectoryCompression: this.#config.vhdDirectoryCompression,
vhdDirectoryEncryption: this.#config.vhdDirectoryEncryption,
})
} finally {
await handler.forget()

View File

@@ -5,58 +5,11 @@ const { createLogger } = require('@xen-orchestra/log')
const { fuFooter, fuHeader, checksumStruct } = require('../_structs')
const { test, set: setBitmap } = require('../_bitmap')
const { VhdAbstract } = require('./VhdAbstract')
const { _getCompressor: getCompressor } = require('./_compressors')
const { _getEncryptor: getEncryptor } = require('./_encryptors')
const assert = require('assert')
const promisify = require('promise-toolbox/promisify')
const zlib = require('zlib')
const { debug } = createLogger('vhd-lib:VhdDirectory')
const NULL_COMPRESSOR = {
compress: buffer => buffer,
decompress: buffer => buffer,
baseOptions: {},
}
const COMPRESSORS = {
gzip: {
compress: (
gzip => buffer =>
gzip(buffer, { level: zlib.constants.Z_BEST_SPEED })
)(promisify(zlib.gzip)),
decompress: promisify(zlib.gunzip),
},
brotli: {
compress: (
brotliCompress => buffer =>
brotliCompress(buffer, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MIN_QUALITY,
},
})
)(promisify(zlib.brotliCompress)),
decompress: promisify(zlib.brotliDecompress),
},
}
// inject identifiers
for (const id of Object.keys(COMPRESSORS)) {
COMPRESSORS[id].id = id
}
function getCompressor(compressorType) {
if (compressorType === undefined) {
return NULL_COMPRESSOR
}
const compressor = COMPRESSORS[compressorType]
if (compressor === undefined) {
throw new Error(`Compression type ${compressorType} is not supported`)
}
return compressor
}
// ===================================================================
// Directory format
// <path>
@@ -77,10 +30,15 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
#header
footer
#compressor
#encryptor
#encryption
get compressionType() {
return this.#compressor.id
}
get encryption() {
return this.#encryption
}
set header(header) {
this.#header = header
@@ -116,9 +74,9 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
}
}
static async create(handler, path, { flags = 'wx+', compression } = {}) {
static async create(handler, path, { flags = 'wx+', compression, encryption } = {}) {
await handler.mkdir(path)
const vhd = new VhdDirectory(handler, path, { flags, compression })
const vhd = new VhdDirectory(handler, path, { flags, compression, encryption })
return {
dispose: () => {},
value: vhd,
@@ -131,6 +89,8 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
this._path = path
this._opts = opts
this.#compressor = getCompressor(opts?.compression)
this.#encryption = opts?.encryption
this.#encryptor = getEncryptor(opts?.encryption)
}
async readBlockAllocationTable() {
@@ -150,7 +110,8 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
// here we can implement compression and / or crypto
const buffer = await this._handler.readFile(this._getChunkPath(partName))
const uncompressed = await this.#compressor.decompress(buffer)
const decrypted = await this.#encryptor.decrypt(buffer)
const uncompressed = await this.#compressor.decompress(decrypted)
return {
buffer: uncompressed,
}
@@ -164,7 +125,8 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
)
const compressed = await this.#compressor.compress(buffer)
return this._handler.outputFile(this._getChunkPath(partName), compressed, this._opts)
const encrypted = await this.#encryptor.encrypt(compressed)
return this._handler.outputFile(this._getChunkPath(partName), encrypted, this._opts)
}
// put block in subdirectories to limit impact when doing directory listing
@@ -246,7 +208,8 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
if (
!(child instanceof VhdDirectory) ||
this._handler !== child._handler ||
child.compressionType !== this.compressionType
child.compressionType !== this.compressionType ||
child.encryption !== this.encryption
) {
return super.coalesceBlock(child, blockId)
}
@@ -273,11 +236,12 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
async #writeChunkFilters() {
const compressionType = this.compressionType
const encryption = this.encryption
const path = this._path + '/chunk-filters.json'
if (compressionType === undefined) {
if (compressionType === undefined && encryption === undefined) {
await this._handler.unlink(path)
} else {
await this._handler.writeFile(path, JSON.stringify([compressionType]), { flags: 'w' })
await this._handler.writeFile(path, JSON.stringify([compressionType, encryption]), { flags: 'w' })
}
}
@@ -288,6 +252,9 @@ exports.VhdDirectory = class VhdDirectory extends VhdAbstract {
}
throw error
})
assert(chunkFilters.length, 2)
this.#compressor = getCompressor(chunkFilters[0])
this.#encryption = chunkFilters[1]
this.#encryptor = getEncryptor(chunkFilters[1])
}
}

View File

@@ -0,0 +1,51 @@
'use strict'
const zlib = require('zlib')
const promisify = require('promise-toolbox/promisify')
const NULL_COMPRESSOR = {
compress: buffer => buffer,
decompress: buffer => buffer,
}
const COMPRESSORS = {
gzip: {
compress: (
gzip => buffer =>
gzip(buffer, { level: zlib.constants.Z_BEST_SPEED })
)(promisify(zlib.gzip)),
decompress: promisify(zlib.gunzip),
},
brotli: {
compress: (
brotliCompress => buffer =>
brotliCompress(buffer, {
params: {
[zlib.constants.BROTLI_PARAM_QUALITY]: zlib.constants.BROTLI_MIN_QUALITY,
},
})
)(promisify(zlib.brotliCompress)),
decompress: promisify(zlib.brotliDecompress),
},
}
// inject identifiers
for (const id of Object.keys(COMPRESSORS)) {
COMPRESSORS[id].id = id
}
function getCompressor(compressorType) {
if (compressorType === undefined) {
return NULL_COMPRESSOR
}
const compressor = COMPRESSORS[compressorType]
if (compressor === undefined) {
throw new Error(`Compression type ${compressorType} is not supported`)
}
return compressor
}
exports._getCompressor = getCompressor

View File

@@ -0,0 +1,22 @@
'use strict'
const crypto = require('crypto')
const { _getEncryptor: getEncryptor } = require('./_encryptors')
/* eslint-env jest */
test('can encrypt and decryp AES 256', async () => {
const { encrypt, decrypt } = getEncryptor(
JSON.stringify({
algorithm: 'aes-256-cbc',
key: crypto.randomBytes(32),
ivLength: 16,
})
)
const buffer = crypto.randomBytes(1024)
const encrypted = encrypt(buffer)
const decrypted = decrypt(encrypted)
expect(buffer.equals(decrypted)).toEqual(true)
})

View File

@@ -0,0 +1,44 @@
'use strict'
const crypto = require('crypto')
const secretStore = require('./_secretStore.js')
function getEncryptor(id = '{}') {
const { algorithm, key, ivLength } = secretStore.get(id)
if (algorithm === undefined) {
return {
id: 'NULL_COMPRESSOR',
algorithm,
key,
ivLength,
encrypt: buffer => buffer,
decrypt: buffer => buffer,
}
}
function encrypt(buffer) {
const iv = crypto.randomBytes(ivLength)
const cipher = crypto.createCipheriv(algorithm, Buffer.from(key), iv)
const encrypted = cipher.update(buffer)
return Buffer.concat([iv, encrypted, cipher.final()])
}
function decrypt(buffer) {
const iv = buffer.slice(0, ivLength)
const encrypted = buffer.slice(ivLength)
const decipher = crypto.createDecipheriv(algorithm, Buffer.from(key), iv)
const decrypted = decipher.update(encrypted)
return Buffer.concat([decrypted, decipher.final()])
}
return {
id,
algorithm,
key,
ivLength,
encrypt,
decrypt,
}
}
exports._getEncryptor = getEncryptor

View File

@@ -0,0 +1,4 @@
'use strict'
// @todo : should be moved to his own module
module.exports.get = id => JSON.parse(id || '') || {}

View File

@@ -5,8 +5,13 @@ const { VhdDirectory } = require('./Vhd/VhdDirectory.js')
const { Disposable } = require('promise-toolbox')
const { asyncEach } = require('@vates/async-each')
const buildVhd = Disposable.wrap(async function* (handler, path, inputStream, { concurrency, compression }) {
const vhd = yield VhdDirectory.create(handler, path, { compression })
const buildVhd = Disposable.wrap(async function* (
handler,
path,
inputStream,
{ concurrency, compression, encryption }
) {
const vhd = yield VhdDirectory.create(handler, path, { compression, encryption })
await asyncEach(
parseVhdStream(inputStream),
async function (item) {
@@ -41,10 +46,10 @@ exports.createVhdDirectoryFromStream = async function createVhdDirectoryFromStre
handler,
path,
inputStream,
{ validator, concurrency = 16, compression } = {}
{ validator, concurrency = 16, compression, encryption } = {}
) {
try {
await buildVhd(handler, path, inputStream, { concurrency, compression })
await buildVhd(handler, path, inputStream, { concurrency, compression, encryption })
if (validator !== undefined) {
await validator.call(this, path)
}

View File

@@ -88,6 +88,7 @@ snapshotNameLabelTpl = '[XO Backup {job.name}] {vm.name_label}'
listingDebounce = '1 min'
vhdDirectoryCompression = 'brotli'
vhdDirectoryEncryption = '{"algorithm": "aes-256-cbc", "key": "45eb3ffe48dd29e7bd04a7941ba425f2" ,"ivLength": 16}'
# This is a work-around.
#