1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00

Derive the cipher key with SHA-256 at repository format 6.

Format 6 derives encryption keys from the passphrase with SHA-256 rather than SHA-1. Older repositories keep SHA-1, since the migration does not rewrite the files they already encrypted. The digest is picked when a passphrase is generated rather than at each use, so it is stored with the passphrase in CipherSpec and defaults to SHA-256.

A passphrase outlives the format of the file it is stored in, so the format cannot decide how the key is derived. Info files at format 6 store cipher-digest beside cipher-pass, and a passphrase stored without one derives with SHA-1. stanza-upgrade writes back the digest the passphrase already had, so archives and backups from before the migration still load. Only a new passphrase gets SHA-256: the one for a stanza created at format 6, or for the manifest of a full backup written at it.

The info file has the same problem in reverse. Its format sits inside the content that the passphrase decrypts, so a reader would have to decrypt the file to find out how to decrypt it. Format 6 puts a plaintext header in front instead: PGBR, the three-digit format, and a spare byte for later. It goes where the OpenSSL Salted__ magic used to go and is the same eight bytes, so the salt still starts in the same place. A file that starts with the magic came from format 5, the only format older than the header. The header format is checked before anything is decrypted, then compared against the format found in the content, so a file stitched together from two others is rejected rather than half read.

The format constants move out of version.h into a new common/format, along with repoFormatValidate(), repoFormatDigest(), and CipherBlockFormat. That filter reads the header, builds a block cipher with the digest the format calls for, and streams the rest of the file through it. The block cipher no longer knows anything about repositories: it takes the digest from the spec and takes what sits in front of the content as a parameter.

Verify checksums info files as stored rather than as decrypted, since the header has to come off before decryption can start. A file and its copy are written from the same bytes, so comparing the two still works. In an encrypted repository created by a Perl version the file and its copy are separate encryptions, so their checksums differ and verify reports it at detail level. A log line is better than a separate code path for repositories with seven year old backups.
This commit is contained in:
David Steele
2026-09-11 18:25:19 +03:00
committed by GitHub
parent 567a245124
commit 700722b8b5
51 changed files with 1664 additions and 236 deletions

View File

@@ -1691,7 +1691,7 @@ option:
# Command-line only so that a format left in a configuration file cannot upgrade a repository as a side effect of an unrelated
# stanza-upgrade. This does not cover the environment, which can set any option that is valid for the command, so a format left
# there does migrate on the next stanza-upgrade and the option reference documents it. The allow list must be kept in sync with
# REPOSITORY_FORMAT_MIN/MAX in version.h
# REPOSITORY_FORMAT_MIN/MAX in common/format/format.h
repo-format:
group: repo
type: integer

View File

@@ -2929,7 +2929,7 @@
<allow-list caption="formats">
<allow-item id="5">Format supported by all versions since <proper>1.00</proper></allow-item>
<allow-item id="6">Adds no features yet and is reserved for features in development</allow-item>
<allow-item id="6">Derives new repository encryption keys with <id>SHA-256</id> rather than <id>SHA-1</id></allow-item>
</allow-list>
<p>An existing stanza is migrated to a newer format with the <cmd>stanza-upgrade</cmd> command rather than with this command, which sets the format only for a stanza it creates.</p>
@@ -2968,10 +2968,14 @@
<p>The first backup after a migration is a full backup even when a differential or incremental backup is requested, since no backup in the repository was written at the new format.</p>
<p>A passphrase already stored in the repository keeps deriving its key with <id>SHA-1</id>, since the files it encrypted are not rewritten by the migration. Only a new passphrase derives with <id>SHA-256</id>, i.e. the passphrase generated for each full backup written at the new format, or the passphrases of a stanza created at that format.</p>
<allow-list inherit="repo-format" command="stanza-create"/>
<p>Once the info files have been migrated, a version of <backrest/> that does not support the new format is no longer able to read the stanza at all, including the parts of it that are still at the older format. The format cannot be downgraded.</p>
<p>On an encrypted repository such a version reports an error about the cipher rather than the format, since the header written from format 6 takes the place of the magic that an older version expects to find.</p>
<p>This option cannot be set in a configuration file but it can be set in the environment, where it applies to every <cmd>stanza-upgrade</cmd> run until it is removed.</p>
<admonition type="caution">Set this option for the run that migrates the stanza. A format left in the environment migrates every stanza that is upgraded from that environment.</admonition>

View File

@@ -138,6 +138,17 @@
<p>General framework for new repository formats.</p>
</release-item>
<release-item>
<github-pull-request id="2836"/>
<release-item-contributor-list>
<release-item-contributor id="david.steele"/>
<release-item-reviewer id="douglas.j.hunley"/>
</release-item-contributor-list>
<p>Derive the cipher key with <id>SHA-256</id> at repository format 6</p>
</release-item>
<release-item>
<github-pull-request id="2826"/>

View File

@@ -241,7 +241,7 @@ cmdBackup(void)
// Build an incremental backup if type is not full (manifestPrior will be freed in this call)
if (!backupBuildIncr(manifest, manifestPrior, backupStartResult.walSegmentName))
manifestCipherSpecSet(manifest, cipherSpecGen(cfgOptionStrId(cfgOptRepoCipherType)));
manifestCipherSpecSet(manifest, cipherSpecGen(cfgOptionStrId(cfgOptRepoCipherType), manifestFormat(manifest)));
// Set delta if it is not already set and the manifest requires it
if (!cfgOptionBool(cfgOptDelta) && varBool(manifestData(manifest)->backupOptionDelta))

View File

@@ -103,7 +103,8 @@ backupFileBlockIncr(
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioWriteFilterGroup(blockMapWrite), cipherBlockNewP(cipherModeDecrypt, cipherSpecBackup, .raw = true));
ioWriteFilterGroup(blockMapWrite), cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup, .header = cipherBlockHeaderNone));
}
ioWriteOpen(blockMapWrite);
@@ -370,7 +371,8 @@ backupFile(
IoFilter *const encrypt =
cipherSpecType(cipherSpecBackup) != cipherTypeNone ?
cipherBlockNewP(
cipherModeEncrypt, cipherSpecBackup, .raw = bundleRaw || file->blockIncrSize != 0) :
cipherModeEncrypt, cipherSpecBackup,
.header = bundleRaw || file->blockIncrSize != 0 ? cipherBlockHeaderNone : cipherBlockHeaderMagic) :
NULL;
// If block incremental then add the filter and pass compress/encrypt filters to it since each block is

View File

@@ -235,7 +235,7 @@ cmdManifestBlockDeltaRender(const Manifest *const manifest, const ManifestFile *
{
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(read)),
cipherBlockNewP(cipherModeDecrypt, manifestCipherSpec(manifest), .raw = true));
cipherBlockNewP(cipherModeDecrypt, manifestCipherSpec(manifest), .header = cipherBlockHeaderNone));
}
ioReadOpen(storageReadIo(read));

View File

@@ -14,6 +14,7 @@ Remote Command
#include "common/crypto/cipherBlock.h"
#include "common/crypto/hash.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/filter/sink.h"
#include "common/io/filter/size.h"
#include "common/log.h"
@@ -44,6 +45,7 @@ static const StorageRemoteFilterHandler storageRemoteFilterHandler[] =
{.type = BLOCK_CHECKSUM_FILTER_TYPE, .handlerParam = blockChecksumNewPack},
{.type = BLOCK_INCR_FILTER_TYPE, .handlerParam = blockIncrNewPack},
{.type = CIPHER_BLOCK_FILTER_TYPE, .handlerParam = cipherBlockNewPack},
{.type = CIPHER_BLOCK_FORMAT_FILTER_TYPE, .handlerParam = cipherBlockFormatNewPack},
{.type = CRYPTO_HASH_FILTER_TYPE, .handlerParam = cryptoHashNewPack},
{.type = PAGE_CHECKSUM_FILTER_TYPE, .handlerParam = pageChecksumNewPack},
{.type = SINK_FILTER_TYPE, .handlerNoParam = ioSinkNew},

View File

@@ -9,6 +9,7 @@ Repository Get Command
#include "command/repo/get.h"
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/fdWrite.h"
#include "common/io/io.h"
#include "common/log.h"
@@ -16,6 +17,7 @@ Repository Get Command
#include "config/config.h"
#include "storage/helper.h"
#include "info/info.h"
#include "info/infoArchive.h"
#include "info/infoBackup.h"
@@ -47,6 +49,9 @@ storageGetProcess(IoWrite *const destination)
IoRead *const source = storageReadIo(
storageNewReadP(storageRepo(), file, .ignoreMissing = cfgOptionBool(cfgOptIgnoreMissing)));
// Is the file an info file, i.e. one that has a header in front of its content?
bool fileIsInfo = false;
// Add decryption if needed
if (!cfgOptionBool(cfgOptRaw))
{
@@ -96,6 +101,9 @@ storageGetProcess(IoWrite *const destination)
cfgCipherSpecMain());
cipherSpec = infoArchiveCipherSpec(info);
}
// Else the file is the archive info, which the repo passphrase opens
else
fileIsInfo = true;
}
// Backup path
@@ -128,6 +136,9 @@ storageGetProcess(IoWrite *const destination)
else
cipherSpec = cipherSpecManifest;
}
// Else the file is the backup info, which the repo passphrase opens
else
fileIsInfo = true;
}
}
@@ -137,8 +148,11 @@ storageGetProcess(IoWrite *const destination)
ASSERT(cipherSpecType(cipherSpec) != cipherTypeNone);
// Add encryption filter
cipherBlockFilterGroupAdd(ioReadFilterGroup(source), cipherModeDecrypt, cipherSpec);
// Add the decryption filter. An info file is read through the filter that reads its header.
if (fileIsInfo)
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(source), cipherSpec);
else
cipherBlockFilterGroupAdd(ioReadFilterGroup(source), cipherModeDecrypt, cipherSpec);
}
}

View File

@@ -237,7 +237,8 @@ blockDeltaNext(BlockDelta *const this, const BlockDeltaRead *const readDelta, Io
if (cipherSpecType(this->cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(this->limitRead), cipherBlockNewP(cipherModeDecrypt, this->cipherSpecBackup, .raw = true));
ioReadFilterGroup(this->limitRead), cipherBlockNewP(
cipherModeDecrypt, this->cipherSpecBackup, .header = cipherBlockHeaderNone));
}
if (this->compressType != compressTypeNone)

View File

@@ -264,7 +264,8 @@ restoreFile(
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(blockMapRead), cipherBlockNewP(cipherModeDecrypt, cipherSpecBackup, .raw = true));
ioReadFilterGroup(blockMapRead), cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup, .header = cipherBlockHeaderNone));
}
ioReadOpen(blockMapRead);
@@ -336,7 +337,11 @@ restoreFile(
// Add decryption filter
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeDecrypt, cipherSpecBackup, .raw = bundleRaw));
ioFilterGroupAdd(
filterGroup,
cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup,
.header = bundleRaw ? cipherBlockHeaderNone : cipherBlockHeaderMagic));
}
// Add decompression filter

View File

@@ -6,6 +6,7 @@ Stanza Commands Handler
#include "command/check/common.h"
#include "command/stanza/common.h"
#include "common/debug.h"
#include "common/format/format.h"
#include "common/log.h"
#include "config/config.h"
#include "db/helper.h"
@@ -16,10 +17,11 @@ Stanza Commands Handler
/**********************************************************************************************************************************/
FN_EXTERN CipherSpec *
cipherSpecGen(const CipherType cipherType)
cipherSpecGen(const CipherType cipherType, const unsigned int format)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(STRING_ID, cipherType);
FUNCTION_TEST_PARAM(UINT, format);
FUNCTION_TEST_END();
CipherSpec *result;
@@ -33,8 +35,12 @@ cipherSpecGen(const CipherType cipherType)
uint8_t buffer[48]; // 48 is the amount of entropy needed to get a 64 base key
cryptoRandomBytes(buffer, sizeof(buffer));
// The pass is the encoded text rather than the bytes it encodes, so it is stored and derived from as that text
result = cipherSpecNew(cipherType, BUFSTR(strNewEncode(encodingBase64, BUF(buffer, sizeof(buffer)))));
// The pass is the base64 text rather than the bytes it encodes, since the text is what gets stored and what the key
// derives from. A new pass derives with the digest its format calls for and that digest is stored with it, so a
// reader derives the same key.
result = cipherSpecNewP(
cipherType, BUFSTR(strNewEncode(encodingBase64, BUF(buffer, sizeof(buffer)))),
.digest = repoFormatDigest(format));
cipherSpecMove(result, memContextPrior());
}
MEM_CONTEXT_TEMP_END();

View File

@@ -10,8 +10,8 @@ Stanza Commands Handler
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
// Generate cipher spec with a new pass, none when the type is none
FN_EXTERN CipherSpec *cipherSpecGen(CipherType cipherType);
// Generate a sub pass for a file at this format, which sets the digest since that is what will derive it when it is read back
FN_EXTERN CipherSpec *cipherSpecGen(CipherType cipherType, unsigned int format);
// Validate and return database information
FN_EXTERN PgControl pgValidate(void);

View File

@@ -78,7 +78,7 @@ cmdStanzaCreate(void)
const unsigned int format = cfgOptionIdxUInt(cfgOptRepoFormat, repoIdx);
// If the repo is encrypted, generate a cipher passphrase for encrypting subsequent archive files
const CipherSpec *const cipherSpecArchive = cipherSpecGen(cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx));
const CipherSpec *const cipherSpecArchive = cipherSpecGen(cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx), format);
// Create and save archive info
infoArchive = infoArchiveNew(pgControl.version, pgControl.systemId, format, cipherSpecArchive);
@@ -86,7 +86,8 @@ cmdStanzaCreate(void)
infoArchiveSaveFile(infoArchive, storageRepoWriteStanza, INFO_ARCHIVE_PATH_FILE_STR, cfgCipherSpecMainIdx(repoIdx));
// If the repo is encrypted, generate a cipher passphrase for encrypting subsequent manifests
const CipherSpec *const cipherSpecManifest = cipherSpecGen(cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx));
const CipherSpec *const cipherSpecManifest = cipherSpecGen(
cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx), format);
// Create and save backup info
infoBackup = infoBackupNew(

View File

@@ -17,6 +17,7 @@ Verify contents of the repository.
#include "common/compress/helper.h"
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/fdWrite.h"
#include "common/io/io.h"
#include "common/log.h"
@@ -163,35 +164,6 @@ verifyInvalidFileAdd(List *const invalidFileList, const VerifyResult reason, con
FUNCTION_TEST_RETURN_VOID();
}
/***********************************************************************************************************************************
Load a file into memory
***********************************************************************************************************************************/
static StorageRead *
verifyFileLoad(const String *const pathFileName, const CipherSpec *const cipherSpec)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(STRING, pathFileName); // Fully qualified path/file name
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec); // Cipher spec to open file if encrypted
FUNCTION_TEST_END();
ASSERT(pathFileName != NULL);
// Read the file and error if missing
StorageRead *const result = storageNewReadP(storageRepo(), pathFileName);
// *read points to a location within result so update result with contents based on necessary filters
IoRead *const read = storageReadIo(result);
cipherBlockFilterGroupAdd(ioReadFilterGroup(read), cipherModeDecrypt, cipherSpec);
ioFilterGroupAdd(ioReadFilterGroup(read), cryptoHashNew(hashTypeSha1));
// If the file is compressed, add a decompression filter
if (compressTypeFromName(pathFileName) != compressTypeNone)
ioFilterGroupAdd(ioReadFilterGroup(read), decompressFilterP(compressTypeFromName(pathFileName)));
FUNCTION_TEST_RETURN(STORAGE_READ, result);
}
/***********************************************************************************************************************************
Get status of info files in the repository
***********************************************************************************************************************************/
@@ -214,18 +186,32 @@ verifyInfoFile(const String *const pathFileName, const bool keepFile, const Ciph
{
TRY_BEGIN()
{
IoRead *const infoRead = storageReadIo(verifyFileLoad(pathFileName, cipherSpec));
StorageRead *const infoStorageRead = storageNewReadP(storageRepo(), pathFileName);
IoRead *const infoRead = storageReadIo(infoStorageRead);
ioFilterGroupAdd(ioReadFilterGroup(infoRead), cryptoHashNew(hashTypeSha1));
const bool isBackup = strBeginsWith(pathFileName, INFO_BACKUP_PATH_FILE_STR);
const bool isArchive = strBeginsWith(pathFileName, INFO_ARCHIVE_PATH_FILE_STR);
// Add decryption after the hash so the checksum is over the file as stored. An info file is read through the filter
// that reads its header, a manifest with the spec as it was given since it has no header of its own.
if (isBackup || isArchive)
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(infoRead), cipherSpec);
else
cipherBlockFilterGroupAdd(ioReadFilterGroup(infoRead), cipherModeDecrypt, cipherSpec);
// If directed to keep the loaded file in memory, then move the file into the result, else drain the io and close it
if (keepFile)
{
if (strBeginsWith(pathFileName, INFO_BACKUP_PATH_FILE_STR))
if (isBackup)
result.backup = infoBackupMove(infoBackupNewLoad(infoRead, cipherSpec), memContextPrior());
else if (strBeginsWith(pathFileName, INFO_ARCHIVE_PATH_FILE_STR))
else if (isArchive)
result.archive = infoArchiveMove(infoArchiveNewLoad(infoRead, cipherSpec), memContextPrior());
else
result.manifest = manifestMove(manifestNewLoad(infoRead, cipherSpec), memContextPrior());
}
// Else nothing needs the file, so the drain runs the filters
else
ioReadDrain(infoRead);
@@ -823,11 +809,18 @@ verifyArchive(VerifyJobData *const jobData)
if (archiveResult->pgWalInfo.size == 0)
{
// Initialize the WAL segment size from the first WAL
StorageRead *const walRead = verifyFileLoad(
strNewFmt(
STORAGE_REPO_ARCHIVE "/%s/%s/%s", strZ(archiveResult->archiveId), strZ(walPath),
strZ(strLstGet(jobData->walFileList, 0))),
jobData->cipherSpecArchive);
const String *const walFile = strNewFmt(
STORAGE_REPO_ARCHIVE "/%s/%s/%s", strZ(archiveResult->archiveId), strZ(walPath),
strZ(strLstGet(jobData->walFileList, 0)));
StorageRead *const walRead = storageNewReadP(storageRepo(), walFile);
IoFilterGroup *const walFilterGroup = ioReadFilterGroup(storageReadIo(walRead));
// Add decryption filter when required
cipherBlockFilterGroupAdd(walFilterGroup, cipherModeDecrypt, jobData->cipherSpecArchive);
// If the file is compressed, add a decompression filter
if (compressTypeFromName(walFile) != compressTypeNone)
ioFilterGroupAdd(walFilterGroup, decompressFilterP(compressTypeFromName(walFile)));
const PgWal walInfo = pgWalFromBuffer(
storageGetP(walRead, .exactSize = PG_WAL_HEADER_SIZE), cfgOptionStrNull(cfgOptPgVersionForce));

View File

@@ -10,22 +10,13 @@ Block Cipher
#include "common/crypto/cipherBlock.h"
#include "common/crypto/common.h"
#include "common/crypto/common.intern.h"
#include "common/debug.h"
#include "common/io/filter/filter.h"
#include "common/log.h"
#include "common/type/convert.h"
#include "common/type/object.h"
/***********************************************************************************************************************************
Header constants and sizes
***********************************************************************************************************************************/
// Magic constant for salted encrypt. Only salted encrypt is done here, but this constant is required for compatibility with the
// openssl command-line tool.
#define CIPHER_BLOCK_MAGIC "Salted__"
#define CIPHER_BLOCK_MAGIC_SIZE (sizeof(CIPHER_BLOCK_MAGIC) - 1)
// Total length of cipher header
#define CIPHER_BLOCK_HEADER_SIZE (CIPHER_BLOCK_MAGIC_SIZE + PKCS5_SALT_LEN)
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
@@ -37,7 +28,7 @@ typedef struct CipherBlock
bool processDone; // Has any data been processed?
const Buffer *pass; // Passphrase used to generate encryption key
size_t headerSize; // Size of header read during decrypt
uint8_t header[CIPHER_BLOCK_HEADER_SIZE]; // Buffer to hold partial header during decrypt
uint8_t header[CIPHER_BLOCK_MAGIC_SIZE + PKCS5_SALT_LEN]; // Buffer to hold partial header during decrypt
const EVP_CIPHER *cipher; // Cipher object
const EVP_MD *digest; // Message digest object
EVP_CIPHER_CTX *cipherContext; // Encrypt/decrypt context
@@ -149,7 +140,7 @@ cipherBlockProcessBlock(CipherBlock *const this, const uint8_t *source, size_t s
else if (sourceSize > 0)
{
// Check if the entire header has been read
const size_t headerExpected = this->raw ? PKCS5_SALT_LEN : CIPHER_BLOCK_HEADER_SIZE;
const size_t headerExpected = this->raw ? PKCS5_SALT_LEN : CIPHER_BLOCK_MAGIC_SIZE + PKCS5_SALT_LEN;
if (this->headerSize + sourceSize >= headerExpected)
{
@@ -161,8 +152,8 @@ cipherBlockProcessBlock(CipherBlock *const this, const uint8_t *source, size_t s
source += headerExpected - this->headerSize;
sourceSize -= headerExpected - this->headerSize;
// The first bytes of the file to decrypt should be equal to the magic. If not then this is not an encrypted file,
// or at least not in a format we recognize.
// The first bytes of the file to decrypt should be equal to the magic. If not then this is not an encrypted
// file, or at least not in a format we recognize.
if (!this->raw && memcmp(this->header, CIPHER_BLOCK_MAGIC, CIPHER_BLOCK_MAGIC_SIZE) != 0)
THROW(CryptoError, "cipher header invalid");
}
@@ -389,7 +380,7 @@ cipherBlockNew(const CipherMode mode, const CipherSpec *const cipherSpec, const
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(STRING_ID, mode);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_LOG_PARAM(BOOL, param.raw);
FUNCTION_LOG_PARAM(ENUM, param.header);
FUNCTION_LOG_END();
ASSERT(cipherSpec != NULL);
@@ -409,16 +400,17 @@ cipherBlockNew(const CipherMode mode, const CipherSpec *const cipherSpec, const
zFree(cipherTypeZ);
// Lookup digest, which the spec supplies since the pass and the digest are chosen together
const EVP_MD *const digest = cryptoDigest(cipherSpecDigest(cipherSpec));
OBJ_NEW_BEGIN(CipherBlock, .childQty = MEM_CONTEXT_QTY_MAX, .callbackQty = 1)
{
*this = (CipherBlock)
{
.mode = mode,
.raw = param.raw,
.raw = param.header == cipherBlockHeaderNone,
.cipher = cipher,
// The key is always derived with SHA-1. Deriving it with anything else is not supported yet.
.digest = EVP_sha1(),
.digest = digest,
.pass = bufDup(cipherSpecPass(cipherSpec)),
};
}
@@ -433,7 +425,7 @@ cipherBlockNew(const CipherMode mode, const CipherSpec *const cipherSpec, const
pckWriteU64P(packWrite, mode);
cipherSpecPack(packWrite, cipherSpec);
pckWriteBoolP(packWrite, param.raw);
pckWriteU32P(packWrite, param.header);
pckWriteEndP(packWrite);
paramList = pckMove(pckWriteResult(packWrite), memContextPrior());
@@ -457,9 +449,9 @@ cipherBlockNewPack(const Pack *const paramList)
PackRead *const paramListPack = pckReadNew(paramList);
const CipherMode cipherMode = (CipherMode)pckReadU64P(paramListPack);
const CipherSpec *const cipherSpec = cipherSpecNewPack(paramListPack);
const bool raw = pckReadBoolP(paramListPack);
const CipherBlockHeader header = (CipherBlockHeader)pckReadU32P(paramListPack);
result = ioFilterMove(cipherBlockNewP(cipherMode, cipherSpec, .raw = raw), memContextPrior());
result = ioFilterMove(cipherBlockNewP(cipherMode, cipherSpec, .header = header), memContextPrior());
}
MEM_CONTEXT_TEMP_END();

View File

@@ -7,6 +7,13 @@ Block Cipher Header
#include "common/crypto/spec.h"
#include "common/io/filter/group.h"
/***********************************************************************************************************************************
Magic constant for salted encrypt, written before the salt unless the header is none. Anything written in place of the magic must be
the same size so that what follows still begins with the salt.
***********************************************************************************************************************************/
#define CIPHER_BLOCK_MAGIC "Salted__"
#define CIPHER_BLOCK_MAGIC_SIZE (sizeof(CIPHER_BLOCK_MAGIC) - 1)
/***********************************************************************************************************************************
Filter type constant
***********************************************************************************************************************************/
@@ -15,10 +22,19 @@ Filter type constant
/***********************************************************************************************************************************
Constructors
***********************************************************************************************************************************/
// What precedes the content of a file. The magic is what openssl writes and is the default. None saves the eight bytes the magic
// takes for a file nothing but this version reads, e.g. a file bundled into a backup, and for a file that something else has
// written in place of the magic, e.g. the format header.
typedef enum
{
cipherBlockHeaderMagic = 0, // Salted magic openssl writes
cipherBlockHeaderNone, // Nothing, to save space
} CipherBlockHeader;
typedef struct CipherBlockNewParam
{
VAR_PARAM_HEADER;
bool raw; // Omit header magic to save space
CipherBlockHeader header; // What precedes the content, the magic by default
} CipherBlockNewParam;
#define cipherBlockNewP(mode, cipherSpec, ...) \

View File

@@ -10,6 +10,7 @@ Crypto Common
#include <openssl/ssl.h>
#include "common/crypto/common.h"
#include "common/crypto/common.intern.h"
#include "common/debug.h"
#include "common/log.h"
@@ -85,3 +86,22 @@ cryptoRandomBytes(uint8_t *const buffer, const size_t size)
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN const EVP_MD *
cryptoDigest(const HashType type)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(STRING_ID, type);
FUNCTION_TEST_END();
char typeZ[STRID_MAX + 1];
strIdToZ(type, typeZ);
const EVP_MD *const result = EVP_get_digestbyname(typeZ);
if (result == NULL)
THROW_FMT(AssertError, "unable to load digest '%s'", typeZ);
FUNCTION_TEST_RETURN_TYPE_CONST_P(EVP_MD, result);
}

View File

@@ -0,0 +1,21 @@
/***********************************************************************************************************************************
Crypto Common Internal
Openssl types are used here, so this is included only by the crypto modules that work with them rather than by everything that
needs a hash type.
***********************************************************************************************************************************/
#ifndef COMMON_CRYPTO_COMMON_INTERN_H
#define COMMON_CRYPTO_COMMON_INTERN_H
#include <openssl/evp.h>
#include "common/crypto/common.h"
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
// Digest openssl knows by the name of the hash type. Error when there is no such digest, which is a coding error since a hash type
// is not read from user input.
FN_EXTERN const EVP_MD *cryptoDigest(HashType type);
#endif

View File

@@ -10,6 +10,7 @@ Cryptographic Hash
#include <openssl/hmac.h>
#include "common/crypto/common.h"
#include "common/crypto/common.intern.h"
#include "common/crypto/hash.h"
#include "common/debug.h"
#include "common/io/filter/filter.h"
@@ -193,12 +194,7 @@ cryptoHashNew(const HashType type)
// Else use the standard OpenSSL implementation
else
{
// Lookup digest
char typeZ[STRID_MAX + 1];
strIdToZ(type, typeZ);
if ((this->hashType = EVP_get_digestbyname(typeZ)) == NULL)
THROW_FMT(AssertError, "unable to load hash '%s'", typeZ);
this->hashType = cryptoDigest(type);
// Create context
cryptoError((this->hashContext = EVP_MD_CTX_new()) == NULL, "unable to create hash context");
@@ -299,12 +295,7 @@ cryptoHmacOne(const HashType type, const Buffer *const key, const Buffer *const
// Init crypto subsystem
cryptoInit();
// Lookup digest
char typeZ[STRID_MAX + 1];
strIdToZ(type, typeZ);
const EVP_MD *const hashType = EVP_get_digestbyname(typeZ);
ASSERT(hashType != NULL);
const EVP_MD *const hashType = cryptoDigest(type);
// Allocate a buffer to hold the hmac
Buffer *const result = bufNew((size_t)EVP_MD_size(hashType));

View File

@@ -17,11 +17,12 @@ struct CipherSpec
/**********************************************************************************************************************************/
FN_EXTERN CipherSpec *
cipherSpecNew(const CipherType type, const Buffer *const pass)
cipherSpecNew(const CipherType type, const Buffer *const pass, const CipherSpecNewParam param)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(STRING_ID, type);
FUNCTION_TEST_PARAM(BUFFER, pass);
FUNCTION_TEST_PARAM(STRING_ID, param.digest);
FUNCTION_TEST_END();
ASSERT((type == cipherTypeNone) == (pass == NULL));
@@ -32,7 +33,10 @@ cipherSpecNew(const CipherType type, const Buffer *const pass)
*this = (CipherSpec){.pub = {.type = type}};
if (this->pub.type != cipherTypeNone)
{
this->pub.digest = param.digest == 0 ? hashTypeSha256 : param.digest;
this->pub.pass = bufDup(pass);
}
}
OBJ_NEW_END();
@@ -56,7 +60,10 @@ cipherSpecNewPack(PackRead *const packRead)
// Nothing else was written when there is no cipher. The pass is read here rather than copied in since this context is the
// one it belongs in.
if (this->pub.type != cipherTypeNone)
{
this->pub.digest = (HashType)pckReadStrIdP(packRead);
this->pub.pass = pckReadBinP(packRead);
}
}
OBJ_NEW_END();
@@ -79,7 +86,10 @@ cipherSpecPack(PackWrite *const packWrite, const CipherSpec *const this)
// Nothing else is needed when there is no cipher
if (cipherSpecType(this) != cipherTypeNone)
{
pckWriteStrIdP(packWrite, cipherSpecDigest(this));
pckWriteBinP(packWrite, cipherSpecPass(this));
}
FUNCTION_TEST_RETURN_VOID();
}
@@ -91,6 +101,14 @@ cipherSpecToLog(const CipherSpec *const this, StringStatic *const debugLog)
char typeZ[STRID_MAX + 1];
strIdToZ(cipherSpecType(this), typeZ);
// The pass is never logged
strStcFmt(debugLog, "{type: %s}", typeZ);
// There is no digest when there is no cipher. The pass is never logged.
if (cipherSpecType(this) == cipherTypeNone)
strStcFmt(debugLog, "{type: %s}", typeZ);
else
{
char digestZ[STRID_MAX + 1];
strIdToZ(cipherSpecDigest(this), digestZ);
strStcFmt(debugLog, "{type: %s, digest: %s}", typeZ, digestZ);
}
}

View File

@@ -4,8 +4,16 @@ Cipher Spec
Everything needed to encrypt or decrypt, kept together so that adding to it does not mean changing every function and protocol
message that contains it.
The passphrase (pass) contains the bytes the key is derived from. There is no pass when the type is none, and the pass is never
logged.
The pass holds the bytes used to derive the key. The digest travels with the pass because the two are chosen together and deriving
with the wrong digest gives a wrong key instead of an error.
The pass is a buffer rather than a string because it may be binary or it may be text and nothing here needs to know which. It is
copied into the object, so the caller can release whatever it read the pass from.
The digest defaults to SHA-256, so a caller with no preference gets the digest new work should use. Repositories before format 6
derived with SHA-1, so a caller that works with that format must pass SHA-1 rather than taking the default.
There is no digest or pass when the type is none, and the pass is never logged.
***********************************************************************************************************************************/
#ifndef COMMON_CRYPTO_SPEC_H
#define COMMON_CRYPTO_SPEC_H
@@ -23,14 +31,23 @@ typedef struct CipherSpec CipherSpec;
/***********************************************************************************************************************************
Constructors
***********************************************************************************************************************************/
// Create from a pass
FN_EXTERN CipherSpec *cipherSpecNew(CipherType type, const Buffer *pass);
// Create from a pass, which is the key bytes or the passphrase text
typedef struct CipherSpecNewParam
{
VAR_PARAM_HEADER;
HashType digest; // Digest to derive the key with instead of SHA-256
} CipherSpecNewParam;
// Create for no encryption
#define cipherSpecNewP(type, pass, ...) \
cipherSpecNew(type, pass, (CipherSpecNewParam){VAR_PARAM_INIT, __VA_ARGS__})
FN_EXTERN CipherSpec *cipherSpecNew(CipherType type, const Buffer *pass, CipherSpecNewParam param);
// Create for data that is not encrypted
FN_INLINE_ALWAYS CipherSpec *
cipherSpecNewNone(void)
{
return cipherSpecNew(cipherTypeNone, NULL);
return cipherSpecNewP(cipherTypeNone, NULL);
}
// Create from a pack written by cipherSpecPack()
@@ -42,6 +59,7 @@ Getters/Setters
typedef struct CipherSpecPub
{
CipherType type; // Cipher type, none when not encrypted
HashType digest; // Digest the pass derives the key with
const Buffer *pass; // Passphrase text or key bytes
} CipherSpecPub;
@@ -52,6 +70,13 @@ cipherSpecType(const CipherSpec *const this)
return THIS_PUB(CipherSpec)->type;
}
// Digest the pass derives the key with
FN_INLINE_ALWAYS HashType
cipherSpecDigest(const CipherSpec *const this)
{
return THIS_PUB(CipherSpec)->digest;
}
// Passphrase text or key bytes
FN_INLINE_ALWAYS const Buffer *
cipherSpecPass(const CipherSpec *const this)
@@ -66,7 +91,7 @@ Functions
FN_INLINE_ALWAYS CipherSpec *
cipherSpecDup(const CipherSpec *const this)
{
return cipherSpecNew(cipherSpecType(this), cipherSpecPass(this));
return cipherSpecNewP(cipherSpecType(this), cipherSpecPass(this), .digest = cipherSpecDigest(this));
}
// Write to a pack so it can be passed over a protocol

View File

@@ -0,0 +1,432 @@
/***********************************************************************************************************************************
Cipher Block Format
***********************************************************************************************************************************/
#include <build.h>
#include <ctype.h>
#include <string.h>
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/format/format.h"
#include "common/io/filter/filter.h"
#include "common/log.h"
#include "common/type/convert.h"
#include "common/type/object.h"
#include "common/type/pack.h"
/***********************************************************************************************************************************
Format header
A file written at a format that has one begins with fixed-size plaintext naming the format it was written with. The header exists
because the digest the pass derives with follows the format, and the format is recorded inside the file that the pass encrypts. A
reader that could not see the format in advance would have to decrypt to learn what it should have decrypted with.
The header takes the place of the salted magic, which is why the content behind it is encrypted raw. Both are eight bytes followed
by the salt, so the eight bytes are consumed either way and what follows begins with the salt no matter which was there. It also
means a file of either kind is opened with the openssl command-line tool the same way: replace the first eight bytes with the magic
that tool expects.
A file that begins with the magic rather than the header was written at format 5, the only format there was before the header, so
that is not an error when a header was expected.
For the four bytes after the header magic, the first three are the format and the last is reserved for future use, e.g. naming
which key the file was encrypted with once a repository can hold more than one. The format comes first so that it is always at the
same place, which is what lets a version work out whether it can read the file at all. Once the format is identified as compatible
with this version, the spare byte is examined, and it must only be the underscore this version writes.
***********************************************************************************************************************************/
#define CIPHER_BLOCK_FORMAT_MAGIC "PGBR"
#define CIPHER_BLOCK_FORMAT_MAGIC_SIZE (sizeof(CIPHER_BLOCK_FORMAT_MAGIC) - 1)
#define CIPHER_BLOCK_FORMAT_RESERVED '_'
#define CIPHER_BLOCK_FORMAT_SIZE 3
// Total length of the header, which is the magic, the format, and the reserved byte
#define CIPHER_BLOCK_FORMAT_HEADER_SIZE \
(CIPHER_BLOCK_FORMAT_MAGIC_SIZE + CIPHER_BLOCK_FORMAT_SIZE + 1)
// The header is written in place of the magic, so the two must add up to exactly the same size or the salt would no longer begin at
// the same place
static_assert(
CIPHER_BLOCK_FORMAT_HEADER_SIZE == CIPHER_BLOCK_MAGIC_SIZE, "cipher header must be the size of the magic it replaces");
// A format too large for the digits it is written in would be truncated to a different format
static_assert(REPOSITORY_FORMAT_MAX < 1000, "repository format must fit in the header digits");
static_assert(CIPHER_BLOCK_FORMAT_SIZE == 3, "format digits must match the digits the header is written with");
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
typedef struct CipherBlockFormat
{
const CipherSpec *cipherSpec; // Cipher spec the content is decrypted with
unsigned int formatExpected; // Format the caller expects, zero when any will do
unsigned int format; // Format the header gave
uint8_t header[CIPHER_BLOCK_FORMAT_HEADER_SIZE]; // Header bytes held until there are enough to read
size_t headerSize; // Header bytes held so far
IoFilter *cipherBlock; // Block cipher the content behind the header is decrypted by
size_t sourceOffset; // Bytes of the current source taken for the header
} CipherBlockFormat;
/***********************************************************************************************************************************
Macros for function logging
***********************************************************************************************************************************/
static void
cipherBlockFormatToLog(const CipherBlockFormat *const this, StringStatic *const debugLog)
{
strStcFmt(debugLog, "{format: %u}", this->format);
}
#define FUNCTION_LOG_CIPHER_BLOCK_FORMAT_TYPE \
CipherBlockFormat *
#define FUNCTION_LOG_CIPHER_BLOCK_FORMAT_FORMAT(value, buffer, bufferSize) \
FUNCTION_LOG_OBJECT_FORMAT(value, cipherBlockFormatToLog, buffer, bufferSize)
/***********************************************************************************************************************************
Read the header and return the format it gives. A file that begins with the magic instead was written at format 5, the only format
there was before the header, so that start is not an error here.
***********************************************************************************************************************************/
static unsigned int
cipherBlockFormatHeaderRead(const uint8_t *const header)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM_P(BYTEDATA, header);
FUNCTION_TEST_END();
ASSERT(header != NULL);
const char *const headerZ = (const char *)header;
unsigned int result = REPOSITORY_FORMAT_5;
if (memcmp(headerZ, CIPHER_BLOCK_FORMAT_MAGIC, CIPHER_BLOCK_FORMAT_MAGIC_SIZE) == 0)
{
for (unsigned int digitIdx = 0; digitIdx < CIPHER_BLOCK_FORMAT_SIZE; digitIdx++)
{
if (!isdigit((unsigned char)headerZ[CIPHER_BLOCK_FORMAT_MAGIC_SIZE + digitIdx]))
THROW(FormatError, "invalid cipher header");
}
result = cvtZSubNToUInt(headerZ, CIPHER_BLOCK_FORMAT_MAGIC_SIZE, CIPHER_BLOCK_FORMAT_SIZE);
// Error on a format this version cannot read before anything is decrypted
repoFormatValidate(result);
// The format is one this version can read, so the reserved byte must be the value this version writes
if (headerZ[CIPHER_BLOCK_FORMAT_HEADER_SIZE - 1] != CIPHER_BLOCK_FORMAT_RESERVED)
THROW(FormatError, "invalid cipher header");
}
// Else the bytes must be the magic, since that is all a file with no header can begin with
else if (memcmp(headerZ, CIPHER_BLOCK_MAGIC, CIPHER_BLOCK_MAGIC_SIZE) != 0)
THROW(CryptoError, "cipher header invalid");
FUNCTION_TEST_RETURN(UINT, result);
}
/***********************************************************************************************************************************
Build the block cipher the content behind the header is decrypted by, which is only possible once the header has been read since the
header is what gives the format and the format is what gives the digest
***********************************************************************************************************************************/
static void
cipherBlockFormatCipherNew(CipherBlockFormat *const this)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_BLOCK_FORMAT, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
ASSERT(this->cipherBlock == NULL);
this->format = cipherBlockFormatHeaderRead(this->header);
// Error when the format the caller expected is not the one the file was written with
if (this->formatExpected != 0 && this->formatExpected != this->format)
THROW_FMT(FormatError, "expected repository format %u but found %u", this->formatExpected, this->format);
// The eight bytes at the front have been read and are not passed on, whichever of the two they were, so what reaches the block
// cipher begins with the salt either way and it is told to expect no header of its own
MEM_CONTEXT_OBJ_BEGIN(this)
{
this->cipherBlock = cipherBlockNewP(
cipherModeDecrypt,
cipherSpecNewP(
cipherSpecType(this->cipherSpec), cipherSpecPass(this->cipherSpec), .digest = repoFormatDigest(this->format)),
.header = cipherBlockHeaderNone);
}
MEM_CONTEXT_OBJ_END();
FUNCTION_TEST_RETURN_VOID();
}
/***********************************************************************************************************************************
Read the header and pass everything behind it through the block cipher the header selected
***********************************************************************************************************************************/
static void
cipherBlockFormatProcess(THIS_VOID, const Buffer *const source, Buffer *const destination)
{
THIS(CipherBlockFormat);
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(CIPHER_BLOCK_FORMAT, this);
FUNCTION_LOG_PARAM(BUFFER, source);
FUNCTION_LOG_PARAM(BUFFER, destination);
FUNCTION_LOG_END();
ASSERT(this != NULL);
ASSERT(destination != NULL);
if (source != NULL)
{
// Hold back the header until there is enough of it to read
if (this->cipherBlock == NULL)
{
this->sourceOffset = CIPHER_BLOCK_FORMAT_HEADER_SIZE - this->headerSize;
if (this->sourceOffset > bufUsed(source))
this->sourceOffset = bufUsed(source);
memcpy(this->header + this->headerSize, bufPtrConst(source), this->sourceOffset);
this->headerSize += this->sourceOffset;
// Nothing can be decrypted until the header is complete
if (this->headerSize < CIPHER_BLOCK_FORMAT_HEADER_SIZE)
FUNCTION_LOG_RETURN_VOID();
cipherBlockFormatCipherNew(this);
}
// Decrypt whatever of the source the header did not take
if (this->sourceOffset < bufUsed(source))
{
ioFilterProcessInOut(
this->cipherBlock, BUF(bufPtrConst(source) + this->sourceOffset, bufUsed(source) - this->sourceOffset),
destination);
}
// The header is stripped from the source only once
if (!ioFilterInputSame(this->cipherBlock))
this->sourceOffset = 0;
}
// Else all the input has been seen, so the block cipher is flushed
else
{
// A file too short to hold a header cannot have one
if (this->cipherBlock == NULL)
THROW(CryptoError, "cipher header missing");
ioFilterProcessInOut(this->cipherBlock, NULL, destination);
}
FUNCTION_LOG_RETURN_VOID();
}
/***********************************************************************************************************************************
Is the filter done?
***********************************************************************************************************************************/
static bool
cipherBlockFormatDone(const THIS_VOID)
{
THIS(const CipherBlockFormat);
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_BLOCK_FORMAT, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
FUNCTION_TEST_RETURN(BOOL, this->cipherBlock != NULL && ioFilterDone(this->cipherBlock));
}
/***********************************************************************************************************************************
Should the same input be provided again?
***********************************************************************************************************************************/
static bool
cipherBlockFormatInputSame(const THIS_VOID)
{
THIS(const CipherBlockFormat);
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_BLOCK_FORMAT, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
FUNCTION_TEST_RETURN(BOOL, this->cipherBlock != NULL && ioFilterInputSame(this->cipherBlock));
}
/***********************************************************************************************************************************
Report the format the header gave
***********************************************************************************************************************************/
static Pack *
cipherBlockFormatResultPack(THIS_VOID)
{
THIS(CipherBlockFormat);
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_BLOCK_FORMAT, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
Pack *result;
MEM_CONTEXT_TEMP_BEGIN()
{
PackWrite *const packWrite = pckWriteNewP();
pckWriteU32P(packWrite, this->format);
pckWriteEndP(packWrite);
result = pckMove(pckWriteResult(packWrite), memContextPrior());
}
MEM_CONTEXT_TEMP_END();
FUNCTION_TEST_RETURN(PACK, result);
}
/**********************************************************************************************************************************/
FN_EXTERN IoFilter *
cipherBlockFormatNew(const CipherSpec *const cipherSpec, const CipherBlockFormatNewParam param)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_LOG_PARAM(UINT, param.format);
FUNCTION_LOG_END();
FUNCTION_AUDIT_HELPER();
ASSERT(cipherSpec != NULL);
ASSERT(cipherSpecType(cipherSpec) != cipherTypeNone);
OBJ_NEW_BEGIN(CipherBlockFormat, .childQty = MEM_CONTEXT_QTY_MAX)
{
*this = (CipherBlockFormat)
{
.cipherSpec = cipherSpecDup(cipherSpec),
.formatExpected = param.format,
};
}
OBJ_NEW_END();
// Create param list
Pack *paramList;
MEM_CONTEXT_TEMP_BEGIN()
{
PackWrite *const packWrite = pckWriteNewP();
cipherSpecPack(packWrite, cipherSpec);
pckWriteU32P(packWrite, param.format);
pckWriteEndP(packWrite);
paramList = pckMove(pckWriteResult(packWrite), memContextPrior());
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN(
IO_FILTER,
ioFilterNewP(
CIPHER_BLOCK_FORMAT_FILTER_TYPE, this, paramList, .done = cipherBlockFormatDone,
.inOut = cipherBlockFormatProcess, .inputSame = cipherBlockFormatInputSame,
.result = cipherBlockFormatResultPack));
}
FN_EXTERN IoFilter *
cipherBlockFormatNewPack(const Pack *const paramList)
{
IoFilter *result = NULL;
MEM_CONTEXT_TEMP_BEGIN()
{
PackRead *const paramListPack = pckReadNew(paramList);
const CipherSpec *const cipherSpec = cipherSpecNewPack(paramListPack);
const unsigned int format = pckReadU32P(paramListPack);
result = ioFilterMove(cipherBlockFormatNewP(cipherSpec, .format = format), memContextPrior());
}
MEM_CONTEXT_TEMP_END();
return result;
}
/**********************************************************************************************************************************/
FN_EXTERN unsigned int
cipherBlockFormatResult(PackRead *const packRead)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(PACK_READ, packRead);
FUNCTION_TEST_END();
ASSERT(packRead != NULL);
FUNCTION_TEST_RETURN(UINT, pckReadU32P(packRead));
}
/**********************************************************************************************************************************/
FN_EXTERN void
cipherBlockFormatFilterGroupWriteAdd(
Buffer *const buffer, IoFilterGroup *const filterGroup, const CipherSpec *const cipherSpec, const unsigned int format)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(BUFFER, buffer);
FUNCTION_LOG_PARAM(IO_FILTER_GROUP, filterGroup);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_LOG_PARAM(UINT, format);
FUNCTION_LOG_END();
FUNCTION_AUDIT_HELPER();
ASSERT(buffer != NULL);
ASSERT(filterGroup != NULL);
ASSERT(cipherSpec != NULL);
ASSERT(format >= REPOSITORY_FORMAT_MIN && format <= REPOSITORY_FORMAT_MAX);
if (cipherSpecType(cipherSpec) != cipherTypeNone)
{
// A format from 6 writes the header in place of the magic, so the content behind it is encrypted raw
const bool header = format >= REPOSITORY_FORMAT_6;
if (header)
{
char headerZ[CIPHER_BLOCK_FORMAT_HEADER_SIZE + 1];
// Write the format zero-padded so it is always the same size, taking the digits it is written in from the value so
// that a format too large to fit could not run past them. The terminator lands on the byte after the header, which is
// not written to the buffer.
snprintf(
headerZ, sizeof(headerZ), CIPHER_BLOCK_FORMAT_MAGIC "%03u%c", format % 1000, CIPHER_BLOCK_FORMAT_RESERVED);
bufCatC(buffer, (const uint8_t *)headerZ, 0, CIPHER_BLOCK_FORMAT_HEADER_SIZE);
}
ioFilterGroupAdd(
filterGroup,
cipherBlockNewP(
cipherModeEncrypt,
cipherSpecNewP(
cipherSpecType(cipherSpec), cipherSpecPass(cipherSpec), .digest = repoFormatDigest(format)),
.header = header ? cipherBlockHeaderNone : cipherBlockHeaderMagic));
}
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN IoFilterGroup *
cipherBlockFormatFilterGroupReadAdd(IoFilterGroup *const filterGroup, const CipherSpec *const cipherSpec)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(IO_FILTER_GROUP, filterGroup);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_LOG_END();
FUNCTION_AUDIT_HELPER();
ASSERT(filterGroup != NULL);
ASSERT(cipherSpec != NULL);
if (cipherSpecType(cipherSpec) != cipherTypeNone)
ioFilterGroupAdd(filterGroup, cipherBlockFormatNewP(cipherSpec));
FUNCTION_LOG_RETURN(IO_FILTER_GROUP, filterGroup);
}

View File

@@ -0,0 +1,55 @@
/***********************************************************************************************************************************
Cipher Block Format
Encryption of a file written at a repository format, which is the format header in front of the content and the digest the format
derives the key with. Everything the format defines is here so that the block cipher itself has no knowledge of the repository.
A writer knows the format, so it writes the header itself and encrypts the content raw behind it. A reader cannot know the format
until it has read the header, and a filter cannot be added to a read once it is open, so the read side is a filter that parses
the header, works out from it what the content is encrypted with, and passes everything after it through a block cipher of its own.
***********************************************************************************************************************************/
#ifndef COMMON_FORMAT_CIPHERBLOCKFORMAT_H
#define COMMON_FORMAT_CIPHERBLOCKFORMAT_H
#include "common/crypto/spec.h"
#include "common/io/filter/group.h"
/***********************************************************************************************************************************
Filter type constant
***********************************************************************************************************************************/
#define CIPHER_BLOCK_FORMAT_FILTER_TYPE STRID5("cipher-fmt", 0x28d36e45441230)
/***********************************************************************************************************************************
Constructors
***********************************************************************************************************************************/
typedef struct CipherBlockFormatNewParam
{
VAR_PARAM_HEADER;
unsigned int format; // Format the file is expected at, any when zero
} CipherBlockFormatNewParam;
#define cipherBlockFormatNewP(cipherSpec, ...) \
cipherBlockFormatNew(cipherSpec, (CipherBlockFormatNewParam){VAR_PARAM_INIT, __VA_ARGS__})
FN_EXTERN IoFilter *cipherBlockFormatNew(const CipherSpec *cipherSpec, CipherBlockFormatNewParam param);
FN_EXTERN IoFilter *cipherBlockFormatNewPack(const Pack *paramList);
/***********************************************************************************************************************************
Getters/Setters
***********************************************************************************************************************************/
// The format the header gave, which is what the header was read for
FN_EXTERN unsigned int cipherBlockFormatResult(PackRead *packRead);
/***********************************************************************************************************************************
Helper functions
***********************************************************************************************************************************/
// Write the header a format calls for into the buffer the content will be written into and add encryption to the filter group.
// Nothing is written and nothing is added when the repository is not encrypted.
FN_EXTERN void cipherBlockFormatFilterGroupWriteAdd(
Buffer *buffer, IoFilterGroup *filterGroup, const CipherSpec *cipherSpec, unsigned int format);
// Add decryption to the filter group for a file at a format that has yet to be read. Nothing is added when the repository is not
// encrypted.
FN_EXTERN IoFilterGroup *cipherBlockFormatFilterGroupReadAdd(IoFilterGroup *filterGroup, const CipherSpec *cipherSpec);
#endif

View File

@@ -0,0 +1,51 @@
/***********************************************************************************************************************************
Repository Format
***********************************************************************************************************************************/
#include <build.h>
#include "common/debug.h"
#include "common/format/format.h"
#include "version.h"
/**********************************************************************************************************************************/
FN_EXTERN void
repoFormatValidate(const unsigned int format)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(UINT, format);
FUNCTION_TEST_END();
// A format newer than this version can read requires an upgrade. Do not suggest a version since this version cannot know which
// version added the format.
if (format > REPOSITORY_FORMAT_MAX)
{
THROW_FMT(
FormatError,
"repository format %u requires a newer version of " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format %d to %d.",
format, REPOSITORY_FORMAT_MIN, REPOSITORY_FORMAT_MAX);
}
// A format older than this version can read requires an older version to migrate the repository
if (format < REPOSITORY_FORMAT_MIN)
{
THROW_FMT(
FormatError,
"repository format %u is no longer supported by " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format %d to %d.",
format, REPOSITORY_FORMAT_MIN, REPOSITORY_FORMAT_MAX);
}
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN HashType
repoFormatDigest(const unsigned int format)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(UINT, format);
FUNCTION_TEST_END();
FUNCTION_TEST_RETURN(STRING_ID, format >= REPOSITORY_FORMAT_6 ? hashTypeSha256 : hashTypeSha1);
}

View File

@@ -0,0 +1,37 @@
/***********************************************************************************************************************************
Repository Format
Defines the format for info and manifest files as well as on-disk structure. Each info file and manifest stores the format it was
written with, so a repository may contain more than one format while older backups and archives expire.
A constant is defined for each format so that code which varies by format can be explicit about the format it applies to.
REPOSITORY_FORMAT_MIN/MAX are the range that can be read. The allow list for repo-format in build/config.yaml must be kept in sync
with MIN/MAX and its default is the format used for new repositories.
***********************************************************************************************************************************/
#ifndef COMMON_FORMAT_FORMAT_H
#define COMMON_FORMAT_FORMAT_H
#include "common/crypto/common.h"
/***********************************************************************************************************************************
Format numbers
***********************************************************************************************************************************/
#define REPOSITORY_FORMAT_5 5
#define REPOSITORY_FORMAT_6 6
#define REPOSITORY_FORMAT_MIN REPOSITORY_FORMAT_5
#define REPOSITORY_FORMAT_MAX REPOSITORY_FORMAT_6
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
// Error when a format cannot be read by this version. The format in a cipher header is checked before anything is decrypted, since
// decrypting requires knowing what the format expects and this version does not know what a newer format expects.
FN_EXTERN void repoFormatValidate(unsigned int format);
// Digest a pass stored in a file at this format derives the key with. SHA-1 is what every repository used before format 6 and is
// kept for those, so a repository that has not been migrated is read and written exactly as it was. A pass is generated with the
// digest of the file it will be stored in, since that is what a reader will derive it with.
FN_EXTERN HashType repoFormatDigest(unsigned int format);
#endif

View File

@@ -1117,8 +1117,12 @@ cfgCipherSpecMainIdx(const unsigned int repoIdx)
{
const CipherType cipherType = cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx);
configLocal->cipherSpecMain[repoIdx] = cipherSpecNew(
cipherType, cipherType == cipherTypeNone ? NULL : BUFSTR(cfgOptionIdxStr(cfgOptRepoCipherPass, repoIdx)));
// The digest this pass derives with is decided by the format of the file it opens rather than chosen with the pass,
// so every caller rebuilds the spec from that format. SHA-1 is named here for a caller that does not, since a file
// that can be opened without reading a header first is one written before the format that added the header.
configLocal->cipherSpecMain[repoIdx] = cipherSpecNewP(
cipherType, cipherType == cipherTypeNone ? NULL : BUFSTR(cfgOptionIdxStr(cfgOptRepoCipherPass, repoIdx)),
.digest = hashTypeSha1);
}
MEM_CONTEXT_END();
}

View File

@@ -9,8 +9,11 @@ Info Handler
#include "common/crypto/hash.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/format/format.h"
#include "common/ini.h"
#include "common/io/filter/filter.h"
#include "common/io/io.h"
#include "common/log.h"
#include "common/type/convert.h"
#include "common/type/json.h"
@@ -121,6 +124,7 @@ infoNew(const unsigned int format, const CipherSpec *const cipherSpecSub)
#define INFO_SECTION_BACKREST "backrest"
#define INFO_KEY_CHECKSUM "backrest-checksum"
#define INFO_SECTION_CIPHER "cipher"
#define INFO_KEY_CIPHER_DIGEST "cipher-digest"
#define INFO_KEY_CIPHER_PASS "cipher-pass"
FN_EXTERN Info *
@@ -150,6 +154,7 @@ infoNewLoad(
String *const sectionLast = strNew(); // The last section seen during load
IoFilter *const checksumActualFilter = cryptoHashNew(hashTypeSha1); // Checksum calculated from the file
const String *checksumExpected = NULL; // Checksum found in ini file
HashType cipherDigest = hashTypeSha1; // Digest the stored pass derives with
INFO_CHECKSUM_BEGIN(checksumActualFilter);
@@ -186,30 +191,10 @@ infoNewLoad(
// Validate and store format
if (strEqZ(value->key, INFO_KEY_FORMAT))
{
const uint64_t format = varUInt64(jsonToVar(value->value));
const unsigned int format = jsonReadUInt(jsonReadNew(value->value));
repoFormatValidate(format);
// A format newer than this version can read requires an upgrade. Do not suggest a version since
// this version cannot know which version added the format.
if (format > REPOSITORY_FORMAT_MAX)
{
THROW_FMT(
FormatError,
"repository format %" PRIu64 " requires a newer version of " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format %d to %d.",
format, REPOSITORY_FORMAT_MIN, REPOSITORY_FORMAT_MAX);
}
// A format older than this version can read requires an older version to migrate the repository
if (format < REPOSITORY_FORMAT_MIN)
{
THROW_FMT(
FormatError,
"repository format %" PRIu64 " is no longer supported by " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format %d to %d.",
format, REPOSITORY_FORMAT_MIN, REPOSITORY_FORMAT_MAX);
}
this->pub.format = (unsigned int)format;
this->pub.format = format;
}
// Store pgBackRest version
else if (strEqZ(value->key, INFO_KEY_VERSION))
@@ -233,14 +218,23 @@ infoNewLoad(
// Process cipher section
else if (strEqZ(value->section, INFO_SECTION_CIPHER))
{
// Store the digest the pass derives with. A file written before the digest was stored has none, so the
// default is what every repository derived with then.
if (strEqZ(value->key, INFO_KEY_CIPHER_DIGEST))
{
cipherDigest = jsonReadStrId(jsonReadNew(value->value));
}
// No validation needed for cipher-pass, just store it
if (strEqZ(value->key, INFO_KEY_CIPHER_PASS))
else if (strEqZ(value->key, INFO_KEY_CIPHER_PASS))
{
MEM_CONTEXT_OBJ_BEGIN(this)
{
// The dependent files are encrypted with the same cipher type as this one
this->pub.cipherSpec = cipherSpecNew(
cipherSpecType(cipherSpec), BUFSTR(varStr(jsonToVar(value->value))));
// The dependent files are encrypted with the same cipher type as this one and derive with the
// digest stored with the pass. The digest is read before this since the keys come out in order
// and digest sorts before pass.
this->pub.cipherSpec = cipherSpecNewP(
cipherSpecType(cipherSpec), BUFSTR(varStr(jsonToVar(value->value))),
.digest = cipherDigest);
}
MEM_CONTEXT_OBJ_END();
}
@@ -281,6 +275,22 @@ infoNewLoad(
// format is zero until the key is found and the value stored, so if we got here then the key was not found.
if (infoFormat(this) == 0)
THROW(FormatError, "repository format not found\nHINT: is this a valid " PROJECT_NAME " info file?");
// Only a read that looked for a header reports a format, so a result here means the file had one. The header is written
// from the same format as the content, so a file where they disagree has been damaged or put together from parts of two
// files.
PackRead *const cipherResult = ioFilterGroupResultP(ioReadFilterGroup(read), CIPHER_BLOCK_FORMAT_FILTER_TYPE);
if (cipherResult != NULL)
{
const unsigned int formatHeader = cipherBlockFormatResult(cipherResult);
if (this->pub.format != formatHeader)
{
THROW_FMT(
FormatError, "repository format %u does not match header format %u", this->pub.format, formatHeader);
}
}
}
MEM_CONTEXT_TEMP_END();
@@ -406,6 +416,24 @@ infoSave(Info *const this, IoWrite *const write, InfoSaveCallback *const callbac
if (cipherSpecType(infoCipherSpec(this)) != cipherTypeNone)
{
callbackFunction(callbackData, STRDEF(INFO_SECTION_CIPHER), &data);
// A format before the digest could be stored has nowhere to put it and a reader assumes SHA-1, so a pass stored at
// one of those formats must derive with SHA-1 or the reader will derive a different key. This is checked rather than
// asserted because the file is written either way and nothing reports it, leaving a pass that no reader can derive.
CHECK(
AssertError, infoFormat(this) >= REPOSITORY_FORMAT_6 || cipherSpecDigest(infoCipherSpec(this)) == hashTypeSha1,
"pass must derive with sha1 before the format that stores the digest");
// Store the digest the pass derives with so that a pass outlives the format of the file it is stored in. A pass in a
// file written before this could be stored derives with SHA-1, which is what a reader assumes when it finds no digest.
if (infoFormat(this) >= REPOSITORY_FORMAT_6)
{
char digestZ[STRID_MAX + 1];
strIdToZ(cipherSpecDigest(infoCipherSpec(this)), digestZ);
infoSaveValue(&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_DIGEST, jsonFromVar(VARSTRZ(digestZ)));
}
infoSaveValue(
&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_PASS,
jsonFromVar(VARSTR(strNewBuf(cipherSpecPass(infoCipherSpec(this))))));

View File

@@ -41,8 +41,9 @@ Constructors
***********************************************************************************************************************************/
FN_EXTERN Info *infoNew(unsigned int format, const CipherSpec *cipherSpecSub);
// Create new object and load contents from a file. The cipher spec the file is read with supplies the type for the cipher spec
// built from the pass stored in it.
// Create new object and load contents from a file. Decryption is added by the caller since which filter a file needs depends on
// whether it begins with a header. The cipher spec the file is read with supplies the type for the cipher spec built from the pass
// stored in it.
FN_EXTERN Info *infoNewLoad(
IoRead *read, const CipherSpec *cipherSpec, InfoLoadNewCallback *callbackFunction, void *callbackData);

View File

@@ -8,8 +8,8 @@ Archive Info Handler
#include <stdlib.h>
#include <string.h>
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/ini.h"
#include "common/io/bufferWrite.h"
#include "common/io/io.h"
@@ -252,8 +252,7 @@ infoArchiveLoadFileCallback(void *const data, const unsigned int try)
// Attempt to load the file
IoRead *const read = storageReadIo(storageNewReadP(loadData->storage, fileName));
cipherBlockFilterGroupAdd(
ioReadFilterGroup(read), cipherModeDecrypt, loadData->cipherSpec);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(read), loadData->cipherSpec);
MEM_CONTEXT_BEGIN(loadData->memContext)
{
@@ -340,7 +339,7 @@ infoArchiveSaveFile(
// Write output into a buffer since it needs to be saved to storage twice
Buffer *const buffer = bufNew(ioBufferSize());
IoWrite *const write = ioBufferWriteNew(buffer);
cipherBlockFilterGroupAdd(ioWriteFilterGroup(write), cipherModeEncrypt, cipherSpec);
cipherBlockFormatFilterGroupWriteAdd(buffer, ioWriteFilterGroup(write), cipherSpec, infoArchiveFormat(infoArchive));
infoArchiveSave(infoArchive, write);
// Save the file and make a copy

View File

@@ -9,8 +9,8 @@ Backup Info Handler
#include <string.h>
#include "command/backup/common.h"
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/ini.h"
#include "common/io/bufferWrite.h"
#include "common/io/io.h"
@@ -716,8 +716,7 @@ infoBackupLoadFileCallback(void *const data, const unsigned int try)
// Attempt to load the file
IoRead *const read = storageReadIo(storageNewReadP(loadData->storage, fileName));
cipherBlockFilterGroupAdd(
ioReadFilterGroup(read), cipherModeDecrypt, loadData->cipherSpec);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(read), loadData->cipherSpec);
MEM_CONTEXT_BEGIN(loadData->memContext)
{
@@ -939,7 +938,7 @@ infoBackupSaveFile(
// Write output into a buffer since it needs to be saved to storage twice
Buffer *const buffer = bufNew(ioBufferSize());
IoWrite *const write = ioBufferWriteNew(buffer);
cipherBlockFilterGroupAdd(ioWriteFilterGroup(write), cipherModeEncrypt, cipherSpec);
cipherBlockFormatFilterGroupWriteAdd(buffer, ioWriteFilterGroup(write), cipherSpec, infoBackupFormat(infoBackup));
infoBackupSave(infoBackup, write);
// Save the file and make a copy

View File

@@ -1037,7 +1037,7 @@ manifestLoadFileCallback(void *const data, const unsigned int try)
// Construct filename based on try
const String *const fileName = try == 0 ? loadData->fileName : strNewFmt("%s" INFO_COPY_EXT, strZ(loadData->fileName));
// Attempt to load the file
// Attempt to load the file. A manifest has no header of its own, so it is decrypted with the spec as it was given.
IoRead *const read = storageReadIo(storageNewReadP(loadData->storage, fileName));
cipherBlockFilterGroupAdd(ioReadFilterGroup(read), cipherModeDecrypt, loadData->cipherSpec);

View File

@@ -126,6 +126,8 @@ src_pgbackrest = [
'common/error/retry.c',
'common/exec.c',
'common/fork.c',
'common/format/cipherBlockFormat.c',
'common/format/format.c',
'common/ini.c',
'common/io/bufferRead.c',
'common/io/bufferWrite.c',

View File

@@ -24,20 +24,6 @@ Config include path name. The parent path will vary based on configuration.
***********************************************************************************************************************************/
#define PROJECT_CONFIG_INCLUDE_PATH "conf.d"
/***********************************************************************************************************************************
Format Number -- defines format for info and manifest files as well as on-disk structure. Each info file and manifest stores the
format it was written with, so a repository may contain more than one format while older backups and archives expire.
A constant is defined for each format so that code which varies by format can be explicit about the format it applies to.
REPOSITORY_FORMAT_MIN/MAX are the range that can be read. The allow list for repo-format in build/config.yaml must be kept in sync
with MIN/MAX and its default is the format used for new repositories.
***********************************************************************************************************************************/
#define REPOSITORY_FORMAT_5 5
#define REPOSITORY_FORMAT_6 6
#define REPOSITORY_FORMAT_MIN REPOSITORY_FORMAT_5
#define REPOSITORY_FORMAT_MAX REPOSITORY_FORMAT_6
/***********************************************************************************************************************************
Project version components. PROJECT_VERSION and PROJECT_VERSION_NUM are automatically generated from the component parts.
***********************************************************************************************************************************/

View File

@@ -375,6 +375,14 @@ unit:
- common/crypto/spec
- common/crypto/xxhash
# --------------------------------------------------------------------------------------------------------------------------------
- name: common/format
total: 3
coverage:
- common/format/cipherBlockFormat
- common/format/format
# --------------------------------------------------------------------------------------------------------------------------------
- name: common/io/tls
total: 6

View File

@@ -18,7 +18,7 @@ Config option constants
#define TEST_CIPHER_PASS_ARCHIVE "xarchivex"
// Cipher spec for a test pass, defaulting to the main pass, since most tests encrypt with that
#define TEST_CIPHER_SPEC_PASS(pass) cipherSpecNew(cipherTypeAes256Cbc, BUFSTRZ(pass))
#define TEST_CIPHER_SPEC_PASS(pass) cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRZ(pass))
#define TEST_CIPHER_SPEC TEST_CIPHER_SPEC_PASS(TEST_CIPHER_PASS)
/***********************************************************************************************************************************

View File

@@ -1232,7 +1232,7 @@ hrnHostBuild(const int line, const HrnHostTestDefine *const testMatrix, const si
hrnHostLocal.storage = strIdFromZ(testDef->stg);
hrnHostLocal.compressType = compressTypeEnum(strIdFromZ(testDef->cmp));
hrnHostLocal.cipherSpecMain =
testDef->enc ? cipherSpecNew(cipherTypeAes256Cbc, BUFSTRZ(HRN_CIPHER_PASSPHRASE)) : cipherSpecNewNone();
testDef->enc ? cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRZ(HRN_CIPHER_PASSPHRASE)) : cipherSpecNewNone();
hrnHostLocal.repoTotal = testDef->rt;
hrnHostLocal.tls = testDef->tls;
hrnHostLocal.bundle = testDef->bnd;

View File

@@ -6,8 +6,12 @@ Harness for Loading Test Configurations
#include <string.h>
#include "common/assert.h"
#include "common/crypto/cipherBlock.h"
#include "common/crypto/hash.h"
#include "common/format/cipherBlockFormat.h"
#include "common/format/format.h"
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "common/io/filter/filter.h"
#include "common/type/json.h"
#include "info/info.h"
@@ -116,6 +120,90 @@ harnessInfoChecksum(const String *const info)
FUNCTION_HARNESS_RETURN(BUFFER, harnessInfoChecksumFormat(REPOSITORY_FORMAT_DEFAULT, info));
}
/**********************************************************************************************************************************/
void
hrnInfoPut(const Storage *const storage, const char *const file, const char *const info, HrnInfoPutParam param)
{
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(STORAGE, storage);
FUNCTION_HARNESS_PARAM(STRINGZ, file);
FUNCTION_HARNESS_PARAM(STRINGZ, info);
FUNCTION_HARNESS_PARAM(UINT, param.format);
FUNCTION_HARNESS_PARAM(BOOL, param.header);
FUNCTION_HARNESS_PARAM(CIPHER_SPEC, param.cipherSpec);
FUNCTION_HARNESS_PARAM(STRINGZ, param.comment);
FUNCTION_HARNESS_END();
ASSERT(info != NULL);
// Default to the format a new repository is created at
if (param.format == 0)
param.format = REPOSITORY_FORMAT_DEFAULT;
const Buffer *content = harnessInfoChecksumFormat(param.format, STR(info));
// Encrypt the way the format stores the file. A file that contains a header gets it in place of the magic the cipher writes,
// and the pass derives with the digest the format calls for.
if (param.cipherSpec != NULL && cipherSpecType(param.cipherSpec) != cipherTypeNone)
{
Buffer *const encrypted = bufNew(0);
IoWrite *const write = ioBufferWriteNew(encrypted);
// If a file with a header
if (param.header)
{
cipherBlockFormatFilterGroupWriteAdd(encrypted, ioWriteFilterGroup(write), param.cipherSpec, param.format);
}
// Else a file with no header of its own, e.g. a manifest, which is still stored with the digest of its format
else
{
cipherBlockFilterGroupAdd(
ioWriteFilterGroup(write), cipherModeEncrypt,
cipherSpecNewP(
cipherSpecType(param.cipherSpec), cipherSpecPass(param.cipherSpec),
.digest = repoFormatDigest(param.format)));
}
ioWriteOpen(write);
ioWrite(write, content);
ioWriteClose(write);
content = encrypted;
}
hrnStoragePut(storage, file, content, "put info", (HrnStoragePutParam){VAR_PARAM_INIT, .comment = param.comment});
FUNCTION_HARNESS_RETURN_VOID();
}
/**********************************************************************************************************************************/
Buffer *
harnessInfoEncrypt(const Buffer *const content, const CipherSpec *const cipherSpec, const HarnessInfoEncryptParam param)
{
FUNCTION_HARNESS_BEGIN();
FUNCTION_HARNESS_PARAM(BUFFER, content);
FUNCTION_HARNESS_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_HARNESS_PARAM(UINT, param.format);
FUNCTION_HARNESS_END();
ASSERT(content != NULL);
ASSERT(cipherSpec != NULL);
Buffer *const result = bufNew(0);
IoWrite *const write = ioBufferWriteNew(result);
if (param.format == 0)
cipherBlockFilterGroupAdd(ioWriteFilterGroup(write), cipherModeEncrypt, cipherSpec);
else
cipherBlockFormatFilterGroupWriteAdd(result, ioWriteFilterGroup(write), cipherSpec, param.format);
ioWriteOpen(write);
ioWrite(write, content);
ioWriteClose(write);
FUNCTION_HARNESS_RETURN(BUFFER, result);
}
/***********************************************************************************************************************************
Test callback that logs the results to a string
***********************************************************************************************************************************/

View File

@@ -1,6 +1,7 @@
/***********************************************************************************************************************************
Harness for Generating Test Info Files
***********************************************************************************************************************************/
#include "common/format/format.h"
#include "common/type/buffer.h"
#include "info/info.h"
@@ -13,17 +14,27 @@ know this since the option supplies it everywhere else.
#define REPOSITORY_FORMAT_DEFAULT REPOSITORY_FORMAT_5
/***********************************************************************************************************************************
Write info to a file and add the checksum
Write info to a file and add the checksum, storing it the way the format stores it when it is encrypted
***********************************************************************************************************************************/
typedef struct HrnInfoPutParam
{
VAR_PARAM_HEADER;
unsigned int format; // Repository format, default format when zero
bool header; // Does the file contain a header, i.e. is it an info file?
const CipherSpec *cipherSpec; // Cipher spec when the file is encrypted, digest set by format
const char *comment; // Comment
} HrnInfoPutParam;
#define HRN_INFO_PUT(storage, file, info, ...) \
do \
{ \
hrnTestLogPrefix(__LINE__); \
hrnStoragePut( \
storage, file, harnessInfoChecksumZ(info), "put info", (HrnStoragePutParam){VAR_PARAM_INIT, __VA_ARGS__}); \
hrnInfoPut(storage, file, info, (HrnInfoPutParam){VAR_PARAM_INIT, __VA_ARGS__}); \
} \
while (0)
void hrnInfoPut(const Storage *storage, const char *file, const char *info, HrnInfoPutParam param);
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
@@ -31,4 +42,17 @@ Buffer *harnessInfoChecksum(const String *info);
Buffer *harnessInfoChecksumFormat(unsigned int format, const String *info);
Buffer *harnessInfoChecksumZ(const char *info);
// Encrypt content the way a file at a format is stored, i.e. with the header and digest that go with the format. No format means
// the file has none of its own, e.g. a manifest, so it is stored with no header and the digest the spec was given.
typedef struct HarnessInfoEncryptParam
{
VAR_PARAM_HEADER;
unsigned int format; // Repository format the file is stored at
} HarnessInfoEncryptParam;
#define harnessInfoEncryptP(content, cipherSpec, ...) \
harnessInfoEncrypt(content, cipherSpec, (HarnessInfoEncryptParam){VAR_PARAM_INIT, __VA_ARGS__})
Buffer *harnessInfoEncrypt(const Buffer *content, const CipherSpec *cipherSpec, HarnessInfoEncryptParam param);
void harnessInfoLoadNewCallback(void *callbackData, const String *section, const String *key, JsonRead *json);

View File

@@ -69,7 +69,17 @@ testStorageGet(const Storage *const storage, const char *const file, const char
// Add decrypt filter
if (param.cipherSpec != NULL && cipherSpecType(param.cipherSpec) != cipherTypeNone)
{
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeDecrypt, param.cipherSpec));
// Derive with SHA-1 since the harness reads and writes files the way a repository at the format these tests build stores
// them, which is the format that had no header to define anything else. A caller that asked for a digest would not get
// it, so only a spec at the default is accepted.
ASSERT(cipherSpecDigest(param.cipherSpec) == hashTypeSha256);
ioFilterGroupAdd(
filterGroup,
cipherBlockNewP(
cipherModeDecrypt,
cipherSpecNewP(
cipherSpecType(param.cipherSpec), cipherSpecPass(param.cipherSpec), .digest = hashTypeSha1)));
strCatFmt(
filter, "enc[%s,%s] ", zNewStrId(cipherSpecType(param.cipherSpec)),
@@ -393,7 +403,18 @@ hrnStoragePut(
// Add encrypted filter
if (param.cipherSpec != NULL && cipherSpecType(param.cipherSpec) != cipherTypeNone)
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeEncrypt, param.cipherSpec));
{
// Derive with SHA-1 to match how the harness reads these files back. A caller that asked for a digest would not get it,
// so only a spec at the default is accepted.
ASSERT(cipherSpecDigest(param.cipherSpec) == hashTypeSha256);
ioFilterGroupAdd(
filterGroup,
cipherBlockNewP(
cipherModeEncrypt,
cipherSpecNewP(
cipherSpecType(param.cipherSpec), cipherSpecPass(param.cipherSpec), .digest = hashTypeSha1)));
}
// Add file name
printf(

View File

@@ -93,7 +93,8 @@ testBackupValidateFile(
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(read)), cipherBlockNewP(cipherModeDecrypt, cipherSpecBackup, .raw = true));
ioReadFilterGroup(storageReadIo(read)), cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup, .header = cipherBlockHeaderNone));
}
ioReadOpen(storageReadIo(read));
@@ -173,7 +174,8 @@ testBackupValidateFile(
if (cipherSpecType(cipherSpecBackup) != cipherTypeNone)
{
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(read)), cipherBlockNewP(cipherModeDecrypt, cipherSpecBackup, .raw = raw));
ioReadFilterGroup(storageReadIo(read)), cipherBlockNewP(
cipherModeDecrypt, cipherSpecBackup, .header = raw ? cipherBlockHeaderNone : cipherBlockHeaderMagic));
}
if (manifestData->backupOptionCompressType != compressTypeNone)
@@ -1331,7 +1333,8 @@ testRun(void)
blockIncrNew(
3, 3, 8, 2, 4, 5, NULL, compressFilterP(compressTypeGz, 1, .raw = true),
cipherBlockNewP(
cipherModeEncrypt, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS)), .raw = true)))),
cipherModeEncrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS)),
.header = cipherBlockHeaderNone)))),
"block incr pack");
}

View File

@@ -311,7 +311,8 @@ testRun(void)
TEST_ERROR(
restoreFile(
strNewFmt(STORAGE_REPO_BACKUP "/%s/%s.gz", strZ(repoFileReferenceFull), strZ(repoFile1)), repoIdx, compressTypeGz,
0, false, false, false, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("badpass")), NULL, fileList),
0, false, false, false, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("badpass"), .digest = hashTypeSha1), NULL,
fileList),
ChecksumError,
"error restoring 'normal': actual checksum 'd1cd8a7d11daa26814b93eb604e1d49ab4b43770' does not match expected checksum"
" 'ffffffffffffffffffffffffffffffffffffffff'");
@@ -2309,7 +2310,8 @@ testRun(void)
Manifest *manifestEncrypted = manifestLoadFile(
storageRepoIdxWrite(0), STRDEF(STORAGE_REPO_BACKUP "/" TEST_LABEL "/" BACKUP_MANIFEST_FILE),
cipherSpecNewNone());
manifestCipherSpecSet(manifestEncrypted, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS_ARCHIVE)));
manifestCipherSpecSet(
manifestEncrypted, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS_ARCHIVE), .digest = hashTypeSha1));
// Open file for write
IoWrite *write = storageWriteIo(
@@ -2321,7 +2323,8 @@ testRun(void)
#define TEST_CIPHER_PASS_MANIFEST "backpass"
cipherBlockFilterGroupAdd(
ioWriteFilterGroup(write), cipherModeEncrypt,
cipherSpecNew(cfgOptionIdxStrId(cfgOptRepoCipherType, 1), BUFSTRDEF(TEST_CIPHER_PASS_MANIFEST)));
cipherSpecNewP(
cfgOptionIdxStrId(cfgOptRepoCipherType, 1), BUFSTRDEF(TEST_CIPHER_PASS_MANIFEST), .digest = hashTypeSha1));
manifestSave(manifestEncrypted, write);
// Write backup.info to the encrypted repo
@@ -2332,10 +2335,10 @@ testRun(void)
// Write archive.info to the encrypted repo
InfoArchive *infoArchive = infoArchiveNew(
PG_VERSION_11, 6569239123849665679, REPOSITORY_FORMAT_DEFAULT,
cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS_ARCHIVE)));
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS_ARCHIVE), .digest = hashTypeSha1));
infoArchiveSaveFile(
infoArchive, storageRepoIdxWrite(1), INFO_ARCHIVE_PATH_FILE_STR,
cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS)));
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS)));
TEST_RESULT_VOID(hrnCmdRestore(), "successful restore");

View File

@@ -1,6 +1,8 @@
/***********************************************************************************************************************************
Test Stanza Commands
***********************************************************************************************************************************/
#include "common/crypto/cipherBlock.h"
#include "common/io/bufferWrite.h"
#include "postgres/interface.h"
#include "postgres/version.h"
#include "storage/posix/storage.h"
@@ -23,6 +25,7 @@ testRun(void)
Storage *storageHrn = storagePosixNewP(HRN_PATH_STR, .write = true);
#define TEST_STANZA "db"
#define TEST_WAL_MIGRATE "archive/db/15-1/000000010000000000000001"
#define TEST_STANZA_OTHER "otherstanza"
StringList *argListBase = strLstNew();
@@ -137,7 +140,7 @@ testRun(void)
TEST_ASSIGN(
infoArchive,
infoArchiveLoadFile(
storageRepoIdx(1), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("12345678"))),
storageRepoIdx(1), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("12345678"))),
"load archive info from encrypted repo2");
TEST_RESULT_UINT(cipherSpecType(infoArchiveCipherSpec(infoArchive)), cipherTypeAes256Cbc, "cipher sub set");
@@ -145,7 +148,7 @@ testRun(void)
TEST_ASSIGN(
infoBackup,
infoBackupLoadFile(
storageRepoIdx(1), INFO_BACKUP_PATH_FILE_STR, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("12345678"))),
storageRepoIdx(1), INFO_BACKUP_PATH_FILE_STR, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("12345678"))),
"load backup info from encrypted repo2");
TEST_RESULT_UINT(cipherSpecType(infoBackupCipherSpec(infoBackup)), cipherTypeAes256Cbc, "cipher sub set");
@@ -185,14 +188,14 @@ testRun(void)
TEST_ASSIGN(
infoArchive,
infoArchiveLoadFile(
storageRepoIdx(3), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("87654321"))),
storageRepoIdx(3), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("87654321"))),
"load archive info from encrypted repo4");
TEST_RESULT_UINT(cipherSpecType(infoArchiveCipherSpec(infoArchive)), cipherTypeAes256Cbc, "cipher sub set");
TEST_ASSIGN(
infoBackup,
infoBackupLoadFile(
storageRepoIdx(3), INFO_BACKUP_PATH_FILE_STR, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("87654321"))),
storageRepoIdx(3), INFO_BACKUP_PATH_FILE_STR, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("87654321"))),
"load backup info from encrypted repo4");
TEST_RESULT_UINT(cipherSpecType(infoBackupCipherSpec(infoBackup)), cipherTypeAes256Cbc, "cipher sub set");
@@ -1190,6 +1193,68 @@ testRun(void)
hrnCfgEnvKeyRemoveRaw(cfgOptRepoFormat, 1);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("stanza-upgrade - a file encrypted before a format migration is readable after it");
// A stanza on an encrypted repository at the format before the digest could be stored
argList = strLstNew();
hrnCfgArgRawBool(argList, cfgOptOnline, false);
hrnCfgArgRawZ(argList, cfgOptStanza, TEST_STANZA);
hrnCfgArgRawZ(argList, cfgOptPgPath, TEST_PATH "/pg");
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 1, TEST_PATH "/repo-migrate");
hrnCfgArgRawZ(argList, cfgOptPgVersionForce, "15");
hrnCfgArgKeyRawStrId(argList, cfgOptRepoCipherType, 1, cipherTypeAes256Cbc);
hrnCfgEnvKeyRawZ(cfgOptRepoCipherPass, 1, "12345678");
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
TEST_RESULT_VOID(cmdStanzaCreate(), "stanza create on encrypted repo");
TEST_RESULT_LOG("P00 INFO: stanza-create for stanza 'db' on repo1");
const CipherSpec *const cipherSpecMain = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("12345678"));
const InfoArchive *infoArchiveMigrate = NULL;
TEST_ASSIGN(
infoArchiveMigrate, infoArchiveLoadFile(storageRepoIdx(0), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecMain),
"load archive info before migration");
// Write a file with the archive sub-pass, which is how everything below the info files is encrypted
Buffer *const walBuffer = bufNew(0);
IoWrite *const walWrite = ioBufferWriteNew(walBuffer);
cipherBlockFilterGroupAdd(
ioWriteFilterGroup(walWrite), cipherModeEncrypt, infoArchiveCipherSpec(infoArchiveMigrate));
ioWriteOpen(walWrite);
ioWrite(walWrite, BUFSTRDEF("WAL BEFORE MIGRATION"));
ioWriteClose(walWrite);
HRN_STORAGE_PUT(storageRepoIdxWrite(0), TEST_WAL_MIGRATE, walBuffer, .comment = "wal written at format 5");
// Migrate the repository
hrnCfgArgKeyRawZ(argList, cfgOptRepoFormat, 1, "6");
HRN_CFG_LOAD(cfgCmdStanzaUpgrade, argList);
TEST_RESULT_VOID(cmdStanzaUpgrade(), "stanza upgrade - format 6 on encrypted repo");
TEST_RESULT_LOG(
"P00 INFO: stanza-upgrade for stanza 'db' on repo1\n"
"P00 INFO: upgrade repository format from 5 to 6");
// The pass is unchanged by the migration, so a file it encrypted before the migration must still be readable. The digest
// the pass derives with cannot follow the format of the info file storing it or this read would fail.
TEST_ASSIGN(
infoArchiveMigrate, infoArchiveLoadFile(storageRepoIdx(0), INFO_ARCHIVE_PATH_FILE_STR, cipherSpecMain),
"load archive info after migration");
TEST_RESULT_UINT(infoArchiveFormat(infoArchiveMigrate), REPOSITORY_FORMAT_6, "archive info at format 6");
StorageRead *const walRead = storageNewReadP(storageRepoIdx(0), STRDEF(TEST_WAL_MIGRATE));
cipherBlockFilterGroupAdd(
ioReadFilterGroup(storageReadIo(walRead)), cipherModeDecrypt, infoArchiveCipherSpec(infoArchiveMigrate));
TEST_RESULT_STR_Z(
strNewBuf(storageGetP(walRead)), "WAL BEFORE MIGRATION", "wal from before the migration is still readable");
hrnCfgEnvKeyRemoveRaw(cfgOptRepoCipherPass, 1);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("stanza-upgrade - every format that can be read can be requested");

View File

@@ -876,22 +876,57 @@ testRun(void)
hrnCfgEnvKeyRawZ(cfgOptRepoCipherPass, 1, TEST_CIPHER_PASS);
HRN_CFG_LOAD(cfgCmdVerify, argList);
// The copy is a copy of the file rather than a second encryption of the same content, which is how the info files are
// saved, i.e. the content is built once in a buffer and that buffer is written twice
HRN_INFO_PUT(
storageRepoWrite(), INFO_BACKUP_PATH_FILE, TEST_NO_CURRENT_BACKUP, .cipherSpec = TEST_CIPHER_SPEC,
.comment = "encrypted backup.info");
storageRepoWrite(), INFO_BACKUP_PATH_FILE, TEST_NO_CURRENT_BACKUP, .header = true,
.cipherSpec = TEST_CIPHER_SPEC, .comment = "encrypted backup.info");
HRN_STORAGE_COPY(
storageRepo(), INFO_BACKUP_PATH_FILE, storageRepoWrite(), INFO_BACKUP_PATH_FILE INFO_COPY_EXT,
.comment = "encrypted backup.info.copy");
HRN_INFO_PUT(
storageRepoWrite(), INFO_BACKUP_PATH_FILE INFO_COPY_EXT, TEST_NO_CURRENT_BACKUP,
.cipherSpec = TEST_CIPHER_SPEC, .comment = "encrypted backup.info.copy");
HRN_INFO_PUT(
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE, TEST_ARCHIVE_INFO_MULTI_HISTORY_BASE,
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE, TEST_ARCHIVE_INFO_MULTI_HISTORY_BASE, .header = true,
.cipherSpec = TEST_CIPHER_SPEC, .comment = "encrypted archive.info");
HRN_INFO_PUT(
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE INFO_COPY_EXT, TEST_ARCHIVE_INFO_MULTI_HISTORY_BASE,
.cipherSpec = TEST_CIPHER_SPEC, .comment = "encrypted archive.info.copy");
HRN_STORAGE_COPY(
storageRepo(), INFO_ARCHIVE_PATH_FILE, storageRepoWrite(), INFO_ARCHIVE_PATH_FILE INFO_COPY_EXT,
.comment = "encrypted archive.info.copy");
TEST_RESULT_VOID(cmdVerify(), "usable encrypted backup and archive info files");
TEST_RESULT_LOG("");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("encrypted info files at format 6");
HRN_INFO_PUT(
storageRepoWrite(), INFO_BACKUP_PATH_FILE, TEST_NO_CURRENT_BACKUP, .format = REPOSITORY_FORMAT_6, .header = true,
.cipherSpec = TEST_CIPHER_SPEC, .comment = "encrypted backup.info at format 6");
HRN_STORAGE_COPY(
storageRepo(), INFO_BACKUP_PATH_FILE, storageRepoWrite(), INFO_BACKUP_PATH_FILE INFO_COPY_EXT,
.comment = "encrypted backup.info.copy at format 6");
HRN_INFO_PUT(
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE, TEST_ARCHIVE_INFO_MULTI_HISTORY_BASE, .format = REPOSITORY_FORMAT_6,
.header = true, .cipherSpec = TEST_CIPHER_SPEC, .comment = "encrypted archive.info at format 6");
HRN_STORAGE_COPY(
storageRepo(), INFO_ARCHIVE_PATH_FILE, storageRepoWrite(), INFO_ARCHIVE_PATH_FILE INFO_COPY_EXT,
.comment = "encrypted archive.info.copy at format 6");
TEST_RESULT_VOID(cmdVerify(), "usable encrypted backup and archive info files at format 6");
TEST_RESULT_LOG("");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("a copy that cannot be decrypted is reported as such rather than as a checksum that does not match");
// Damage the content of the copy, which nothing but the drain reads, so that it no longer decrypts
Buffer *const archiveInfoCopy = bufDup(
storageGetP(storageNewReadP(storageRepo(), STRDEF(INFO_ARCHIVE_PATH_FILE INFO_COPY_EXT))));
bufPtr(archiveInfoCopy)[bufUsed(archiveInfoCopy) - 1] ^= 0xff;
HRN_STORAGE_PUT(
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE INFO_COPY_EXT, archiveInfoCopy, .comment = "damaged archive.info.copy");
TEST_RESULT_VOID(cmdVerify(), "encrypted archive.info.copy that cannot be decrypted");
TEST_RESULT_LOG("P00 DETAIL: unable to flush");
hrnCfgEnvKeyRemoveRaw(cfgOptRepoCipherPass, 1);
harnessLogLevelReset();
}
@@ -941,12 +976,12 @@ testRun(void)
TEST_RESULT_UINT(
verifyFile(
filePathName, 0, NULL, compressTypeGz, fileChecksum, fileSize,
cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("pass"))),
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("pass"), .digest = hashTypeSha1)),
verifyOk, "file encrypted compressed ok");
TEST_RESULT_UINT(
verifyFile(
filePathName, 0, NULL, compressTypeGz, bufNewDecode(encodingHex, STRDEF("aa")), fileSize,
cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("pass"))),
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("pass"), .digest = hashTypeSha1)),
verifyChecksumMismatch, "file encrypted compressed checksum mismatch");
}

View File

@@ -2,9 +2,11 @@
Test Block Cipher
***********************************************************************************************************************************/
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "common/io/filter/filter.h"
#include "common/io/io.h"
#include "common/type/json.h"
#include "version.h"
/***********************************************************************************************************************************
Data for testing
@@ -14,6 +16,9 @@ Data for testing
#define TEST_PLAINTEXT "plaintext"
#define TEST_BUFFER_SIZE 256
// What a file that is not raw begins with, i.e. the magic and the salt behind it
#define TEST_HEADER_SIZE (CIPHER_BLOCK_MAGIC_SIZE + PKCS5_SALT_LEN)
/***********************************************************************************************************************************
Test Run
***********************************************************************************************************************************/
@@ -72,21 +77,26 @@ testRun(void)
// *****************************************************************************************************************************
if (testBegin("CipherBlock"))
{
// Cipher error
// Cipher and digest errors
// -------------------------------------------------------------------------------------------------------------------------
TEST_ERROR(
cipherBlockNewP(cipherModeEncrypt, cipherSpecNew(strIdFromZ(BOGUS_STR), testPass)), AssertError,
cipherBlockNewP(cipherModeEncrypt, cipherSpecNewP(strIdFromZ(BOGUS_STR), testPass)), AssertError,
"unable to load cipher 'BOGUS'");
TEST_ERROR(
cipherBlockNewP(cipherModeEncrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass, .digest = strIdFromZ(BOGUS_STR))),
AssertError, "unable to load digest 'BOGUS'");
// Initialization of object
// -------------------------------------------------------------------------------------------------------------------------
// Build from a duplicate to show the copy contains the type and pass of the original
// Build from a duplicate to show the copy contains the type, digest, and pass of the original
TEST_RESULT_UINT(cipherSpecType(cipherSpecDup(cipherSpecNewNone())), cipherTypeNone, "dup of none");
const CipherSpec *const cipherSpec = cipherSpecDup(cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF(TEST_PASS)));
const CipherSpec *const cipherSpec = cipherSpecDup(cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_PASS)));
TEST_RESULT_UINT(cipherSpecType(cipherSpec), cipherTypeAes256Cbc, "dup type");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(cipherSpec)), TEST_PASS, "dup pass");
TEST_RESULT_UINT(cipherSpecDigest(cipherSpec), hashTypeSha256, "dup default digest");
TEST_RESULT_UINT(
cipherSpecDigest(cipherSpecDup(cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_PASS), .digest = hashTypeSha1))),
hashTypeSha1, "dup digest");
// A pack contains nothing but the type when there is no cipher
PackWrite *packWrite = pckWriteNewP();
@@ -97,15 +107,17 @@ testRun(void)
TEST_RESULT_UINT(
cipherSpecType(cipherSpecNewPack(pckReadNew(pckWriteResult(packWrite)))), cipherTypeNone, "unpack none");
// Else it contains the type and pass
// Else it contains the type, digest, and pass. Pack a digest that is not the default so that a pack which loses the digest
// cannot pass by falling back to the default.
packWrite = pckWriteNewP();
cipherSpecPack(packWrite, cipherSpecNew(cipherTypeAes256Cbc, testPass));
cipherSpecPack(packWrite, cipherSpecNewP(cipherTypeAes256Cbc, testPass, .digest = hashTypeSha1));
pckWriteEndP(packWrite);
const CipherSpec *const cipherSpecUnpack = cipherSpecNewPack(pckReadNew(pckWriteResult(packWrite)));
TEST_RESULT_UINT(cipherSpecType(cipherSpecUnpack), cipherTypeAes256Cbc, "unpack type");
TEST_RESULT_UINT(cipherSpecDigest(cipherSpecUnpack), hashTypeSha1, "unpack digest");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(cipherSpecUnpack)), TEST_PASS, "unpack pass");
CipherBlock *cipherBlock = (CipherBlock *)ioFilterDriver(cipherBlockNewP(cipherModeEncrypt, cipherSpec));
@@ -123,7 +135,7 @@ testRun(void)
// -------------------------------------------------------------------------------------------------------------------------
Buffer *encryptBuffer = bufNew(TEST_BUFFER_SIZE);
IoFilter *blockEncryptFilter = cipherBlockNewP(cipherModeEncrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
IoFilter *blockEncryptFilter = cipherBlockNewP(cipherModeEncrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockEncryptFilter = cipherBlockNewPack(ioFilterParamList(blockEncryptFilter));
CipherBlock *blockEncrypt = (CipherBlock *)ioFilterDriver(blockEncryptFilter);
@@ -143,7 +155,7 @@ testRun(void)
TEST_RESULT_BOOL(blockEncrypt->saltDone, true, "salt done is true");
TEST_RESULT_BOOL(blockEncrypt->processDone, true, "process done is true");
TEST_RESULT_UINT(blockEncrypt->headerSize, 0, "header size is 0");
TEST_RESULT_UINT(bufUsed(encryptBuffer), CIPHER_BLOCK_HEADER_SIZE, "cipher size is header len");
TEST_RESULT_UINT(bufUsed(encryptBuffer), TEST_HEADER_SIZE, "cipher size is header len");
TEST_RESULT_UINT(
cipherBlockProcessSize(blockEncrypt, strlen(TEST_PLAINTEXT)),
@@ -158,13 +170,13 @@ testRun(void)
bufLimitClear(encryptBuffer);
TEST_RESULT_UINT(
bufUsed(encryptBuffer), CIPHER_BLOCK_HEADER_SIZE + (size_t)EVP_CIPHER_block_size(blockEncrypt->cipher),
bufUsed(encryptBuffer), TEST_HEADER_SIZE + (size_t)EVP_CIPHER_block_size(blockEncrypt->cipher),
"cipher size increases by one block");
TEST_RESULT_BOOL(ioFilterDone(blockEncryptFilter), false, "filter is not done");
ioFilterProcessInOut(blockEncryptFilter, NULL, encryptBuffer);
TEST_RESULT_UINT(
bufUsed(encryptBuffer), CIPHER_BLOCK_HEADER_SIZE + (size_t)(EVP_CIPHER_block_size(blockEncrypt->cipher) * 2),
bufUsed(encryptBuffer), TEST_HEADER_SIZE + (size_t)(EVP_CIPHER_block_size(blockEncrypt->cipher) * 2),
"cipher size increases by one block on flush");
TEST_RESULT_BOOL(ioFilterDone(blockEncryptFilter), true, "filter is done");
@@ -174,7 +186,7 @@ testRun(void)
// -------------------------------------------------------------------------------------------------------------------------
Buffer *decryptBuffer = bufNew(TEST_BUFFER_SIZE);
IoFilter *blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
IoFilter *blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewPack(ioFilterParamList(blockDecryptFilter));
CipherBlock *blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
@@ -194,7 +206,7 @@ testRun(void)
// Decrypt in small chunks to test buffering
// -------------------------------------------------------------------------------------------------------------------------
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
bufUsedZero(decryptBuffer);
@@ -221,7 +233,7 @@ testRun(void)
ioFilterProcessInOut(
blockDecryptFilter,
bufNewC(bufPtr(encryptBuffer) + CIPHER_BLOCK_HEADER_SIZE, bufUsed(encryptBuffer) - CIPHER_BLOCK_HEADER_SIZE),
bufNewC(bufPtr(encryptBuffer) + TEST_HEADER_SIZE, bufUsed(encryptBuffer) - TEST_HEADER_SIZE),
decryptBuffer);
TEST_RESULT_UINT_INT(bufUsed(decryptBuffer), EVP_CIPHER_block_size(blockDecrypt->cipher), "decrypt size is one block");
@@ -235,7 +247,8 @@ testRun(void)
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("encrypt zero byte file with no magic");
blockEncryptFilter = cipherBlockNewP(cipherModeEncrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass), .raw = true);
blockEncryptFilter = cipherBlockNewP(
cipherModeEncrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass), .header = cipherBlockHeaderNone);
blockEncrypt = (CipherBlock *)ioFilterDriver(blockEncryptFilter);
bufUsedZero(encryptBuffer);
@@ -248,13 +261,14 @@ testRun(void)
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("error on decrypt expecting magic");
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
TEST_ERROR(ioFilterProcessInOut(blockDecryptFilter, encryptBuffer, decryptBuffer), CryptoError, "cipher header invalid");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("decrypt zero byte file with no magic");
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass), .raw = true);
blockDecryptFilter = cipherBlockNewP(
cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass), .header = cipherBlockHeaderNone);
blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
bufUsedZero(decryptBuffer);
@@ -268,7 +282,7 @@ testRun(void)
// Invalid cipher header
// -------------------------------------------------------------------------------------------------------------------------
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
TEST_ERROR(
@@ -279,7 +293,7 @@ testRun(void)
// Invalid encrypted data cannot be flushed
// -------------------------------------------------------------------------------------------------------------------------
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
bufUsedZero(decryptBuffer);
@@ -293,7 +307,7 @@ testRun(void)
// File with no header should not flush
// -------------------------------------------------------------------------------------------------------------------------
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
bufUsedZero(decryptBuffer);
@@ -304,7 +318,7 @@ testRun(void)
// File with header only should error
// -------------------------------------------------------------------------------------------------------------------------
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, testPass));
blockDecryptFilter = cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, testPass));
blockDecrypt = (CipherBlock *)ioFilterDriver(blockDecryptFilter);
bufUsedZero(decryptBuffer);
@@ -325,7 +339,7 @@ testRun(void)
TEST_RESULT_VOID(
cipherBlockFilterGroupAdd(
filterGroup, cipherModeEncrypt, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("X"))), " filter add");
filterGroup, cipherModeEncrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("X"))), " filter add");
TEST_RESULT_UINT(ioFilterGroupSize(filterGroup), 1, " check filter add");
}
@@ -334,7 +348,7 @@ testRun(void)
{
IoFilter *hash = NULL;
TEST_ERROR(cryptoHashNew(STRID5("bogus", 0x13a9de20)), AssertError, "unable to load hash 'bogus'");
TEST_ERROR(cryptoHashNew(STRID5("bogus", 0x13a9de20)), AssertError, "unable to load digest 'bogus'");
// -------------------------------------------------------------------------------------------------------------------------
TEST_ASSIGN(hash, cryptoHashNew(hashTypeSha1), "create sha1 hash");

View File

@@ -0,0 +1,272 @@
/***********************************************************************************************************************************
Test Repository Format
***********************************************************************************************************************************/
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "common/io/io.h"
#include "version.h"
/***********************************************************************************************************************************
Data for testing
***********************************************************************************************************************************/
#define TEST_PASS "areallybadpassphrase"
#define TEST_PLAINTEXT "plaintext"
#define TEST_BUFFER_SIZE 256
/***********************************************************************************************************************************
Test Run
***********************************************************************************************************************************/
static void
testRun(void)
{
FUNCTION_HARNESS_VOID();
// *****************************************************************************************************************************
if (testBegin("repoFormatValidate()"))
{
TEST_ERROR(
repoFormatValidate(REPOSITORY_FORMAT_MIN - 1), FormatError,
"repository format 4 is no longer supported by " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format 5 to 6.");
TEST_ERROR(
repoFormatValidate(REPOSITORY_FORMAT_MAX + 1), FormatError,
"repository format 7 requires a newer version of " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format 5 to 6.");
TEST_RESULT_VOID(repoFormatValidate(REPOSITORY_FORMAT_5), "format 5 is readable");
TEST_RESULT_VOID(repoFormatValidate(REPOSITORY_FORMAT_6), "format 6 is readable");
}
// *****************************************************************************************************************************
if (testBegin("repoFormatDigest()"))
{
TEST_RESULT_UINT(repoFormatDigest(REPOSITORY_FORMAT_5), hashTypeSha1, "format 5 derives with sha1");
TEST_RESULT_UINT(repoFormatDigest(REPOSITORY_FORMAT_6), hashTypeSha256, "format 6 derives with sha256");
}
// *****************************************************************************************************************************
if (testBegin("cipherBlockFormatNew()"))
{
const Buffer *const testPlainText = BUFSTRDEF(TEST_PLAINTEXT);
const Buffer *const testPass = BUFSTRDEF(TEST_PASS);
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, testPass);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("nothing is added when the repository is not encrypted");
Buffer *plain = bufNew(0);
IoWrite *plainWrite = ioBufferWriteNew(plain);
TEST_RESULT_VOID(
cipherBlockFormatFilterGroupWriteAdd(plain, ioWriteFilterGroup(plainWrite), cipherSpecNewNone(), REPOSITORY_FORMAT_6),
"add write filter for no cipher");
ioWriteOpen(plainWrite);
ioWrite(plainWrite, testPlainText);
ioWriteClose(plainWrite);
TEST_RESULT_STR_Z(strNewBuf(plain), TEST_PLAINTEXT, "content is stored as it is");
IoWrite *plainRead = ioBufferWriteNew(bufNew(0));
TEST_RESULT_PTR_NE(
cipherBlockFormatFilterGroupReadAdd(ioWriteFilterGroup(plainRead), cipherSpecNewNone()), NULL,
"add read filter for no cipher");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("write a header and read the format back from it");
Buffer *headerBuffer = bufNew(0);
IoWrite *headerWrite = ioBufferWriteNew(headerBuffer);
cipherBlockFormatFilterGroupWriteAdd(headerBuffer, ioWriteFilterGroup(headerWrite), cipherSpec, REPOSITORY_FORMAT_6);
ioWriteOpen(headerWrite);
ioWrite(headerWrite, testPlainText);
ioWriteClose(headerWrite);
TEST_RESULT_BOOL(
memcmp(bufPtrConst(headerBuffer), CIPHER_BLOCK_FORMAT_MAGIC "006_", CIPHER_BLOCK_FORMAT_HEADER_SIZE) == 0, true,
"header names the format");
// The format is not given on decrypt, so it comes from the header and is what the pass derives with
Buffer *headerResult = bufNew(0);
IoWrite *headerRead = ioBufferWriteNew(headerResult);
IoFilterGroup *headerFilterGroup = ioWriteFilterGroup(headerRead);
cipherBlockFormatFilterGroupReadAdd(headerFilterGroup, cipherSpec);
ioWriteOpen(headerRead);
ioWrite(headerRead, headerBuffer);
ioWriteClose(headerRead);
TEST_RESULT_STR_Z(strNewBuf(headerResult), TEST_PLAINTEXT, "content decrypted with the digest the header called for");
TEST_RESULT_UINT(
cipherBlockFormatResult(ioFilterGroupResultP(headerFilterGroup, CIPHER_BLOCK_FORMAT_FILTER_TYPE)),
REPOSITORY_FORMAT_6, "filter reports the format");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("content is handed on in pieces when the destination cannot take it whole");
// Content long enough that decryption has output to hand on while input is still arriving, so the destination fills before
// the source has been consumed
Buffer *const piecesPlainText = bufNew(TEST_BUFFER_SIZE);
memset(bufPtr(piecesPlainText), 'x', bufSize(piecesPlainText));
bufUsedSet(piecesPlainText, bufSize(piecesPlainText));
Buffer *const piecesBuffer = bufNew(0);
IoWrite *const piecesWrite = ioBufferWriteNew(piecesBuffer);
cipherBlockFormatFilterGroupWriteAdd(piecesBuffer, ioWriteFilterGroup(piecesWrite), cipherSpec, REPOSITORY_FORMAT_6);
ioWriteOpen(piecesWrite);
ioWrite(piecesWrite, piecesPlainText);
ioWriteClose(piecesWrite);
IoRead *const readPieces = ioBufferReadNew(piecesBuffer);
Buffer *const piecesResult = bufNew(0);
Buffer *const piece = bufNew(4);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(readPieces), cipherSpec);
ioBufferSizeSet(4);
ioReadOpen(readPieces);
while (!ioReadEof(readPieces))
{
bufUsedZero(piece);
ioRead(readPieces, piece);
bufCat(piecesResult, piece);
}
ioReadClose(readPieces);
ioBufferSizeSet(TEST_BUFFER_SIZE);
TEST_RESULT_BOOL(bufEq(piecesResult, piecesPlainText), true, "content decrypted in pieces");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("the header is read no matter how the input is split");
Buffer *const splitResult = bufNew(0);
IoWrite *const splitWrite = ioBufferWriteNew(splitResult);
cipherBlockFormatFilterGroupReadAdd(ioWriteFilterGroup(splitWrite), cipherSpec);
// Write in pieces smaller than the header so it arrives in more than one part, then a piece that completes it exactly, so
// that what follows arrives with the header already read
ioBufferSizeSet(4);
ioWriteOpen(splitWrite);
ioWrite(splitWrite, BUF(bufPtrConst(headerBuffer), 4));
ioWrite(splitWrite, BUF(bufPtrConst(headerBuffer) + 4, 4));
ioWrite(splitWrite, BUF(bufPtrConst(headerBuffer) + 8, bufUsed(headerBuffer) - 8));
ioWriteClose(splitWrite);
ioBufferSizeSet(TEST_BUFFER_SIZE);
TEST_RESULT_STR_Z(strNewBuf(splitResult), TEST_PLAINTEXT, "content decrypted from split input");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("a file that begins with the magic was written before there was a header");
Buffer *magicBuffer = bufNew(0);
IoWrite *magicWrite = ioBufferWriteNew(magicBuffer);
cipherBlockFormatFilterGroupWriteAdd(magicBuffer, ioWriteFilterGroup(magicWrite), cipherSpec, REPOSITORY_FORMAT_5);
ioWriteOpen(magicWrite);
ioWrite(magicWrite, testPlainText);
ioWriteClose(magicWrite);
TEST_RESULT_BOOL(
memcmp(bufPtrConst(magicBuffer), CIPHER_BLOCK_MAGIC, CIPHER_BLOCK_MAGIC_SIZE) == 0, true,
"a format before the header writes the magic");
headerResult = bufNew(0);
headerRead = ioBufferWriteNew(headerResult);
headerFilterGroup = ioWriteFilterGroup(headerRead);
cipherBlockFormatFilterGroupReadAdd(headerFilterGroup, cipherSpec);
ioWriteOpen(headerRead);
ioWrite(headerRead, magicBuffer);
ioWriteClose(headerRead);
TEST_RESULT_STR_Z(strNewBuf(headerResult), TEST_PLAINTEXT, "content decrypted");
TEST_RESULT_UINT(
cipherBlockFormatResult(ioFilterGroupResultP(headerFilterGroup, CIPHER_BLOCK_FORMAT_FILTER_TYPE)),
REPOSITORY_FORMAT_5, "filter reports the format before the header");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("a format given on decrypt must be the one the header names");
IoWrite *const headerMismatch = ioBufferWriteNew(bufNew(0));
ioFilterGroupAdd(
ioWriteFilterGroup(headerMismatch), cipherBlockFormatNewP(cipherSpec, .format = REPOSITORY_FORMAT_5));
ioWriteOpen(headerMismatch);
TEST_ERROR(ioWrite(headerMismatch, headerBuffer), FormatError, "expected repository format 5 but found 6");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("a format given on decrypt that the header agrees with is accepted, here from a pack");
headerResult = bufNew(0);
headerRead = ioBufferWriteNew(headerResult);
ioFilterGroupAdd(
ioWriteFilterGroup(headerRead),
cipherBlockFormatNewPack(
ioFilterParamList(cipherBlockFormatNewP(cipherSpec, .format = REPOSITORY_FORMAT_6))));
ioWriteOpen(headerRead);
ioWrite(headerRead, headerBuffer);
ioWriteClose(headerRead);
TEST_RESULT_STR_Z(strNewBuf(headerResult), TEST_PLAINTEXT, "content decrypted");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("a file too short to hold a header cannot have one");
IoWrite *const headerShort = ioBufferWriteNew(bufNew(0));
cipherBlockFormatFilterGroupReadAdd(ioWriteFilterGroup(headerShort), cipherSpec);
ioWriteOpen(headerShort);
ioWrite(headerShort, BUFSTRDEF("PGBR"));
TEST_ERROR(ioWriteClose(headerShort), CryptoError, "cipher header missing");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("damaged headers");
// Decrypt the buffer above after damaging a byte of the header, which is the same buffer for each case
#define TEST_HEADER_DAMAGE(damageIdx, damageChar, errorType, errorMessage) \
do \
{ \
Buffer *const damaged = bufDup(headerBuffer); \
bufPtr(damaged)[damageIdx] = damageChar; \
\
IoWrite *const write = ioBufferWriteNew(bufNew(0)); \
cipherBlockFormatFilterGroupReadAdd(ioWriteFilterGroup(write), cipherSpec); \
ioWriteOpen(write); \
\
TEST_ERROR(ioWrite(write, damaged), errorType, errorMessage); \
} \
while (0)
// Where the format should be, so this version cannot tell what it is looking at
TEST_HEADER_DAMAGE(CIPHER_BLOCK_FORMAT_MAGIC_SIZE, 'X', FormatError, "invalid cipher header");
// The byte held back for later, which must be the one this version writes
TEST_HEADER_DAMAGE(CIPHER_BLOCK_FORMAT_HEADER_SIZE - 1, 'X', FormatError, "invalid cipher header");
// A format newer than this version can read, reported before anything is decrypted
TEST_HEADER_DAMAGE(
CIPHER_BLOCK_FORMAT_HEADER_SIZE - 2, '7', FormatError,
"repository format 7 requires a newer version of " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format 5 to 6.");
// A format older than this version can read
TEST_HEADER_DAMAGE(
CIPHER_BLOCK_FORMAT_HEADER_SIZE - 2, '4', FormatError,
"repository format 4 is no longer supported by " PROJECT_NAME "\n"
"HINT: " PROJECT_NAME " " PROJECT_VERSION " supports repository format 5 to 6.");
// Neither a header nor the magic, which is what a file that was never encrypted looks like from here
TEST_HEADER_DAMAGE(0, 'X', CryptoError, "cipher header invalid");
#undef TEST_HEADER_DAMAGE
}
FUNCTION_HARNESS_RETURN_VOID();
}

View File

@@ -87,16 +87,26 @@ testRun(void)
info,
infoArchiveNew(
PG_VERSION_10, 6569239123849665999, REPOSITORY_FORMAT_DEFAULT,
cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("zWa/6Xtp-IVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO"))),
cipherSpecNewP(
cipherTypeAes256Cbc, BUFSTRDEF("zWa/6Xtp-IVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO"),
.digest = hashTypeSha1)),
"infoArchiveNew() - cipher sub");
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"));
contentSave = bufNew(0);
TEST_RESULT_VOID(infoArchiveSave(info, ioBufferWriteNew(contentSave)), "save new with cipher");
IoWrite *write = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(write), cipherSpec, REPOSITORY_FORMAT_DEFAULT);
TEST_ASSIGN(
info, infoArchiveNewLoad(ioBufferReadNew(contentSave), cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x"))),
"load encrypted archive info");
TEST_RESULT_VOID(infoArchiveSave(info, write), "save new with cipher");
TEST_RESULT_BOOL(
strBeginsWithZ(strNewBuf(contentSave), "PGBR"), false, "no header before the format that added it");
IoRead *const read = ioBufferReadNew(contentSave);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(read), cipherSpec);
TEST_ASSIGN(info, infoArchiveNewLoad(read, cipherSpec), "load encrypted archive info");
TEST_RESULT_STR_Z(infoArchiveId(info), "10-1", "archiveId set");
TEST_RESULT_PTR(infoArchivePg(info), infoArchivePg(info), "infoPg set");
TEST_RESULT_STR_Z(

View File

@@ -87,18 +87,27 @@ testRun(void)
TEST_ASSIGN(
infoBackup,
infoBackupNew(
PG_VERSION_10, 6569239123849665999, hrnPgCatalogVersion(PG_VERSION_10), REPOSITORY_FORMAT_DEFAULT,
cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("zWa/6Xtp-IVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO"))),
PG_VERSION_10, 6569239123849665999, hrnPgCatalogVersion(PG_VERSION_10), REPOSITORY_FORMAT_6,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("zWa/6Xtp-IVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO"))),
"infoBackupNew() - cipher sub");
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"));
contentSave = bufNew(0);
TEST_RESULT_VOID(infoBackupSave(infoBackup, ioBufferWriteNew(contentSave)), "save new with cipher sub");
IoWrite *write = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(write), cipherSpec, REPOSITORY_FORMAT_6);
TEST_RESULT_VOID(infoBackupSave(infoBackup, write), "save new with cipher sub");
TEST_RESULT_STR_Z(
strNewZN((const char *)bufPtrConst(contentSave), 8), "PGBR006_", "header names the format");
infoBackup = NULL;
TEST_ASSIGN(
infoBackup, infoBackupNewLoad(ioBufferReadNew(contentSave), cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x"))),
"load backup info with cipher sub");
IoRead *const read = ioBufferReadNew(contentSave);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(read), cipherSpec);
TEST_ASSIGN(infoBackup, infoBackupNewLoad(read, cipherSpec), "load backup info with cipher sub");
TEST_RESULT_PTR(infoBackupPg(infoBackup), infoBackupPg(infoBackup), "infoPg set");
TEST_RESULT_STR_Z(
strNewBuf(cipherSpecPass(infoBackupCipherSpec(infoBackup))),

View File

@@ -43,7 +43,7 @@ testRun(void)
TEST_ASSIGN(
infoPg,
infoPgNew(infoPgArchive, REPOSITORY_FORMAT_DEFAULT, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
infoPgNew(infoPgArchive, REPOSITORY_FORMAT_DEFAULT, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
"infoPgNew(cipherTypeAes256Cbc, 123xyz)");
TEST_RESULT_INT(infoPgDataTotal(infoPg), 0, " 0 history");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(infoPgInfo(infoPg)))), "123xyz", " cipherPass set");
@@ -80,7 +80,7 @@ testRun(void)
TEST_ASSIGN(
infoPg,
infoPgSet(
infoPgNew(infoPgBackup, REPOSITORY_FORMAT_DEFAULT, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
infoPgNew(infoPgBackup, REPOSITORY_FORMAT_DEFAULT, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
PG_VERSION_18, 6569239123849665679, hrnPgCatalogVersion(PG_VERSION_18)),
"infoPgSet - infoPgBackup");
TEST_RESULT_INT(infoPgDataTotal(infoPg), 1, " 1 history");

View File

@@ -2,12 +2,25 @@
Test Info Handler
***********************************************************************************************************************************/
#include "common/crypto/cipherBlock.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "storage/posix/storage.h"
#include "harness/info.h"
/***********************************************************************************************************************************
Read a buffer through the filter that reads the header, as a caller loading a file that has one does
***********************************************************************************************************************************/
static IoRead *
testInfoReadHeader(const Buffer *const buffer, const CipherSpec *const cipherSpec)
{
IoRead *const result = ioBufferReadNew(buffer);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(result), cipherSpec);
return result;
}
/***********************************************************************************************************************************
Test load callback
***********************************************************************************************************************************/
@@ -79,7 +92,7 @@ testRun(void)
Info *info = NULL;
TEST_ASSIGN(
info, infoNew(REPOSITORY_FORMAT_DEFAULT, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
info, infoNew(REPOSITORY_FORMAT_DEFAULT, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
"infoNew(cipher)");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(info))), "123xyz", " cipherPass is set");
@@ -167,7 +180,7 @@ testRun(void)
IoRead *read = ioBufferReadNew(contentLoad);
ioFilterGroupAdd(
ioReadFilterGroup(read),
cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("X"))));
cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("X"))));
TEST_ERROR(
infoNewLoad(read, cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent), CryptoError,
@@ -255,20 +268,163 @@ testRun(void)
callbackContent = strNew();
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"));
// A file with no header, e.g. a manifest, is decrypted with the spec as it was given since nothing defines the digest
// as anything else
IoRead *const readNoHeader = ioBufferReadNew(harnessInfoEncryptP(contentLoad, cipherSpec));
cipherBlockFilterGroupAdd(ioReadFilterGroup(readNoHeader), cipherModeDecrypt, cipherSpec);
TEST_ASSIGN(
info,
infoNewLoad(
ioBufferReadNew(contentLoad), cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x")), harnessInfoLoadNewCallback,
callbackContent),
info, infoNewLoad(readNoHeader, cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info with content and cipher");
TEST_RESULT_STR_Z(callbackContent, "[c] key=1\n[d] key=1\n", " check callback content");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(info))), "somepass", " check cipher pass set");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha1, " check cipher sub digest");
TEST_RESULT_STR_Z(infoBackrestVersion(info), PROJECT_VERSION, " check backrest version");
contentSave = bufNew(0);
TEST_RESULT_VOID(infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "info save");
TEST_RESULT_STR(strNewBuf(contentSave), strNewBuf(contentLoad), " check save");
// Migrating to a format that stores the digest does not change the digest the pass derives with, since the files the pass
// encrypted before the migration are not rewritten
contentSave = bufNew(0);
TEST_RESULT_VOID(infoFormatSet(info, REPOSITORY_FORMAT_6), "migrate to format 6");
TEST_RESULT_VOID(
infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "save migrated info");
TEST_RESULT_BOOL(
strstr(strZ(strNewBuf(contentSave)), "cipher-digest=\"sha1\"") != NULL, true, " check digest stored with pass");
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentSave, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, strNew()),
"load migrated info");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_6, " check format");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha1, " check cipher sub digest unchanged");
// Header
// -------------------------------------------------------------------------------------------------------------------------
// An unencrypted file has no header no matter the format, since the format is read from the content
contentSave = bufNew(0);
IoWrite *const writeNone = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(writeNone), cipherSpecNewNone(), REPOSITORY_FORMAT_6);
TEST_RESULT_VOID(infoSave(info, writeNone, testInfoSaveCallback, strNewZ("1")), "info save");
TEST_RESULT_BOOL(strBeginsWithZ(strNewBuf(contentSave), "PGBR"), false, " check no header");
contentLoad = harnessInfoChecksumFormat(
REPOSITORY_FORMAT_6,
STRDEF(
"[cipher]\n"
"cipher-digest=\"sha256\"\n"
"cipher-pass=\"somepass\"\n"));
callbackContent = strNew();
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info with header");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_6, " check format");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha256, " check cipher sub digest");
// A pass migrated from a format that could not store the digest keeps deriving with SHA-1, so the files it encrypted
// before the migration are still readable
const Buffer *const contentMigrated = harnessInfoChecksumFormat(
REPOSITORY_FORMAT_6,
STRDEF(
"[cipher]\n"
"cipher-digest=\"sha1\"\n"
"cipher-pass=\"somepass\"\n"));
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentMigrated, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info migrated to the format that stores the digest");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha1, " check cipher sub digest");
// The content on its own, which is how a caller that wants the file rather than the values in it reads an info file
IoRead *const infoRead = ioBufferReadNew(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6));
ioFilterGroupAdd(
ioReadFilterGroup(infoRead), cipherBlockFormatNewP(cipherSpec));
ioReadOpen(infoRead);
TEST_RESULT_STR(strNewBuf(ioReadBuf(infoRead)), strNewBuf(contentLoad), "info content read");
// A file written before the header existed is read as the format that had none
contentLoad = harnessInfoChecksumZ("[c]\nkey=1\n");
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_5), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info with no header");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_5, " check format");
// A file too short to hold a header cannot have one
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(BUFSTRDEF("PGBR"), cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
CryptoError,
"cipher header missing\n"
"HINT: is or was the repo encrypted?");
// A file with neither header was never encrypted, which is reported the way the cipher reports it
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(BUFSTRDEF("[backrest]\nbackrest-format=5\n"), cipherSpec), cipherSpec,
harnessInfoLoadNewCallback, callbackContent),
CryptoError,
"cipher header invalid\n"
"HINT: is or was the repo encrypted?");
// Header uses the byte held back for later, so this version does not know what it is looking at
Buffer *contentHeader = harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6);
bufPtr(contentHeader)[7] = 'X';
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(contentHeader, cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError, "invalid cipher header");
// Header damaged where the format should be
contentHeader = harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6);
bufPtr(contentHeader)[5] = 'X';
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(contentHeader, cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError, "invalid cipher header");
// Header names a format this version cannot read, which is reported before anything is decrypted
contentHeader = harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6);
bufPtr(contentHeader)[6] = '7';
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(contentHeader, cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError,
"repository format 7 requires a newer version of pgBackRest\n"
"HINT: pgBackRest " PROJECT_VERSION " supports repository format 5 to 6.");
// Header and content disagree about the format
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError, "repository format 5 does not match header format 6");
}
// *****************************************************************************************************************************

View File

@@ -1453,6 +1453,7 @@ testRun(void)
"pg_data={\"path\":\"/pg/base\",\"type\":\"path\"}\n"
"\n"
"[cipher]\n"
"cipher-digest=\"sha256\"\n"
"cipher-pass=\"somepass\"\n"
"\n"
"[target:file]\n"
@@ -1477,9 +1478,11 @@ testRun(void)
MEM_CONTEXT_TEMP_BEGIN()
{
TEST_ASSIGN(
manifest, manifestNewLoad(ioBufferReadNew(contentLoad), cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x"))),
"load manifest");
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"));
IoRead *const read = ioBufferReadNew(harnessInfoEncryptP(contentLoad, cipherSpec));
cipherBlockFilterGroupAdd(ioReadFilterGroup(read), cipherModeDecrypt, cipherSpec);
TEST_ASSIGN(manifest, manifestNewLoad(read, cipherSpec), "load manifest");
TEST_RESULT_VOID(manifestMove(manifest, memContextPrior()), "move manifest");
}
MEM_CONTEXT_TEMP_END();
@@ -1781,7 +1784,8 @@ testRun(void)
TEST_RESULT_UINT(cipherSpecType(manifestCipherSpec(manifest)), cipherTypeNone, "check cipher subpass");
TEST_RESULT_VOID(
manifestCipherSpecSet(manifest, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("supersecret"))),
manifestCipherSpecSet(
manifest, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("supersecret"), .digest = hashTypeSha1)),
"cipher subpass set");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(manifestCipherSpec(manifest))), "supersecret", "check cipher subpass");

View File

@@ -381,8 +381,8 @@ testRun(void)
IoFilterGroup *filterGroup = ioReadFilterGroup(storageReadIo(fileRead));
ioFilterGroupAdd(filterGroup, ioSizeNew());
ioFilterGroupAdd(filterGroup, cryptoHashNew(hashTypeSha1));
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeEncrypt, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x"))));
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x"))));
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeEncrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"))));
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"))));
ioFilterGroupAdd(filterGroup, compressFilterP(compressTypeGz, 3));
ioFilterGroupAdd(filterGroup, decompressFilterP(compressTypeGz));
@@ -422,7 +422,7 @@ testRun(void)
// Write an encrypted file larger than the buffer size so the error will happen after the first chunk has been read
HRN_STORAGE_PUT(
storageTest, TEST_PATH "/repo128/test.bin", contentBuf,
.cipherSpec = cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x")));
.cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x")));
// Enable read retry to show that it is disabled when the filters are passed to the remote
uint64_t featureOld = storageRepo->pub.interface.feature;
@@ -435,7 +435,7 @@ testRun(void)
ioFilterGroupAdd(
ioReadFilterGroup(storageReadIo(fileRead)),
cipherBlockNewP(cipherModeDecrypt, cipherSpecNew(cipherTypeAes256Cbc, BUFSTRDEF("x"))));
cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"))));
TEST_ERROR(storageGetP(fileRead), CryptoError, "raised from remote-0 shim protocol: unable to flush");