From: Alain Spineux Date: Thu, 6 Oct 2022 12:16:41 +0000 (+0200) Subject: New BB03 volume format: volume encryption & new 64bits checksum X-Git-Tag: Beta-15.0.0~404 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=3e340948f17a163c4003e8265f5cbdcc34bb4cbc;p=thirdparty%2Fbacula.git New BB03 volume format: volume encryption & new 64bits checksum - this is a 64bits XXHASH64 check that replace the 32bits - The old crc32 location is used for a "block option" bit field - use the block header option bit field to store information about - if block checksum (XXH64) is used - encryption is used - if "this" block is encrypted (volume label are not) - add encryption_command - use the BlockNum for the IV - new SD->Device->BlockEncryption directive to enable encryption - the xxhas64 is in one block at the end of the header - add documentation - add a unittest for block_crypto, to show how to use it and do some basic encrypte/decrypte - 2 options bit in the block header - BLKHOPT_ENCRYPT_VOL if the volume is encrypter - BLKHOPT_ENCRYPT_BLOCK if THIS block is encrypted - bsdjson can display the encryption type - high level protocol between the sd_encryption_script and the SD - use ENV to send the data - use stdout/stdin to read the answere from the keymanager - handle error reported by the script - obfuscate data in the volume when needed (BlockEncryption = STRONG) - support data spooling (don't encode data in the spool) - support %V in edit_device_codes() but not used anymore, use environment instead - add support for a master key (store encoded version of the encryption key and the key id of the masterkey) - DEVICE::load_encryption_key() that call the keymanager - Add new fields to volume label : EncCipherKey[Size] & MasterKeyId[Size] - add int DEV_RECORD::BlockVer to know the volume version, in case we decode the volume header and its extra fields - add the NULL cipher only for testing purpose - the XXHASH is always there in the block header event when not used (set to 0) - the block header has a constant length # Conflicts: # bacula/src/stored/stored_conf.h --- diff --git a/bacula/src/stored/block.c b/bacula/src/stored/block.c index 0f8fa2cbc..f205aa7ab 100644 --- a/bacula/src/stored/block.c +++ b/bacula/src/stored/block.c @@ -74,6 +74,12 @@ bool DCR::write_block_to_device(bool final) ok = false; goto bail_out; /* fatal error */ } + if (dcr->despooling && dev->device->block_encryption!=ET_NONE && dev->crypto_device_ctx!=NULL) { +// DEVICE *old = block->dev; + block->dev = dev; +// uint64_t checksum = ser_block_header(block, dev->do_checksum()); +// block->dev = old; + } Dmsg1(500, "Write block to dev=%p\n", dcr->dev); if (!write_block_to_dev()) { @@ -125,7 +131,7 @@ bool DCR::write_block_to_dev() uint32_t wlen; /* length to write */ bool ok = true; DCR *dcr = this; - uint32_t checksum; + uint64_t checksum; uint32_t pad; /* padding or zeros written */ boffset_t pos; char ed1[50]; @@ -234,10 +240,11 @@ bool DCR::write_block_to_dev() bmicrosleep(5, 0); /* pause a bit if busy or lots of errors */ dev->clrerror(-1); } - stat = dev->write(block->buf, (size_t)wlen); + stat = dev->write(block->buf_out, (size_t)wlen); + Dmsg4(100, "%s write() BlockAddr=%lld wlen=%d Vol=%s\n", - block->adata?"Adata":"Ameta", - block->BlockAddr, wlen, dev->VolHdr.VolumeName); + block->adata?"Adata":"Ameta", block->BlockAddr, wlen, + dev->VolHdr.VolumeName); } while (stat == -1 && (errno == EBUSY || errno == EIO) && retry++ < 3); /* ***FIXME*** remove 2 lines debug */ @@ -269,7 +276,14 @@ bool DCR::write_block_to_dev() } } - if (stat != (ssize_t)wlen) { + if (stat == (ssize_t)wlen) { + if (block->first_block) + { // It was a volume label, it has been successfully written + // next block should not hold a volume_label and should + // be encrypted if encryption is enable + block->first_block = false; + } + } else { /* Some devices simply report EIO when the volume is full. * With a little more thought we may be able to check * capacity and distinguish real errors and EOT diff --git a/bacula/src/stored/block.h b/bacula/src/stored/block.h index 36c16053e..e1d4b35ba 100644 --- a/bacula/src/stored/block.h +++ b/bacula/src/stored/block.h @@ -36,15 +36,29 @@ /* Block Header definitions. */ #define BLKHDR1_ID "BB01" #define BLKHDR2_ID "BB02" +#define BLKHDR3_ID "BB03" #define BLKHDR_ID_LENGTH 4 #define BLKHDR_CS_LENGTH 4 /* checksum length */ #define BLKHDR1_LENGTH 16 /* Total length */ #define BLKHDR2_LENGTH 24 /* Total length */ +#define BLKHDR3_LENGTH 32 /* Total length including the checksum) */ +#define BLKHDR_CS64_OFFSET 24 /* 64bits checksum offset */ +#define BLKHDR_CS64_LENGTH 8 /* 64bits checksum length */ -#define WRITE_BLKHDR_ID BLKHDR2_ID -#define WRITE_BLKHDR_LENGTH BLKHDR2_LENGTH +#define WRITE_BLKHDR_ID BLKHDR3_ID +#define WRITE_BLKHDR_LENGTH BLKHDR3_LENGTH #define WRITE_ADATA_BLKHDR_LENGTH (6*sizeof(int32_t)+sizeof(uint64_t)) -#define BLOCK_VER 2 +#define BLOCK_VER 3 + +enum BLKH_OPTIONS { + BLKHOPT_NONE = 0x0, + BLKHOPT_CHKSUM = 0x1, // set if the checksum is enable + BLKHOPT_ENCRYPT_VOL = 0x2, // set if the volume is encrypted + BLKHOPT_ENCRYPT_BLOCK = 0x4, // set if this block is encrypted (volume label are not) + + BLKHOPT_MASK = 0x7, // Update this one after adding some values above + BLKHOPT_LAST = 0x8, // Update this one too +}; /* Record header definitions */ #define RECHDR1_LENGTH 20 @@ -55,6 +69,7 @@ * uint32_t data_length */ #define RECHDR2_LENGTH (3*sizeof(int32_t)) +#define RECHDR3_LENGTH RECHDR2_LENGTH #define WRITE_RECHDR_LENGTH RECHDR2_LENGTH /* @@ -118,6 +133,18 @@ uint32_t VolSessionTime; uint64_t BlockAddr; + * for BB03 block, we have + variable header length, minimum is 24 bytes + + uint32_t blkh_options; // bit field describing the extra fields + uint32_t block_len; + uint32_t BlockNumber; + char Id[BLKHDR_ID_LENGTH]; + uint32_t VolSessionId; + uint32_t VolSessionTime; + ---- no come extra field ---- + uint64_t xxhash64; if (blkh_options&BLKHOPT_CHKSUM) + */ class DEVICE; /* for forward reference */ @@ -148,7 +175,7 @@ struct DEV_BLOCK { uint32_t VolSessionId; /* */ uint32_t VolSessionTime; /* */ uint32_t read_errors; /* block errors (checksum, header, ...) */ - uint32_t CheckSum; /* Block checksum */ + uint64_t CheckSum64; /* Block checksum */ uint32_t RecNum; /* Number of records read from the current block */ uint32_t extra_bytes; /* the extra size that must be accounted in VolABytes */ int BlockVer; /* block version 1 or 2 */ @@ -158,13 +185,17 @@ struct DEV_BLOCK { bool adata; /* adata block */ bool no_header; /* Set if no block header */ bool new_fi; /* New FI arrived */ + bool first_block; /* Is the block holding a volume label */ int32_t FirstIndex; /* first index this block */ int32_t LastIndex; /* last index this block */ int32_t rechdr_items; /* number of items in rechdr queue */ char *bufp; /* pointer into buffer */ POOLMEM *rechdr_queue; /* record header queue */ POOLMEM *buf; /* actual data buffer */ + POOLMEM *buf_enc; /* buffer with the encrypted data */ + char *buf_out; /* point to the data that must be written to the media */ alist *filemedia; /* Filemedia attached to the current block */ + uint32_t blkh_options; /* see BLKH_OPTIONS */ }; #define block_is_empty(block) ((block)->read_len == 0) diff --git a/bacula/src/stored/block_util.c b/bacula/src/stored/block_util.c index 3d05e9286..b6c70b8d8 100644 --- a/bacula/src/stored/block_util.c +++ b/bacula/src/stored/block_util.c @@ -26,6 +26,7 @@ #include "bacula.h" #include "stored.h" +#include "lib/xxhash.h" static const int dbglvl = 160; @@ -51,7 +52,8 @@ void dump_block(DEVICE *dev, DEV_BLOCK *b, const char *msg, bool force) char *p; char *bufp; char Id[BLKHDR_ID_LENGTH+1]; - uint32_t CheckSum, BlockCheckSum; + uint64_t CheckSum64, BlockCheckSum64; + uint32_t CheckSumLo; uint32_t block_len, reclen; uint32_t BlockNumber; uint32_t VolSessionId, VolSessionTime, data_len; @@ -74,35 +76,55 @@ void dump_block(DEVICE *dev, DEV_BLOCK *b, const char *msg, bool force) } } unser_begin(b->buf, BLKHDR1_LENGTH); - unser_uint32(CheckSum); + unser_uint32(CheckSumLo); unser_uint32(block_len); unser_uint32(BlockNumber); unser_bytes(Id, BLKHDR_ID_LENGTH); ASSERT(unser_length(b->buf) == BLKHDR1_LENGTH); Id[BLKHDR_ID_LENGTH] = 0; - if (Id[3] == '2') { + + if (block_len > 4000000 || block_len < BLKHDR_CS_LENGTH) { + Dmsg3(20, "Will not dump blocksize too %s %lu msg: %s\n", + (block_len < BLKHDR_CS_LENGTH)?"small":"big", + block_len, msg); + return; + } + + if (Id[3] == '3') { + // CheckSumLo is the header option field + unser_uint32(VolSessionId); + unser_uint32(VolSessionTime); + unser_uint64(CheckSum64); + bhl = BLKHDR3_LENGTH; + rhl = RECHDR3_LENGTH; + /* backup the CheckSum64, replace it with 0x0...0 and calculate the CheckSum64 */ + uint64_t save; + memcpy(&save, b->buf+BLKHDR_CS64_OFFSET, BLKHDR_CS64_LENGTH); + memset(b->buf+BLKHDR_CS64_OFFSET, '\0', BLKHDR_CS64_LENGTH); + BlockCheckSum64 = XXH3_64bits((uint8_t *)b->buf+BLKHDR_CS_LENGTH, + block_len-BLKHDR_CS_LENGTH); + /* restore the checksum */ + memcpy(b->buf+BLKHDR_CS64_OFFSET, &save, BLKHDR_CS64_LENGTH); + } else if (Id[3] == '2') { unser_uint32(VolSessionId); unser_uint32(VolSessionTime); bhl = BLKHDR2_LENGTH; rhl = RECHDR2_LENGTH; + CheckSum64 = CheckSumLo; + BlockCheckSum64 = bcrc32((uint8_t *)b->buf+BLKHDR_CS_LENGTH, + block_len-BLKHDR_CS_LENGTH); } else { VolSessionId = VolSessionTime = 0; bhl = BLKHDR1_LENGTH; rhl = RECHDR1_LENGTH; + CheckSum64 = CheckSumLo; + BlockCheckSum64 = bcrc32((uint8_t *)b->buf+BLKHDR_CS_LENGTH, + block_len-BLKHDR_CS_LENGTH); } - if (block_len > 4000000 || block_len < BLKHDR_CS_LENGTH) { - Dmsg3(20, "Will not dump blocksize too %s %lu msg: %s\n", - (block_len < BLKHDR_CS_LENGTH)?"small":"big", - block_len, msg); - return; - } - - BlockCheckSum = bcrc32((uint8_t *)b->buf+BLKHDR_CS_LENGTH, - block_len-BLKHDR_CS_LENGTH); Pmsg7(000, _("Dump block %s %p: adata=%d size=%d BlkNum=%d\n" -" Hdrcksum=%x cksum=%x\n"), - msg, b, b->adata, block_len, BlockNumber, CheckSum, BlockCheckSum); +" Hdrcksum=%llx cksum=%llx\n"), + msg, b, b->adata, block_len, BlockNumber, CheckSum64, BlockCheckSum64); p = b->buf + bhl; while (p < bufp) { unser_begin(p, WRITE_RECHDR_LENGTH); @@ -164,11 +186,14 @@ DEV_BLOCK *DEVICE::new_block(DCR *dcr, int size) } block->buf_len = len; block->buf = get_memory(block->buf_len); + block->buf_enc = get_memory(block->buf_len); block->rechdr_queue = get_memory(block->buf_len); block->rechdr_items = 0; Dmsg2(510, "Rechdr len=%d max_items=%d\n", sizeof_pool_memory(block->rechdr_queue), sizeof_pool_memory(block->rechdr_queue)/WRITE_ADATA_RECHDR_LENGTH); block->filemedia = New(alist(1, owned_by_alist)); + block->blkh_options = 0; + block->blkh_options |= do_checksum() ? BLKHOPT_CHKSUM : 0; empty_block(block); block->BlockVer = BLOCK_VER; /* default write version */ Dmsg3(150, "New block adata=%d len=%d block=%p\n", block->adata, len, block); @@ -187,7 +212,11 @@ DEV_BLOCK *dup_block(DEV_BLOCK *eblock) memcpy(block, eblock, sizeof(DEV_BLOCK)); block->buf = get_memory(buf_len); + block->buf_enc = get_memory(buf_len); + block->buf_out = (eblock->buf_out==eblock->buf)?block->buf:eblock->buf_enc; + memcpy(block->buf, eblock->buf, buf_len); + memcpy(block->buf_enc, eblock->buf_enc, buf_len); block->rechdr_queue = get_memory(rechdr_len); memcpy(block->rechdr_queue, eblock->rechdr_queue, rechdr_len); @@ -298,6 +327,9 @@ void free_block(DEV_BLOCK *block) if (block->buf) { free_memory(block->buf); } + if (block->buf_enc) { + free_memory(block->buf_enc); + } if (block->rechdr_queue) { free_memory(block->rechdr_queue); } @@ -329,6 +361,7 @@ void empty_block(DEV_BLOCK *block) Dmsg3(250, "empty_block: adata=%d len=%d set binbuf=%d\n", block->adata, block->buf_len, block->binbuf); block->bufp = block->buf + block->binbuf; + block->buf_out = block->buf; block->read_len = 0; block->write_failed = false; block->block_read = false; @@ -338,6 +371,7 @@ void empty_block(DEV_BLOCK *block) block->BlockAddr = 0; block->filemedia->destroy(); block->extra_bytes = 0; + block->first_block = false; // by default this block don't hold a volume label } /* @@ -345,40 +379,60 @@ void empty_block(DEV_BLOCK *block) * in the buffer should have already been reserved by * init_block. */ -uint32_t ser_block_header(DEV_BLOCK *block, bool do_checksum) +uint64_t ser_block_header(DEV_BLOCK *block, bool do_checksum) { + DEVICE *dev = block->dev; ser_declare; uint32_t block_len = block->binbuf; - - block->CheckSum = 0; + uint32_t hdr_option = 0x0; + bool do_encrypt_vol = dev->device->block_encryption!=ET_NONE && dev->crypto_device_ctx!=NULL; + bool do_encrypt_block = do_encrypt_vol && !block->first_block; + hdr_option |= (do_checksum?BLKHOPT_CHKSUM:0) | + (do_encrypt_vol?BLKHOPT_ENCRYPT_VOL:0) | + (do_encrypt_block?BLKHOPT_ENCRYPT_BLOCK:0); + block->CheckSum64 = 0; /* ***BEEF*** */ if (block->adata) { /* Checksum whole block */ if (do_checksum) { - block->CheckSum = bcrc32((uint8_t *)block->buf, block_len); + block->CheckSum64 = bcrc32((uint8_t *)block->buf, block_len); } } else { + int chk_off = BLKHDR_CS64_OFFSET; + int bhl = BLKHDR3_LENGTH; Dmsg1(160, "block_header: block_len=%d\n", block_len); - ser_begin(block->buf, BLKHDR2_LENGTH); - ser_uint32(block->CheckSum); + ser_begin(block->buf, BLKHDR3_LENGTH); + ser_uint32(hdr_option); /* Was the crc32, is now an "option" bit field in BB03 */ ser_uint32(block_len); ser_uint32(block->BlockNumber); - ser_bytes(WRITE_BLKHDR_ID, BLKHDR_ID_LENGTH); - if (BLOCK_VER >= 2) { - ser_uint32(block->VolSessionId); - ser_uint32(block->VolSessionTime); - } - - /* Checksum whole block except for the checksum */ + ser_bytes(WRITE_BLKHDR_ID, BLKHDR_ID_LENGTH); // BB03 + // BLOCK_VER >= 2 + ser_uint32(block->VolSessionId); + ser_uint32(block->VolSessionTime); + // BLOCK_VER >= 3; +// ASSERTD((do_checksum && (block->blkh_options&BLKHOPT_CHKSUM)) +// || (!do_checksum && !(block->blkh_options&BLKHOPT_CHKSUM)), "checksum configuration mismatch"); + ser_uint64(0x00); // the XXH3_64 checksum if (do_checksum) { - block->CheckSum = bcrc32((uint8_t *)block->buf+BLKHDR_CS_LENGTH, - block_len-BLKHDR_CS_LENGTH); + /* Checksum whole block including the checksum with value 0x0 */ + block->CheckSum64 = XXH3_64bits((uint8_t *)block->buf, + block_len); + /* update the checksum in the block header */ + ser_begin(block->buf+chk_off, BLKHDR_CS64_LENGTH); + ser_uint64(block->CheckSum64); + } + Dmsg3(160, "ser_block_header: adata=%d checksum=0x%016llx enc=%d\n", + block->adata, block->CheckSum64, do_encrypt_block); + block->buf_out = block->buf; + if (do_encrypt_block) { + /* Does the encryption, checksum is encrypted too */ + block_cipher_init_iv_header(block->dev->crypto_device_ctx, block->BlockNumber, block->VolSessionId, block->VolSessionTime); + block_cipher_encrypt(block->dev->crypto_device_ctx, block_len-bhl, block->buf+bhl, block->buf_enc+bhl); + memcpy(block->buf_enc, block->buf, bhl); + block->buf_out = block->buf_enc; } - Dmsg2(160, "ser_block_header: adata=%d checksum=%x\n", block->adata, block->CheckSum); - ser_begin(block->buf, BLKHDR2_LENGTH); - ser_uint32(block->CheckSum); /* now add checksum to block header */ } - return block->CheckSum; + return block->CheckSum64; } /* @@ -392,23 +446,25 @@ bool unser_block_header(DCR *dcr, DEVICE *dev, DEV_BLOCK *block) { ser_declare; char Id[BLKHDR_ID_LENGTH+1]; - uint32_t BlockCheckSum; + uint64_t BlockCheckSum64; uint32_t block_len; uint32_t block_end; uint32_t BlockNumber; + uint32_t CheckSumLo; JCR *jcr = dcr->jcr; int bhl; + int chk_off = BLKHDR_CS64_OFFSET; /* used only in BB03 */ if (block->adata) { /* Checksum the whole block */ if (block->block_len <= block->read_len && dev->do_checksum()) { - BlockCheckSum = dcr->crc32((uint8_t *)block->buf, block->block_len, block->CheckSum); - if (BlockCheckSum != block->CheckSum) { + BlockCheckSum64 = dcr->crc32((uint8_t *)block->buf, block->block_len, block->CheckSum64); + if (BlockCheckSum64 != block->CheckSum64) { dev->dev_errno = EIO; Mmsg5(dev->errmsg, _("Volume data error at %lld!\n" - "Adata block checksum mismatch in block=%u len=%d: calc=%x blk=%x\n"), + "Adata block checksum mismatch in block=%u len=%d: calc=%llx blk=%llx\n"), block->BlockAddr, block->BlockNumber, - block->block_len, BlockCheckSum, block->CheckSum); + block->block_len, BlockCheckSum64, block->CheckSum64); if (block->read_errors == 0 || verbose >= 2) { Jmsg(jcr, M_ERROR, 0, "%s", dev->errmsg); dump_block(dev, block, "with checksum error"); @@ -426,13 +482,13 @@ bool unser_block_header(DCR *dcr, DEVICE *dev, DEV_BLOCK *block) return true; } unser_begin(block->buf, BLKHDR_LENGTH); - unser_uint32(block->CheckSum); + unser_uint32(CheckSumLo); // can be blkh_options in BB03 unser_uint32(block_len); unser_uint32(BlockNumber); unser_bytes(Id, BLKHDR_ID_LENGTH); ASSERT(unser_length(block->buf) == BLKHDR1_LENGTH); Id[BLKHDR_ID_LENGTH] = 0; - + block->CheckSum64 = CheckSumLo; // only for BB01 & BB02 if (Id[3] == '1') { bhl = BLKHDR1_LENGTH; block->BlockVer = 1; @@ -470,12 +526,45 @@ bool unser_block_header(DCR *dcr, DEVICE *dev, DEV_BLOCK *block) block->read_errors++; return false; } + } else if (Id[3] == '3') { + bhl = BLKHDR3_LENGTH; + uint32_t blkh_options = CheckSumLo; /* in BB03 blkh_options is stored at the checksum location */ + unser_uint32(block->VolSessionId); + unser_uint32(block->VolSessionTime); + + /* decrypt the block before to calculate the checksum */ + if ((blkh_options & BLKHOPT_ENCRYPT_BLOCK) && block->dev->crypto_device_ctx) + { + block_cipher_init_iv_header(block->dev->crypto_device_ctx, BlockNumber, block->VolSessionId, block->VolSessionTime); + block_cipher_decrypt(block->dev->crypto_device_ctx, block_len-bhl, block->buf+bhl, block->buf_enc); + memcpy(block->buf+bhl, block->buf_enc, block_len-bhl); + } + + unser_begin(block->buf+chk_off, BLKHDR_CS64_LENGTH); + unser_uint64(block->CheckSum64); + + block->BlockVer = 3; + block->bufp = block->buf + bhl; + //Dmsg5(100, "Read-blkhdr Block=%p adata=%d buf=%p bufp=%p off=%d\n", block, block->adata, + // block->buf, block->bufp, block->bufp-block->buf); + if (strncmp(Id, BLKHDR3_ID, BLKHDR_ID_LENGTH) != 0) { + dev->dev_errno = EIO; + Mmsg4(dev->errmsg, _("Volume data error at %u:%u! Wanted ID: \"%s\", got \"%s\". Buffer discarded.\n"), + dev->get_hi_addr(block->BlockAddr), + dev->get_low_addr(block->BlockAddr), + BLKHDR3_ID, Id); + if (block->read_errors == 0 || verbose >= 2) { + Jmsg(jcr, M_ERROR, 0, "%s", dev->errmsg); + } + block->read_errors++; + return false; + } } else { dev->dev_errno = EIO; Mmsg4(dev->errmsg, _("Volume data error at %u:%u! Wanted ID: \"%s\", got \"%s\". Buffer discarded.\n"), dev->get_hi_addr(block->BlockAddr), dev->get_low_addr(block->BlockAddr), - BLKHDR2_ID, Id); + BLKHDR3_ID, Id); Dmsg1(50, "%s", dev->errmsg); if (block->read_errors == 0 || verbose >= 2) { Jmsg(jcr, M_FATAL, 0, "%s", dev->errmsg); @@ -512,16 +601,24 @@ bool unser_block_header(DCR *dcr, DEVICE *dev, DEV_BLOCK *block) Dmsg3(390, "Read binbuf = %d %d block_len=%d\n", block->binbuf, bhl, block_len); if (block_len <= block->read_len && dev->do_checksum()) { - BlockCheckSum = dcr->crc32((uint8_t *)block->buf+BLKHDR_CS_LENGTH, + if (Id[3] == '3') { + uint64_t save; /* make a copy of the xxh64 */ + memcpy(&save, block->buf+chk_off, BLKHDR_CS64_LENGTH); + memset(block->buf+chk_off, '\0', BLKHDR_CS64_LENGTH); + BlockCheckSum64 = XXH3_64bits((uint8_t *)block->buf, + block_len); + memcpy(block->buf+chk_off, &save, BLKHDR_CS64_LENGTH); + } else { + BlockCheckSum64 = dcr->crc32((uint8_t *)block->buf+BLKHDR_CS_LENGTH, block_len-BLKHDR_CS_LENGTH, - block->CheckSum); - - if (BlockCheckSum != block->CheckSum) { + block->CheckSum64); + } + if (BlockCheckSum64 != block->CheckSum64) { dev->dev_errno = EIO; Mmsg6(dev->errmsg, _("Volume data error at %u:%u!\n" - "Block checksum mismatch in block=%u len=%d: calc=%x blk=%x\n"), + "Block checksum mismatch in block=%u len=%d: calc=%llx blk=%llx\n"), dev->file, dev->block_num, (unsigned)BlockNumber, - block_len, BlockCheckSum, block->CheckSum); + block_len, BlockCheckSum64, block->CheckSum64); if (block->read_errors == 0 || verbose >= 2) { Jmsg(jcr, M_ERROR, 0, "%s", dev->errmsg); dump_block(dev, block, "with checksum error"); @@ -547,6 +644,7 @@ uint32_t get_len_and_clear_block(DEV_BLOCK *block, DEVICE *dev, uint32_t &pad) * and on tape devices, apply min and fixed blocking. */ wlen = block->binbuf; + if (wlen != block->buf_len) { Dmsg2(250, "binbuf=%d buf_len=%d\n", block->binbuf, block->buf_len); @@ -564,7 +662,7 @@ uint32_t get_len_and_clear_block(DEV_BLOCK *block, DEVICE *dev, uint32_t &pad) } } if (block->adata && dev->padding_size > 0) { - /* Write to next aligned boundry */ + /* Write to next aligned boundary */ wlen = ((wlen + dev->padding_size - 1) / dev->padding_size) * dev->padding_size; } ASSERT(wlen <= block->buf_len); diff --git a/bacula/src/stored/bsdjson.c b/bacula/src/stored/bsdjson.c index a2465f178..541ce3aeb 100644 --- a/bacula/src/stored/bsdjson.c +++ b/bacula/src/stored/bsdjson.c @@ -245,6 +245,18 @@ static void display_devtype(HPKT &hpkt) } } +static void display_enctype(HPKT &hpkt) +{ + int i; + for (i=0; enc_types[i].name; i++) { + if (*(int32_t *)(hpkt.ritem->value) == enc_types[i].token) { + hpkt.sendit(hpkt, "\n \"%s\": \"%s\"", hpkt.ritem->name, + enc_types[i].name); + return; + } + } +} + static void display_label(HPKT &hpkt) { int i; @@ -478,6 +490,8 @@ static void dump_json(display_filter *filter) display_int32_pair(hpkt); } else if (items[item].handler == store_devtype) { display_devtype(hpkt); + } else if (items[item].handler == store_enctype) { + display_enctype(hpkt); } else if (items[item].handler == store_label) { display_label(hpkt); } else if (items[item].handler == store_cloud_driver) { diff --git a/bacula/src/stored/dev.c b/bacula/src/stored/dev.c index 5d1992ee2..c5b0985fe 100644 --- a/bacula/src/stored/dev.c +++ b/bacula/src/stored/dev.c @@ -652,6 +652,10 @@ void DEVICE::term(DCR *dcr) if (device && device->dev == this) { device->dev = NULL; } + if (crypto_device_ctx) { + block_cipher_context_free(crypto_device_ctx); + crypto_device_ctx = NULL; + } delete this; } @@ -1168,3 +1172,238 @@ bool DEVICE::get_tape_worm(DCR *dcr) { return false; } + +bool DEVICE::load_encryption_key(DCR *dcr, const char *operation, + const char *volume_name, + uint32_t *enc_cipher_key_size, unsigned char *enc_cipher_key, + uint32_t *master_keyid_size, unsigned char *master_keyid) +{ + enum { op_none, op_label, op_read }; + bool ok = true; // No error + if (!device->block_encryption) { + return ok; + } + JCR *jcr = dcr->jcr; +// char *edit_device_codes(DCR *dcr, char *omsg, const char *imsg, const char *cmd) + POOLMEM *encrypt_program = get_pool_memory(PM_FNAME); + POOL_MEM results(PM_MESSAGE); + POOL_MEM err_msg(PM_MESSAGE); + POOL_MEM envv; + + int op = op_none; + if (0 == strcmp(operation, "LABEL")) { + op = op_label; + } else if (0 == strcmp(operation, "READ")) { + op = op_read; + } + + edit_device_codes(dcr, &encrypt_program, me->encryption_command, "load"); + char *envp[5]; + Mmsg(envv, "OPERATION=%s", operation); + envp[0] = bstrdup(envv.c_str()); + Mmsg(envv, "VOLUME_NAME=%s", volume_name); + envp[1] = bstrdup(envv.c_str()); + if (op == op_read && enc_cipher_key != NULL && *enc_cipher_key_size > 0) { + char buf[2*MAX_BLOCK_CIPHER_KEY_LEN]; // for the base64 encoded enc_cipherkey + bin_to_base64_pad(buf, sizeof(buf), (char *)enc_cipher_key, *enc_cipher_key_size); + Mmsg(envv, "ENC_CIPHER_KEY=%s", buf); + } else { + Mmsg(envv, "ENC_CIPHER_KEY="); + } + envp[2] = bstrdup(envv.c_str()); + if (op == op_read && master_keyid != NULL && *master_keyid_size > 0) { + char buf[2*MAX_MASTERKEY_ID_LEN]; // for the base64 encoded masterkey_id + bin_to_base64_pad(buf, sizeof(buf), (char *)master_keyid, *master_keyid_size); + Mmsg(envv, "MASTER_KEYID=%s", buf); + } else { + Mmsg(envv, "MASTER_KEYID="); + } + envp[3] = bstrdup(envv.c_str()); + envp[4] = NULL; + + Dmsg3(60, "Run keymanager op=%s volume=%s %s\n", operation, volume_name, encrypt_program); + for (char **p=envp; *p!=NULL; p++) Dmsg1(200, "keymanager query %s\n", *p); + + uint32_t timeout = 60; + int status = run_program_full_output(encrypt_program, timeout, results.addr(), envp); + free_pool_memory(encrypt_program); + for (unsigned i = 0; i < sizeof(envp)/sizeof(char *)-1; i++) + { + bfree(envp[i]); + } + + block_cipher_type cipher = BLOCK_CIPHER_NONE; + const char *cipher_name = "undefined"; + int cipher_key_size = 0; /* from the cipher */ + char *in_cipher_key = NULL; + int in_cipher_key_size = 0; + char *in_enc_cipher_key = NULL; + int in_enc_cipher_key_size = 0; + int in_master_keyid_size = 0; + char *in_master_keyid = NULL; + char *comment = NULL; + char keybuf[4096], enckeybuf[4096], masterkeyidbuf[4096]; + if (status == 0) { + if (chk_dbglvl(200)) { + /* display the response of the key manager */ + char *s = results.c_str(); + while (*s != '\0') { + char *e = strchr(s, '\n'); + if (e != NULL) { + char c = *e; + *e = '\0'; + Dmsg1(200, "keymanager response %s\n", s); + *e = c; + s = e + 1; + } + } + } + /* iterate line to retrieve field */ + char *p = results.c_str(); + char *fieldname = NULL; + char *value = NULL; + char *end; + while (*p != '\0') { + while (isspace(*p)) p++; + fieldname = p; /* start of the fieldname */ + while (isalnum(*p) || *p == '_') p++; + if (fieldname == p) { + /* fieldname is empty, EOF */ + break; + } + if (isblank(*p) || *p=='=' || *p==':') { + end = p; + } else { + Dmsg1(10, "keymanager response format mismatch at %d\n", p-results.c_str()); + Mmsg(err_msg, "line format mismatch"); + break; + } + while (isblank(*p)) p++; /* skip blank just before the separator */ + if (*p != '=' && *p != ':') { + Dmsg2(10, "keymanager response wrong separator %d %d\n", p-results.c_str(), *p); + Mmsg(err_msg, "wrong separator"); + break; + } + p++; /* skip the separator */ + while (isblank(*p)) p++; /* skip blank just after the separator */ + *end = '\0'; /* end the fieldname */ + value = p; /* start of the value */ + while (*p && *p != '\n') p++; + if (*p != '\0') { + *p = '\0'; + p++; + } + Dmsg3(200, "keymanager response fieldname=%s value=\"%s\" pos=%d\n", + fieldname, value, p-results.c_str()); + if (0==strcmp("error", fieldname)) { + Mmsg(err_msg, "got error message: \"%s\"", value); + break; + } else if (0==strcmp("volume_name", fieldname)) { + /* ignore */ + } else if (0==strcmp("comment", fieldname)) { + comment=value; + /* ignore */ + } else if (0==strcmp("cipher", fieldname)) { + cipher_name = value; + if (0==strcasecmp("AES_128_XTS", value)) { + cipher = BLOCK_CIPHER_AES_128_XTS; + cipher_key_size = 32; + } else if (0==strcasecmp("AES_256_XTS", value)) { + cipher = BLOCK_CIPHER_AES_256_XTS; + cipher_key_size = 64; + } else if (0==strcasecmp("NULL", value)) { + cipher = BLOCK_CIPHER_NULL; + cipher_key_size = 16; + } else { + cipher_key_size = 0; + Mmsg(err_msg, "unknown cipher: \"%s\"", value); + break; + } + } else if (0==strcmp("cipher_key", fieldname)) { + in_cipher_key = value; + } else if (0==strcmp("enc_cipher_key", fieldname)) { + in_enc_cipher_key = value; + } else if (0==strcmp("master_keyid", fieldname)) { + in_master_keyid = value; + } + } + if (err_msg.c_str()[0] == '\0') { + /* no error, check that we have all the parameter we need */ + if (cipher == BLOCK_CIPHER_NONE) { + Mmsg(err_msg, "cipher is missing"); + } else if (in_cipher_key == NULL) { + Mmsg(err_msg, "key is missing"); + } + } + if (err_msg.c_str()[0] == '\0' && in_cipher_key != NULL) { + /* no error, check that we can decode the key */ + in_cipher_key_size = base64_to_bin(keybuf, sizeof(keybuf), in_cipher_key, strlen(in_cipher_key)); + if (cipher_key_size != in_cipher_key_size) { + Mmsg(err_msg, "Wrong cipher key size for \"%s\" expect %d, got %d", cipher_name, cipher_key_size, in_cipher_key_size); + } + } + if (err_msg.c_str()[0] == '\0' && in_enc_cipher_key != NULL) { + /* no error, check that we can decode the key */ + in_enc_cipher_key_size = base64_to_bin(enckeybuf, sizeof(enckeybuf), in_enc_cipher_key, strlen(in_enc_cipher_key)); + if (cipher_key_size != in_enc_cipher_key_size) { + Mmsg(err_msg, "Wrong cipher key size for \"%s\" expect %d, got %d", cipher_name, cipher_key_size, in_cipher_key_size); + } + } + if (err_msg.c_str()[0] == '\0' && in_master_keyid != NULL) { + /* no error, check that we can decode the master_keyid*/ + in_master_keyid_size = base64_to_bin(masterkeyidbuf, sizeof(masterkeyidbuf), in_master_keyid, strlen(in_master_keyid)); + } + } else { + /* status != 0 the script returned an error code */ + berrno be; + be.set_errno(status); + Mmsg(err_msg, "encryption script returned an error, code=%d ERR=%s", status, be.bstrerror()); + } + if (crypto_device_ctx != NULL) { + block_cipher_context_free(crypto_device_ctx); + crypto_device_ctx = NULL; + } + if (err_msg.c_str()[0] == '\0' && in_enc_cipher_key_size > 0) { + if (in_enc_cipher_key_size > MAX_BLOCK_CIPHER_KEY_LEN) { + Mmsg(err_msg, "encrypted key is too large"); + } else { + *enc_cipher_key_size = in_enc_cipher_key_size; + memcpy(enc_cipher_key, enckeybuf, *enc_cipher_key_size); + } + } + if (err_msg.c_str()[0] == '\0' && in_master_keyid_size > 0) { + if (in_master_keyid_size > MAX_MASTERKEY_ID_LEN) { + Mmsg(err_msg, "masterkey id is too large"); + } else { + *master_keyid_size = in_master_keyid_size; + memcpy(master_keyid, masterkeyidbuf, *master_keyid_size); + } + } + + if (err_msg.c_str()[0] == '\0') { + /* initialize the crypto context */ + crypto_device_ctx = block_cipher_context_new(cipher); + block_cipher_init_key(crypto_device_ctx, (unsigned char*)keybuf); + Jmsg(jcr, M_INFO, 0, _("3305 LoadEncryptionKey \"for Volume %s\", status is OK.\n"), + dcr->VolumeName); + if (comment!=NULL) { + /* ignored for now */ + } + Dmsg1(20, "load encryption key for volume %s OK\n", dcr->VolumeName); + } + if (err_msg.c_str()[0] != '\0') { + Dmsg2(10, "load encryption key for volume %s Err=%s\n", dcr->VolumeName, err_msg.c_str()); + Jmsg(jcr, M_FATAL, 0, _("3992 Bad LoadEncryptionKey \"load Volume %s\": " + "ERR=%s\n"), dcr->VolumeName, err_msg.c_str()); + if (jcr != NULL) { + Mmsg(jcr->errmsg, _("3992 Bad LoadEncryptionKey \"load Volume %s\": " + "ERR=%s\n"), dcr->VolumeName, err_msg.c_str()); + } + ok = false; + } else { + /* crypto key successfully loaded */ + if (op == op_label) { + } + } + return ok; +} diff --git a/bacula/src/stored/dev.h b/bacula/src/stored/dev.h index 2b618dab1..92b19b14a 100644 --- a/bacula/src/stored/dev.h +++ b/bacula/src/stored/dev.h @@ -731,6 +731,11 @@ private: bool mount_tape(int mount, int dotimeout); /* in dev.c */ protected: void set_mode(int omode); /* in dev.c */ + bool load_encryption_key(DCR *dcr, const char *operation, + const char *volume_name, /* in dev.c */ + uint32_t *enc_cipher_key_size, unsigned char *enc_cipher_key, + uint32_t *master_keyid_size, unsigned char *master_keyid); + }; inline const char *DEVICE::strerror() const { return errmsg; } inline const char *DEVICE::archive_name() const { return dev_name; } diff --git a/bacula/src/stored/file_dev.c b/bacula/src/stored/file_dev.c index 9c5199446..bdb3ec037 100644 --- a/bacula/src/stored/file_dev.c +++ b/bacula/src/stored/file_dev.c @@ -186,7 +186,6 @@ bool file_dev::open_device(DCR *dcr, int omode) pm_strcat(archive_name, getVolCatName()); } } - mount(1); /* do mount if required */ set_mode(omode); diff --git a/bacula/src/stored/init_dev.c b/bacula/src/stored/init_dev.c index a0dd15909..c7eabe00b 100644 --- a/bacula/src/stored/init_dev.c +++ b/bacula/src/stored/init_dev.c @@ -314,6 +314,7 @@ void DEVICE::device_generic_init(JCR *jcr, DEVRES *device) dev->read_only = device->read_only; dev->dev_type = device->dev_type; dev->device = device; + dev->crypto_device_ctx = NULL; if (dev->is_tape()) { /* No parts on tapes */ dev->max_part_size = 0; } else { diff --git a/bacula/src/stored/label.c b/bacula/src/stored/label.c index 188dff4d6..ad5882cb3 100644 --- a/bacula/src/stored/label.c +++ b/bacula/src/stored/label.c @@ -291,6 +291,11 @@ int DEVICE::read_dev_volume_label(DCR *dcr) goto bail_out; } + if (!load_encryption_key(dcr, "READ", VolHdr.VolumeName, &VolHdr.EncCypherKeySize, VolHdr.EncCypherKey, &VolHdr.MasterKeyIdSize, VolHdr.MasterKeyId)) { + stat = VOL_LABEL_ERROR; + goto bail_out; /* a message is already loaded into jcr->errmsg */ + } + if (dcr->is_writing()) { empty_block(dcr->block); } @@ -408,6 +413,9 @@ bool DEVICE::write_volume_label(DCR *dcr, const char *VolName, } Dmsg1(150, "Label type=%d\n", dev->label_type); + if (!load_encryption_key(dcr, "LABEL", VolName, &VolHdr.EncCypherKeySize, VolHdr.EncCypherKey, &VolHdr.MasterKeyIdSize, VolHdr.MasterKeyId)) { + goto bail_out; + } if (!write_volume_label_to_dev(dcr, VolName, PoolName, relabel, no_prelabel)) { goto bail_out; } @@ -735,7 +743,11 @@ static void create_volume_label_record(DCR *dcr, DEVICE *dev, ser_string(dev->VolHdr.PoolType); ser_string(dev->VolHdr.MediaType); - ser_string(dev->VolHdr.HostName); + if (dev->device->block_encryption == ET_STRONG) { + ser_string("OBFUSCATED"); + } else { + ser_string(dev->VolHdr.HostName); + } ser_string(dev->VolHdr.LabelProg); ser_string(dev->VolHdr.ProgVersion); ser_string(dev->VolHdr.ProgDate); @@ -750,10 +762,18 @@ static void create_volume_label_record(DCR *dcr, DEVICE *dev, /* adata and dedup volumes */ ser_uint32(dev->VolHdr.BlockSize); + /* new in for BB03 */ + ser_uint32(dev->VolHdr.EncCypherKeySize); + ser_bytes(dev->VolHdr.EncCypherKey, dev->VolHdr.EncCypherKeySize); + ser_uint32(dev->VolHdr.MasterKeyIdSize); + ser_bytes(dev->VolHdr.MasterKeyId, dev->VolHdr.MasterKeyIdSize); + ser_uint32(0); /* This for a Signature Key identifier a upcoming feature */ + ser_end(rec->data, SER_LENGTH_Volume_Label); if (!adata) { bstrncpy(dcr->VolumeName, dev->VolHdr.VolumeName, sizeof(dcr->VolumeName)); } + dcr->block->first_block = true; ASSERT2(dcr->VolumeName[0], "Empty Volume name"); rec->data_len = ser_length(rec->data); rec->FileIndex = dev->VolHdr.LabelType; @@ -834,6 +854,9 @@ void create_volume_header(DEVICE *dev, const char *VolName, bstrncpy(dev->VolHdr.LabelProg, my_name, sizeof(dev->VolHdr.LabelProg)); sprintf(dev->VolHdr.ProgVersion, "Ver. %s %s ", VERSION, BDATE); sprintf(dev->VolHdr.ProgDate, "Build %s %s ", __DATE__, __TIME__); + + /* Warning in BB03 EncCypherKey* & MasterKeyId* have already been set, don't reset */ + dev->set_labeled(); /* set has Bacula label */ if (chk_dbglvl(100)) { dev->dump_volume_label(); @@ -1036,13 +1059,26 @@ bool unser_volume_label(DEVICE *dev, DEV_RECORD *rec) unser_string(dev->VolHdr.ProgVersion); unser_string(dev->VolHdr.ProgDate); -// unser_string(dev->VolHdr.AlignedVolumeName); + unser_string(dev->VolHdr.AlignedVolumeName); dev->VolHdr.AlignedVolumeName[0] = 0; unser_uint64(dev->VolHdr.FirstData); unser_uint32(dev->VolHdr.FileAlignment); unser_uint32(dev->VolHdr.PaddingSize); unser_uint32(dev->VolHdr.BlockSize); + if (rec->BlockVer == 3) { + unser_uint32(dev->VolHdr.EncCypherKeySize); + unser_bytes(dev->VolHdr.EncCypherKey, dev->VolHdr.EncCypherKeySize); + unser_uint32(dev->VolHdr.MasterKeyIdSize); + unser_bytes(dev->VolHdr.MasterKeyId, dev->VolHdr.MasterKeyIdSize); + uint32_t SignatureKeyIdSize; + unser_uint32(SignatureKeyIdSize); + (void)SignatureKeyIdSize; /* don't complain about unused variable */ + } else { + dev->VolHdr.EncCypherKeySize = 0; + dev->VolHdr.MasterKeyIdSize = 0; + } + ser_end(rec->data, SER_LENGTH_Volume_Label); Dmsg0(190, "unser_vol_label\n"); if (chk_dbglvl(100)) { @@ -1137,7 +1173,7 @@ void DEVICE::dump_volume_label() break; } - Pmsg12(-1, _("\nVolume Label:\n" + Pmsg14(-1, _("\nVolume Label:\n" "Adata : %d\n" "Id : %s" "VerNo : %d\n" @@ -1150,13 +1186,16 @@ void DEVICE::dump_volume_label() "MediaType : %s\n" "PoolType : %s\n" "HostName : %s\n" +"EncCypherKeySize : %ld\n" +"MasterKeyIdSize : %ld\n" ""), adata, VolHdr.Id, VolHdr.VerNum, VolHdr.VolumeName, VolHdr.PrevVolumeName, File, LabelType, VolHdr.LabelSize, VolHdr.PoolName, VolHdr.MediaType, - VolHdr.PoolType, VolHdr.HostName); + VolHdr.PoolType, VolHdr.HostName, + VolHdr.EncCypherKeySize, VolHdr.MasterKeyIdSize); if (VolHdr.VerNum >= 11) { char dt[50]; diff --git a/bacula/src/stored/protos.h b/bacula/src/stored/protos.h index 6892fb882..1c8948701 100644 --- a/bacula/src/stored/protos.h +++ b/bacula/src/stored/protos.h @@ -320,6 +320,7 @@ void store_truncate(LEX *lc, RES_ITEM *item, int index, int pass); bool find_truncate_option(const char* truncate, uint32_t& truncate_option); void store_upload(LEX *lc, RES_ITEM *item, int index, int pass); void store_devtype(LEX *lc, RES_ITEM *item, int index, int pass); +void store_enctype(LEX *lc, RES_ITEM *item, int index, int pass); void store_cloud_driver(LEX *lc, RES_ITEM *item, int index, int pass); void store_maxblocksize(LEX *lc, RES_ITEM *item, int index, int pass); void store_transfer_priority(LEX *lc, RES_ITEM *item, int index, int pass); diff --git a/bacula/src/stored/record.h b/bacula/src/stored/record.h index 4f87de3bb..50d82174c 100644 --- a/bacula/src/stored/record.h +++ b/bacula/src/stored/record.h @@ -121,6 +121,7 @@ struct DEV_RECORD { uint32_t extra_bytes; /* the extra size that must be accounted in VolABytes */ uint32_t state_bits; /* state bits */ uint32_t RecNum; /* Record number in the block */ + int BlockVer; /* 1, 2 or .. from BB01, BB02, ... */ uint32_t BlockNumber; /* Block number for this record (used in read_records()) */ bool invalid; /* The record may be invalid if it was merged with a previous record */ rec_state wstate; /* state of write_record_to_block */ @@ -205,9 +206,18 @@ struct Volume_Label { /* For Cloud */ uint64_t MaxPartSize; /* Maximum Part Size */ + /* For Volume encryption */ + uint32_t EncCypherKeySize; + uint32_t MasterKeyIdSize; + unsigned char EncCypherKey[MAX_BLOCK_CIPHER_KEY_LEN]; /* The encryption key + encrypted with the MasterKey */ + unsigned char MasterKeyId[MAX_MASTERKEY_ID_LEN]; /* the ID of the MasterKey */ + /* For Volume Signature (not yet implemented) */ + uint32_t SignatureKeyIdSize; + unsigned char SignatureKeyId[MAX_MASTERKEY_ID_LEN]; }; -#define SER_LENGTH_Volume_Label 1024 /* max serialised length of volume label */ +#define SER_LENGTH_Volume_Label 2048 /* max serialised length of volume label */ #define SER_LENGTH_Session_Label 1024 /* max serialised length of session label */ typedef struct Volume_Label VOLUME_LABEL; diff --git a/bacula/src/stored/record_read.c b/bacula/src/stored/record_read.c index f39f21dfb..0b925c937 100644 --- a/bacula/src/stored/record_read.c +++ b/bacula/src/stored/record_read.c @@ -63,8 +63,10 @@ static bool read_header(DCR *dcr, DEV_BLOCK *block, DEV_RECORD *rec) block->adata, block->BlockNumber, block->BlockVer, block->block_len); if (block->BlockVer == 1) { rhl = RECHDR1_LENGTH; - } else { + } if (block->BlockVer == 2) { rhl = RECHDR2_LENGTH; + } else { + rhl = RECHDR3_LENGTH; } if (rec->remlen >= rhl) { Dmsg0(dbgep, "=== rpath 2 begin unserial header\n"); @@ -250,7 +252,7 @@ bool read_record_from_block(DCR *dcr, DEV_RECORD *rec) rec->VolumeName = dcr->CurrentVol->VolumeName; /* From JCR::VolList, freed at the end */ rec->Addr = rec->StartAddr = dcr->block->BlockAddr; } - + rec->BlockVer = dcr->block->BlockVer; /* needed for unser_volume_label() */ /* We read the next record */ dcr->block->RecNum++; diff --git a/bacula/src/stored/record_write.c b/bacula/src/stored/record_write.c index fe05d31a3..e0dde2cb5 100644 --- a/bacula/src/stored/record_write.c +++ b/bacula/src/stored/record_write.c @@ -66,7 +66,7 @@ static bool write_header_to_block(DCR *dcr, DEV_BLOCK *block, DEV_RECORD *rec) Dmsg0(dbgep, "=== wpath 13 write_header_to_block\n"); ser_uint32(rec->VolSessionId); ser_uint32(rec->VolSessionTime); - } else { + } else { // BLOCK_VER is 2 or 3 Dmsg0(dbgep, "=== wpath 14 write_header_to_block\n"); block->VolSessionId = rec->VolSessionId; block->VolSessionTime = rec->VolSessionTime; diff --git a/bacula/src/stored/spool.c b/bacula/src/stored/spool.c index 011eb5830..fe5b8eed2 100644 --- a/bacula/src/stored/spool.c +++ b/bacula/src/stored/spool.c @@ -238,8 +238,8 @@ static bool despool_data(DCR *dcr, bool commit) rdev->device = dcr->dev->device; rdcr = new_dcr(jcr, NULL, rdev, SD_READ); rdcr->spool_fd = dcr->spool_fd; - block = dcr->block; /* save block */ - dcr->block = rdcr->block; /* make read and write block the same */ + block = rdcr->block; /* save block */ + rdcr->block = dcr->block; /* make read and write block the same */ Dmsg1(800, "read/write block size = %d\n", block->buf_len); lseek(rdcr->spool_fd, 0, SEEK_SET); /* rewind */ @@ -302,7 +302,7 @@ static bool despool_data(DCR *dcr, bool commit) despool_elapsed / 3600, despool_elapsed % 3600 / 60, despool_elapsed % 60, edit_uint64_with_suffix(jcr->dcr->job_spool_size / despool_elapsed, ec1)); - dcr->block = block; /* reset block */ + rdcr->block = block; /* reset block */ #if defined(HAVE_POSIX_FADVISE) && defined(POSIX_FADV_DONTNEED) posix_fadvise(rdcr->spool_fd, 0, 0, POSIX_FADV_DONTNEED); @@ -437,7 +437,6 @@ bool write_block_to_spool_file(DCR *dcr) if (block->binbuf <= WRITE_BLKHDR_LENGTH) { /* Does block have data in it? */ return true; } - hlen = sizeof(spool_hdr); wlen = block->binbuf; P(dcr->dev->spool_mutex); diff --git a/bacula/src/stored/storage_encryption.rst b/bacula/src/stored/storage_encryption.rst new file mode 100644 index 000000000..c6b1ac6f3 --- /dev/null +++ b/bacula/src/stored/storage_encryption.rst @@ -0,0 +1,440 @@ + + +New Storage Daemon encryption and XXH64 checksum +++++++++++++++++++++++++++++++++++++++++++++++++ + +This new version switch from the ``BB02`` volume format to the new ``BB03``. +This new format replaces the old 32bits CRC32 checksum with +the new and faster 64bits *XXH64*. +The new format also add support for volume encryption by the Storage Daemon. +Finally this patch add 3 new file Digest Algorithm *XXH64*, +*XXH3_64* and *XXH3_128* to the ones that already exists *MD5*, *SHA1*, *SHA256*, +*SHA512*. These 3 new *XX...* algorithms are not Cryptographic algorithms, +but are very good regarding the dispersion and randomness qualities. +There are also the fastest ones available. + +The New XXH64 checksum +====================== + +The new XXH64 checksum, is optional you can disable it using the same +directive than for the old CRC32 in ``BB02`` ``Block Checksum = no`` + +Storage Daemon encryption +========================= + +Blocks in the volumes can be encrypted using the +*BLOCK_CIPHER_AES_128_XTS* or *BLOCK_CIPHER_AES_256_XTS* ciphers algorithms. +These symmetrical ciphers are fast and used by most applications +that need to do symmetrical encryption of blocks. + +Every blocks are encrypted using a *key* that is unique for the +volume and an *IV* (Initialization vector) that is the block number +that is saved in the block header. +The *XTS's* ciphers are specifically designed to support an *IV* with a low +entropy + +The first *block* of the volume that hold the *Volume Label* +is not encrypted because some fields as the *volume name* +are required to manage the volume and the encryption. +The user has the option to obfuscate some fields +that are not required and could hold critical +information like for example the *hostname*. +These field are replaced by the string *"OBFUSCATED"* + +The header of the block are not encrypted. This 24 bytes header +don't hold any user information. Here is the content of the header: + +- the 32bits header option bit field +- the 32bits block length +- the 32bits block number +- the ``BB03`` string +- the 32bits volume session id +- the 32bits volume session time + +The *volume session time* is the time of the Storage Daemon +at startup and not the time of the backup. +The *volume session id* is reset at zero when the daemon starts +and is incremented at every backup by the Storage Daemon + +The 64bits *XXH64* checksum is encrypted with the data. +The *block* must be be decrypted and then the checksum can be verified. +If the checksum matches, you can be confident that you have the right +encryption key and that the block has not been modified. +The drawback is that it is not possible to verify the integrity of +the block without the encryption key. + +The data in the *data spool* are not encrypted, don't store you +data spool on an unsafe storage! + +The new Storage Daemon Directives +================================= + +Here are the new directives in the Storage resource of the Storage Daemon + +.. _Storage:Device:BlockEncryption: + +**Block Encryption = ** + This directive allows you to enable the encryption for the given device. + The encryption can be of 3 different types: + + **none** This is the default, the device don't do any encryption. + + **enable** The device encrypts the data but let all information in + the *volume label* in clear text. + + **strong** The device encrypts the data, and obfuscate any + information in the *volume label* except the one that are needed + for the management of the volume. The fields that are obfuscate are: + *volume name*. + +.. _Storage:Storage:EncryptionCommand: + +**Encryption Command = ** The **command** specifies an external + program that must provide the key related to a volume. + + +The Encryption Command +====================== + +The *Encryption Command* is called by the Storage Daemon every time it +initialize a new volume or mount an existing one using a device +with encryption enable. +We are providing a very simple script that can handle keys +Here is an example about how to call the command. + +:: + + EncryptionCommand = "/opt/bacula/script/sd_encryption_command.py getkey --key-dir /opt/bacula/working/keys" + +Be careful the command is limited to 127 characters. +The same variable substitution than for the *AutoChanger command* are provided. + +The program can be an interface with your existing key management system or +do the key management on its own. +*Bacula* provides a sample script *sd_encryption_command.py* that do the work. + +Lets illustrate our protocol with some usage of our script:: + + $ OPERATION=LABEL VOLUME_NAME=Volume0001 ./sd_encryption_command.py getkey --cipher AES_128_XTS --key-dir tmp/keys + cipher: AES_128_XTS + cipher_key: G6HksAYDnNGr67AAx2Lb/vecTVjZoYAqSLZ7lGMyDVE= + volume_name: Volume0001 + + $ OPERATION=READ VOLUME_NAME=Volume0001 ./sd_encryption_command.py getkey --cipher AES_128_XTS --key-dir tmp/keys + cipher: AES_128_XTS + cipher_key: G6HksAYDnNGr67AAx2Lb/vecTVjZoYAqSLZ7lGMyDVE= + volume_name: Volume0001 + + $ cat tmp/keys/Volume0001 + cipher: AES_128_XTS + cipher_key: G6HksAYDnNGr67AAx2Lb/vecTVjZoYAqSLZ7lGMyDVE= + volume_name: Volume0001 + + $ OPERATION=READ VOLUME_NAME=DontExist ./sd_encryption_command.py getkey --cipher AES_128_XTS --key-dir tmp/keys 2>/dev/null + error: no key information for volume "DontExist" + $ echo $? + 0 + + $ OPERATION=BAD_CMD VOLUME_NAME=Volume0002 ./sd_encryption_command.py getkey --cipher AES_128_XTS --key-dir tmp/keys 2>/dev/null + error: environment variable OPERATION invalid "BAD_CMD" for volume "Volume0002" + $ echo $? + 0 + +You can see in this command above that we keep the keys into one +directory *tmp/keys*, we pass the arguments using the *environment variables*. + +*Bacula* pass the following variables via the *environment*: + + **OPERATION** This is can *LABEL* when the volume is labeled, in this case + the script should generate a new key or this can be *READ* when + the volume has already a label and the Storage Daemon need the already + existing key to read or append data to the volume + + **VOLUME_NAME** This is the name of the volume + +Some variables are already there to support a *Master Key* in the future. +This feature is not yet supported, but will come later: + + **ENC_CIPHER_KEY** This is a base64 encoded version of the key encrypted by + the *master key* + + **MASTER_KEYID** This is a base64 encoded version of the key Id of + the *master key* that was used to encrypt the *ENC_CIPHER_KEY* above. + +*Bacula* expects some values in return: + + **volumename** This is a repetition of the name of the volume that is + given to the script. This field is optional and ignored by Bacula. + + **cipher** This is the cipher that Bacula must use. + Bacula knows the following ciphers: *AES_128_XTS* and *AES_256_XTS*. + Of course the key length vary with the cipher. + + **cipher_key** This is the symmetric *key* in *base 64* format. + + **comment** This is a single line of comment that is optional and ignored by Bacula. + + **error** This is a single line error message. + This is optional, but when provided, Bacula consider that the script + returned an error and display this error in the job log. + +Bacula expect an *exit code" of 0, if the script exits with a different +error code, any output are ignored and Bacula display a generic message +with the exit code in the job log. +To return an error to bacula, the script must use the *error* field +and return an error code of 0. + +What is encrypted and what is not +================================= + +The main goal of encryption is to prevent anybody that don't own the key to +read the data. And *Bacula* does it well. But encryption alone don't protect +again some modification. + +The first block of the volume is the *volume label* and it is not encrypted. +Some information are required for the management of the *volume* itself. +The only data in the *volume label* coming from the user are: the *hostname*, +the *volumename* *poolname*. The *hostname* can be obfuscated using +the *STRONG* mode of the encryption, the *poolname* and the *volumename* +could be made useless to a attacker by using generic name lie ``PoolAlpha`` or +``Volume12345``. + +Also be aware that data in the catalog: the directories, filenames, and +the *JobLog* are not encrypted. + +An attacker could make some undetected modification to the volume. +The easiest way is to remove one block inside the volume. +Other verification inside *Bacula* could detect such modification and +an attacker must be meticulous, but it is possible. + +The *XXH64* checksum inside each volume are encrypted using the encryption key. +This is not as good as using a certified signature but this provides +substantial confidence that the block will not be modified easily. + +To resume, you can be confident that: + +- An attacker cannot read any of your data: **Very Strong** +- An attacker cannot substitute the volume by another one: **Strong** +- An attacker cannot modify the content of the volume: **Good** + + +Notes for the developers and support and personal ++++++++++++++++++++++++++++++++++++++++++++++++++ + +The new Volume Format +===================== +In the new ``BB03`` volume format, the space used by the CRC32 is now +used by a 32bits bit field that for now only knows about the +two *option* bits: + +:: + + BLKHOPT_CHKSUM = 1 << 0, // If the XXH64 checksum is enable + BLKHOPT_ENCRYPT_VOL = 1 << 1, // If the volume is encrypted + BLKHOPT_ENCRYPT_BLOCK = 1 << 2, // If this block is encrypted + +Notice that the *block number* is reset to zero when the volume is happened +and then is not unique! This could be a little security concern, +as multiple block could be encrypted using the same key and *IV* + +The New XXH64 checksum +====================== + +The new XXH64 checksum, is saved just after *VolSessionTime* and is set to +zero if the checksum is disabled. The checksum is calculated on the all +block with the new 64bits checksum field set to zero. +To verify the checksum +you must first verify that *the BLKHOPT_CHKSUM* bit is enable, then un-serialize +the value of the checksum in the block and save it, then replace it with zeroes, +calculate the checksum of the block and compare it with the value that +you have saved. + + +Volume Signature +================ + +The purpose is to be able to verify: + - the authenticity of the volume (that we have the volume we are looking for) + - the integrity of the volume (that the data on the volume did not change) + +This is independent of the volume encryption. + +Some assertion to validate: + - The Volume Signature is configured on the SD side only. + - Nothing is configured on the DIR side + - The SD should be able to tell the DIR that this feature is enable on one volume, + and the DIR **could** (something to decide) store this information in the *volume media* in the catalog + - The DIR must be able to initiate a volume signature check and display the result. + We need to define an interfece for that. + - The signature are more commonly done using public/private keys pairs. + - The customer could appreciate to use different keys pairs for different groups of volumes + (for example regarding the poolname) + - The KeyManager can handle these keys the same way it is handling the encryption keys. + +Something in the volume must tell the user which key was used to sign the volume and which one can be used to verify: + - this information is not mandatory as the user could keep this information somewhere else + - we could store the keyid, the KeyManager can then provide the public key + - we can store the public key. A 4096bits public key is about 717bytes width (this include the *modulus*). + But this is not valid has me must validate the content of the volume from information from outer the volume. + Then the keyid is better solution, we are sure that the public key will come from outside. + +At the block level. + +The signature digest for a SHA256 and a 2048bits key is 122bytes. +I don't know how the size fluctuate, but the size could vary if we want to use another +hash function or maybe another key size. +Then the size in the block must be variable. +The best place at the end of the header. +How to combine the signature and the XXH64 checksum ? + +First set the signature and the XXH64 checksum spaces to 0x00. +Calculate and fill in the signature area. +Second calculate and fill in the XXH64 checksum area. +For the verification, the XXH64 checksum will always be verified while +the signature will only be verified if we can provide or verify +the authenticity of the public key. + + + +Weakness of the signature in bacula +=================================== + +The signature sign individual block. +If one block is removed from the volume we don't know it. +We could ensure that the BlockNumber is well updated +and verify that there is no missing BlockNumber. +Today the BlockNumber is reset to zero every time the volume is unmounted. +In fact when it is mounted again we don't read the value of the last BlockNumber +and restart at zero. +Even with *valid* BlockNumber we are not safe. +The last blocks of the volume could be removed and we will not know. +For that we should refresh the volume label with this information. +This is impossible with some device like a tape. + +Worst it is possible to swap some block inside a file. +An attacker could recompose a file by switching and dropping some blocks of the same file. + +This make our signature very weak. We only secure the front door. +Do we still need to implement a signature? +We don't need to decide today. What I'm doing today will have a little +impact, not more than 4 extra bytes in the volume label that could +be recycled for the next feature. + +The signature, checksum64 and encryptions together +================================================== + +The question is how to order the 3 operation at backup and restore time:: + + uint32_t header_option; // was checksum in BB02 + uint32_t block_len + uint32_t BlockNumber + char ID[4] // BB03 + uint32_t VolSessionId + uint32_t VolSessionTime + uint64_t CheckSum64 + if (header_option | BLKHOPT_SIGN_BLOCK) + { + uint32_t signature_size + char signature[signature_size] + } + char PAYLOAD[] + +notice that the checksum64 and the signature are in the middle of the block +the only way for them to protect the entire block it to reset these +area to zero before to calculate them. + +(choice 1) +CheckSum64 should be like a hardware checksum. +It should protect all the block, be computed last (after encryption). +At read time it should be checked first. (before decryption) +if we use the wrong decryption key, we will have a block +totally messed up and we will not know +and maybe some crash while the SD try to decode and use the *random* data + +(choice 2) +But It can be done in a different way! +Having the CheckSum64 calculated BEFORE the encryption. +would validate that we have the right key to decrypt the block. +In this case it should be calculated before the encryption +and at read time it should be used after decryption. +In this situation the key is required to verify that there +is no "hardware" error, but reading the volume without the right +key is useless. +This is the choice I made at first +I we are using the wrong key at restore time, the CheckSum64 +will detect it and bacula will not try to decode a corrupted block + +About the signature. +The signature is two time optional: +- we don't need to use a signature +- if we have it in a volume, we don't need to verify it and we can still read the volume. + +The signature must be independent of encryption +we must be able to verify the signature without having the key to decrypt the data. +Then the signature must be calculated AFTER the encryption +At read time it must be verified before decryption. + +In choice1:: + + do encryption + set checksum64 to zero + set signature to zero + calculate and fill in signature + calculate and fill in checksum64 + +At restore time (choice1):: + + read and copy checksum64 + set checksum64 to zero + calculate & verify checksum64 + read and copy signature + set signature to zero + calculate & verify signature <== we can stop here if we just want to check the signature + do decryption + +In choice2:: + + set signature to zero + set checksum64 to zero + calculate and fill in checksum64 + do encryption + set signature to zero one more time + calculate and fill in signature + +At restore time (choice2):: + + read and copy signature + set signature to zero + calculate & verify signature <== we can stop here if we just want to check the signature + do decryption + set signature to zero one more time + read and copy checksum64 + set checksum64 to zero + calculate & verify checksum64 + +Despite the *"set signature to zero one more time"*, and the fact that the CheckSum64 +don't work like a hardware checksum (that anybody can check), I prefer (choice2) + +Signatures +========== +Because we don't know in advance on which volume a block will be written, +and because the space for the signature must be reserved before to +start writing the records inside the volume, all the blocks written by a +device must share the same signature size. +Then some signature parameters must be defined at the device level. +The Key manager can still provide the key for the signature, but +they must match the device parameters! + +The signature should not prevent the volume to be read on any other +device or machine. The information inside the volume, that describe +the structure of the volume must be self sufficient to read the volume. + +If we want to change the configuration of the device, for example +to increase the "security" by increasing the hash or the key size, +the old volumes should be able to be read on this device. + + + +https://medium.com/@bn121rajesh/rsa-sign-and-verify-using-openssl-behind-the-scene-bf3cac0aade2 +https://github.com/bn121rajesh/ipython-notebooks/blob/master/BehindTheScene/OpensslSignVerify/openssl_sign_verify.ipynb + diff --git a/bacula/src/stored/stored_conf.c b/bacula/src/stored/stored_conf.c index 3dabaff73..7891b2182 100644 --- a/bacula/src/stored/stored_conf.c +++ b/bacula/src/stored/stored_conf.c @@ -61,7 +61,7 @@ static int const dbglvl = 200; */ static RES_ITEM store_items[] = { {"Name", store_name, ITEM(res_store.hdr.name), 0, ITEM_REQUIRED, 0}, - {"Description", store_str, ITEM(res_dir.hdr.desc), 0, 0, 0}, + {"Description", store_str, ITEM(res_store.hdr.desc), 0, 0, 0}, {"SdAddress", store_addresses_address, ITEM(res_store.sdaddrs), 0, ITEM_DEFAULT, 9103}, {"SdAddresses", store_addresses, ITEM(res_store.sdaddrs), 0, ITEM_DEFAULT, 9103}, {"Messages", store_res, ITEM(res_store.messages), R_MSGS, 0, 0}, @@ -101,6 +101,7 @@ static RES_ITEM store_items[] = { {"DedupScrubMaximumBandwidth", store_speed, ITEM(res_store.dedup_scrub_max_bandwidth), 0, ITEM_DEFAULT, 15*1024*1024}, {"MaximumContainerSize", store_size64, ITEM(res_store.max_container_size), 0, 0, 0}, #endif + {"EncryptionCommand", store_strname,ITEM(res_store.encryption_command), 0, 0, 0}, {NULL, NULL, {0}, 0, 0, 0} }; @@ -201,6 +202,8 @@ static RES_ITEM dev_items[] = { {"Dedupengine", store_res, ITEM(res_dev.dedup), R_DEDUP, 0, 0}, #endif {"SyncOnClose", store_bit, ITEM(res_dev.cap_bits), CAP_SYNCONCLOSE, ITEM_DEFAULT, 0}, + {"BlockEncryption", store_enctype,ITEM(res_dev.block_encryption), 0, ITEM_DEFAULT, 0}, + {NULL, NULL, {0}, 0, 0, 0} }; @@ -288,6 +291,41 @@ s_kw dev_types[] = { {NULL, 0} }; +/* + * Device Encryption types + * + * encryption type encryption code = token + */ +s_kw enc_types[] = { + {"None", ET_NONE}, + {"Enable", ET_ENABLE}, + {"Strong", ET_STRONG}, + {NULL, 0} +}; + +/* + * Store Encryption Type (None, Enable, Strong) + * + */ +void store_enctype(LEX *lc, RES_ITEM *item, int index, int pass) +{ + bool found = false; + + lex_get_token(lc, T_NAME); + /* Store the label pass 2 so that type is defined */ + for (int i=0; enc_types[i].name; i++) { + if (strcasecmp(lc->str, enc_types[i].name) == 0) { + *(uint32_t *)(item->value) = enc_types[i].token; + found = true; + break; + } + } + if (!found) { + scan_err1(lc, _("Expected a Device Encryption Type keyword, got: %s"), lc->str); + } + scan_to_eol(lc); + set_bit(index, res_all.hdr.item_present); +} /* * Store Device Type (File, FIFO, Tape, Cloud, ...) @@ -969,6 +1007,9 @@ void free_resource(RES *sres, int type) if (res->res_store.dedup_index_dir) { free(res->res_store.dedup_index_dir); } + if (res->res_store.encryption_command) { + free(res->res_store.encryption_command); + } break; case R_CLOUD: if (res->res_cloud.host_name) { diff --git a/bacula/src/stored/stored_conf.h b/bacula/src/stored/stored_conf.h index b6190a202..ef2b46a79 100644 --- a/bacula/src/stored/stored_conf.h +++ b/bacula/src/stored/stored_conf.h @@ -202,6 +202,8 @@ public: int64_t max_container_size; /* Maximum container size then split */ bool dedup_check_hash; /* Check Hash of each chunk after rehydration */ int64_t dedup_scrub_max_bandwidth; /* Maximum disk bandwidth usable for scrub */ + + char *encryption_command; /* encryption key command -- external program */ }; typedef class s_res_store STORES; @@ -238,6 +240,7 @@ public: bool set_vol_append_only; /* Set 'Append Only' filesystem flag for volumes */ bool set_vol_immutable; /* Set 'Immutable' filesystem flag for volumes */ bool set_vol_read_only; /* Set permission of volumes when marking them as Full/Used */ + uint32_t block_encryption; /* call the key-manager command to get the cipher and the key to use */ utime_t min_volume_protection_time; /* Minimum Volume Protection Time */ uint32_t drive_index; /* Autochanger drive index */ uint32_t cap_bits; /* Capabilities of this device */ @@ -292,3 +295,12 @@ union URES { /* Get the size of a give resource */ int get_resource_size(int type); + +extern s_kw enc_types[]; + +/* Encryption type */ +enum { + ET_NONE, + ET_ENABLE, + ET_STRONG, +};