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

Add archive key rotation for repository format 6.

At format 6 the repository can hold more than one encryption key. This is used to rotate the archive key, which is currently the only bulk data key that never changes.

Keys are stored by id in the new CipherSpecMap, with one key marked current for encrypting new files. At format 6 the cipher section of an info file stores every key by id along with the digest it derives with, e.g. cipher-pass={"1":{"digest":"sha256","key":"..."}}, and archive.info also records the current key id in cipher-pass-current. Format 5 files keep their single key, which is loaded as the key with no id.

archive-push writes the id of the archive key it used into the header of each WAL file. The readers use that id to select the key: archive-get, verify, the restore timeline read, the backup archive copy, and repo-get. A pusher that loaded archive.info before a rotation keeps working since each file records the key it was encrypted with. The filter that writes the header now also works through a repository host.

stanza-create for format 6 creates the first archive key with id 1. stanza-upgrade from format 5 keeps the existing archive key as id 0 so WAL already archived stays readable, and adds a key at id 1 for new WAL.

The new repo-cipher-rotate option sets how old the current archive key may be before expire replaces it, with a default of 90 days and a minimum of 15. Expire adds the new key, makes it current, and records the time of the rotation in archive.info as cipher-pass-rotate. At format 5 the option has no effect and warns only when set explicitly.

Expire now saves archive.info on every run. Backup did this only to refresh the timestamps for object store lifecycle rules and no longer writes archive.info, so with expire-auto disabled the timestamps are refreshed only when expire runs.
This commit is contained in:
David Steele
2026-10-02 08:37:45 +02:00
committed by GitHub
parent 3b9f27c086
commit 6e28c0445f
54 changed files with 2041 additions and 284 deletions

View File

@@ -1743,6 +1743,22 @@ option:
deprecate:
repo-cipher-pass: {}
repo-cipher-rotate:
section: global
group: repo
type: time
default: 90d
allow-range: [15d, 9999999d]
command:
backup: {}
expire: {}
command-role:
main: {}
depend:
option: repo-cipher-type
list:
- aes-256-cbc
repo-hardlink:
section: global
group: repo

View File

@@ -1456,6 +1456,18 @@
<example>zWaf6XtpjIVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO</example>
</config-key>
<config-key id="repo-cipher-rotate" name="Repository Cipher Rotation">
<summary>Archive key rotation period.</summary>
<text>
<p>When the key that encrypts archived WAL is older than this period, <cmd>expire</cmd> generates a new key and WAL archived after that is encrypted with it. WAL encrypted with an earlier key remains readable. If rotation is not desired then set the option to the max value.</p>
<p>Archive key rotation requires <br-option>repo-format</br-option> 6 or higher.</p>
</text>
<example>30d</example>
</config-key>
</config-key-list>
</config-section>

View File

@@ -126,13 +126,16 @@
<commit subject="Remove the default digest from CipherSpec.">
<github-pull-request id="2862"/>
</commit>
<commit subject="Add archive key rotation for repository format 6.">
<github-pull-request id="2864"/>
</commit>
<release-item-contributor-list>
<release-item-contributor id="david.steele"/>
<release-item-reviewer id="douglas.j.hunley"/>
</release-item-contributor-list>
<p>Pass a cipher spec rather than a type and passphrase.</p>
<p>Add archive key rotation for repository format 6.</p>
</release-item>
<release-item>

View File

@@ -7,8 +7,8 @@ Archive Get File
#include "command/archive/get/file.h"
#include "command/control/common.h"
#include "common/compress/helper.h"
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/filter/group.h"
#include "common/log.h"
#include "config/config.h"
@@ -53,12 +53,11 @@ archiveGetFile(
StorageWrite *const destination = storageNewWriteP(
storage, walDestination, .noCreatePath = true, .noSyncFile = true, .noSyncPath = true, .noAtomic = true);
// If there is a cipher then add the decrypt filter
if (cipherSpecType(actual->cipherSpecArchive) != cipherTypeNone)
// Add decrypt filter to read the format header
if (cipherSpecMapSize(actual->cipherSpecArchive) != 0)
{
ioFilterGroupAdd(
ioWriteFilterGroup(storageWriteIo(destination)),
cipherBlockNewP(cipherModeDecrypt, actual->cipherSpecArchive));
cipherBlockFormatFilterGroupReadAddMap(
ioWriteFilterGroup(storageWriteIo(destination)), actual->cipherSpecArchive);
compressible = false;
}

View File

@@ -4,7 +4,7 @@ Archive Get File
#ifndef COMMAND_ARCHIVE_GET_FILE_H
#define COMMAND_ARCHIVE_GET_FILE_H
#include "common/crypto/spec.h"
#include "common/format/cipherSpecMap.h"
#include "common/type/string.h"
#include "storage/storage.h"
@@ -17,7 +17,7 @@ typedef struct ArchiveGetFile
const String *file; // File in the repo (with path, checksum, ext, etc.)
unsigned int repoIdx; // Repo idx
const String *archiveId; // Repo archive id
const CipherSpec *cipherSpecArchive; // Repo archive cipher spec
const CipherSpecMap *cipherSpecArchive; // Repo archive cipher keys
} ArchiveGetFile;
typedef struct ArchiveGetFileResult

View File

@@ -94,7 +94,7 @@ typedef struct ArchiveGetFindCacheArchive
typedef struct ArchiveGetFindCacheRepo
{
unsigned int repoIdx;
const CipherSpec *cipherSpecArchive; // Repo archive cipher spec
const CipherSpecMap *cipherSpecArchive; // Repo archive cipher keys
List *archiveList; // Cached list of archiveIds and associated paths
StringList *warnList; // Track repo warnings so each is only reported once
} ArchiveGetFindCacheRepo;
@@ -416,7 +416,7 @@ archiveGetCheck(const StringList *const archiveRequestList)
// Build cipher spec in the result list context once rather than rebuilding it per candidate file later
MEM_CONTEXT_BEGIN(lstMemContext(result.archiveFileMapList))
{
cacheRepo.cipherSpecArchive = cipherSpecDupP(infoArchiveCipherSpec(info));
cacheRepo.cipherSpecArchive = cipherSpecMapDup(infoArchiveCipherSpecMap(info));
}
MEM_CONTEXT_END();
@@ -924,7 +924,7 @@ archiveGetAsyncCallback(void *const data, const unsigned int clientIdx)
pckWriteStrP(param, actual->file);
pckWriteU32P(param, actual->repoIdx);
pckWriteStrP(param, actual->archiveId);
cipherSpecPack(param, actual->cipherSpecArchive);
cipherSpecMapPack(param, actual->cipherSpecArchive);
}
MEM_CONTEXT_PRIOR_BEGIN()

View File

@@ -38,7 +38,7 @@ archiveGetFileProtocol(PackRead *const param)
ArchiveGetFile actual = {.file = pckReadStrP(param)};
actual.repoIdx = pckReadU32P(param);
actual.archiveId = pckReadStrP(param);
actual.cipherSpecArchive = cipherSpecNewPack(param);
actual.cipherSpecArchive = cipherSpecMapNewPack(param);
lstAdd(actualList, &actual);
}

View File

@@ -7,9 +7,9 @@ Archive Push File
#include "command/archive/find.h"
#include "command/archive/push/file.h"
#include "command/control/common.h"
#include "common/crypto/cipherBlock.h"
#include "common/crypto/hash.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/filter/group.h"
#include "common/io/io.h"
#include "common/log.h"
@@ -265,13 +265,10 @@ archivePushFile(
strNewFmt(STORAGE_REPO_ARCHIVE "/%s/%s", strZ(repoData->archiveId), strZ(archiveDestination)),
.compressible = compressible);
// If there is a cipher then add the encrypt filter
if (cipherSpecType(repoData->cipherSpecArchive) != cipherTypeNone)
{
ioFilterGroupAdd(
ioWriteFilterGroup(storageWriteIo(destination[repoListIdx])),
cipherBlockNewP(cipherModeEncrypt, repoData->cipherSpecArchive));
}
// Add the encrypt filter to write the format header when required
cipherBlockFormatFilterGroupWriteAddP(
ioWriteFilterGroup(storageWriteIo(destination[repoListIdx])), repoData->cipherSpecArchive,
repoData->format, .keyId = repoData->cipherIdArchive);
}
}

View File

@@ -17,7 +17,9 @@ typedef struct ArchivePushFileRepoData
{
unsigned int repoIdx;
const String *archiveId;
unsigned int format; // Repo format
const CipherSpec *cipherSpecArchive; // Repo archive cipher spec
const String *cipherIdArchive; // Archive key id, NULL when no id
} ArchivePushFileRepoData;
/***********************************************************************************************************************************

View File

@@ -48,7 +48,9 @@ archivePushFileProtocol(PackRead *const param)
ArchivePushFileRepoData repo = {.repoIdx = pckReadU32P(param)};
repo.archiveId = pckReadStrP(param);
repo.format = pckReadU32P(param);
repo.cipherSpecArchive = cipherSpecNewPack(param);
repo.cipherIdArchive = pckReadStrP(param);
pckReadObjEndP(param);
lstAdd(repoList, &repo);

View File

@@ -255,6 +255,13 @@ archivePushCheck(const bool pgPathSet)
}
}
// Get the current archive key. Format >= 6 also requires the key id.
const CipherSpecMap *const archiveCipherSpecMap = infoArchiveCipherSpecMap(info);
const String *const archiveCipherId = cipherSpecMapIdCurrent(archiveCipherSpecMap);
const CipherSpec *const archiveCipherSpec =
archiveCipherId == NULL ?
infoArchiveCipherSpec(info) : cipherSpecMapGet(archiveCipherSpecMap, archiveCipherId);
MEM_CONTEXT_PRIOR_BEGIN()
{
result.pgVersion = archiveInfo.version;
@@ -264,7 +271,9 @@ archivePushCheck(const bool pgPathSet)
{
.repoIdx = repoIdx,
.archiveId = strDup(archiveId),
.cipherSpecArchive = cipherSpecDupP(infoArchiveCipherSpec(info)),
.format = infoArchiveFormat(info),
.cipherSpecArchive = cipherSpecDupP(archiveCipherSpec),
.cipherIdArchive = strDup(archiveCipherId),
};
lstAdd(result.repoList, &archivePushFileRepoData);
@@ -488,7 +497,9 @@ archivePushAsyncCallback(void *const data, const unsigned int clientIdx)
pckWriteObjBeginP(param);
pckWriteU32P(param, data->repoIdx);
pckWriteStrP(param, data->archiveId);
pckWriteU32P(param, data->format);
cipherSpecPack(param, data->cipherSpecArchive);
pckWriteStrP(param, data->cipherIdArchive);
pckWriteObjEndP(param);
}

View File

@@ -19,6 +19,7 @@ Backup Command
#include "common/compress/helper.h"
#include "common/crypto/cipherBlock.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/filter/size.h"
#include "common/log.h"
#include "common/regExp.h"

View File

@@ -101,8 +101,8 @@ backupArchiveCheckCopy(const BackupData *const backupData, Manifest *const manif
IoFilterGroup *const filterGroup = ioReadFilterGroup(storageReadIo(read));
// Decrypt with archive key if encrypted
cipherBlockFilterGroupAdd(
filterGroup, cipherModeDecrypt, infoArchiveCipherSpec(backupData->archiveInfo));
cipherBlockFormatFilterGroupReadAddMap(
filterGroup, infoArchiveCipherSpecMap(backupData->archiveInfo));
// Compress/decompress if archive and backup do not have the same compression settings
if (archiveCompressType != backupCompressType)
@@ -230,12 +230,6 @@ backupComplete(InfoBackup *const infoBackup, Manifest *const manifest)
// -------------------------------------------------------------------------------------------------------------------------
infoBackupDataAdd(infoBackup, manifest);
infoBackupSaveFile(infoBackup, storageRepoWrite(), INFO_BACKUP_PATH_FILE_STR, cfgCipherSpecMain());
// Save archive.info/copy so the timestamps will be updated to prevent lifecycle settings from removing the files early
// -------------------------------------------------------------------------------------------------------------------------
infoArchiveSaveFile(
infoArchiveLoadFile(storageRepo(), INFO_ARCHIVE_PATH_FILE_STR, cfgCipherSpecMain()), storageRepoWrite(),
INFO_ARCHIVE_PATH_FILE_STR, cfgCipherSpecMain());
}
MEM_CONTEXT_TEMP_END();

View File

@@ -7,7 +7,9 @@ Expire Command
#include "command/backup/common.h"
#include "command/control/common.h"
#include "command/expire/expire.h"
#include "command/stanza/common.h"
#include "common/debug.h"
#include "common/format/format.h"
#include "common/regExp.h"
#include "common/time.h"
#include "common/type/list.h"
@@ -1081,6 +1083,56 @@ removeExpiredHistory(const InfoBackup *const infoBackup, const unsigned int repo
FUNCTION_LOG_RETURN_VOID();
}
/***********************************************************************************************************************************
Rotate the archive key when it is older than the rotation period
***********************************************************************************************************************************/
static void
expireArchiveCipherRotate(InfoArchive *const infoArchive, const unsigned int repoIdx)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(INFO_ARCHIVE, infoArchive);
FUNCTION_LOG_PARAM(UINT, repoIdx);
FUNCTION_LOG_END();
ASSERT(infoArchive != NULL);
if (cfgOptionIdxTest(cfgOptRepoCipherRotate, repoIdx))
{
// Format < 6 has a single archive key that cannot be rotated. Warn only when the option is not the default.
if (infoArchiveFormat(infoArchive) < REPOSITORY_FORMAT_6)
{
if (cfgOptionIdxSource(cfgOptRepoCipherRotate, repoIdx) != cfgSourceDefault)
{
LOG_WARN_FMT(
"option '%s' requires repository format %d or higher", cfgOptionIdxName(cfgOptRepoCipherRotate, repoIdx),
REPOSITORY_FORMAT_6);
}
}
else
{
const time_t timeNow = time(NULL);
if (infoArchiveCipherRotateTime(infoArchive) <
timeNow - (time_t)(cfgOptionIdxUInt64(cfgOptRepoCipherRotate, repoIdx) / MSEC_PER_SEC))
{
MEM_CONTEXT_TEMP_BEGIN()
{
infoArchiveCipherRotate(
infoArchive,
cipherSpecGen(cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx), infoArchiveFormat(infoArchive)), timeNow);
}
MEM_CONTEXT_TEMP_END();
LOG_INFO_FMT(
"%s: rotate archive key to id %s", cfgOptionGroupName(cfgOptGrpRepo, repoIdx),
strZ(cipherSpecMapIdCurrent(infoArchiveCipherSpecMap(infoArchive))));
}
}
}
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN void
cmdExpire(void)
@@ -1240,6 +1292,19 @@ cmdExpire(void)
removeExpiredArchive(infoBackup, timeBasedFullRetention, repoIdx);
removeExpiredHistory(infoBackup, repoIdx);
// Load archive info and rotate the archive key when it is due
InfoArchive *const infoArchive = infoArchiveLoadFile(
storageRepo, INFO_ARCHIVE_PATH_FILE_STR, cfgCipherSpecMainIdx(repoIdx));
expireArchiveCipherRotate(infoArchive, repoIdx);
// Save archive.info/copy to update the timestamps and prevent lifecycle settings from removing the files early
if (!cfgOptionValid(cfgOptDryRun) || !cfgOptionBool(cfgOptDryRun))
{
infoArchiveSaveFile(
infoArchive, storageRepoIdxWrite(repoIdx), INFO_ARCHIVE_PATH_FILE_STR, cfgCipherSpecMainIdx(repoIdx));
}
// Check the oldest retained backup for page checksum errors and issue a warning if any
if (infoBackupDataTotal(infoBackup) > 0)
expireChecksumErrorCheck(infoBackup, infoBackupData(infoBackup, 0).backupLabel, repoIdx, false);

View File

@@ -46,6 +46,7 @@ static const StorageRemoteFilterHandler storageRemoteFilterHandler[] =
{.type = BLOCK_INCR_FILTER_TYPE, .handlerParam = blockIncrNewPack},
{.type = CIPHER_BLOCK_FILTER_TYPE, .handlerParam = cipherBlockNewPack},
{.type = CIPHER_BLOCK_FORMAT_FILTER_TYPE, .handlerParam = cipherBlockFormatNewPack},
{.type = CIPHER_BLOCK_FORMAT_HEADER_FILTER_TYPE, .handlerParam = cipherBlockFormatHeaderNewPack},
{.type = CRYPTO_HASH_FILTER_TYPE, .handlerParam = cryptoHashNewPack},
{.type = PAGE_CHECKSUM_FILTER_TYPE, .handlerParam = pageChecksumNewPack},
{.type = SINK_FILTER_TYPE, .handlerNoParam = ioSinkNew},

View File

@@ -73,6 +73,7 @@ storageGetProcess(IoWrite *const destination)
// should contain only stanza paths.
// -----------------------------------------------------------------------------------------------------------------
const CipherSpec *cipherSpec = NULL;
const CipherSpecMap *cipherSpecMap = NULL;
const StringList *const filePathSplitLst = strLstNewSplit(file, FSLASH_STR);
// At a minimum the path must contain archive/backup, a stanza, and a file
@@ -91,19 +92,20 @@ storageGetProcess(IoWrite *const destination)
// Archive path
if (strEq(strLstGet(filePathSplitLst, 0), STORAGE_PATH_ARCHIVE_STR))
{
cipherSpec = cfgCipherSpecMain();
// Find the archive passphrase
// Find the archive keys. WAL stores the id of the key it is encrypted with in the header.
if (!strEndsWithZ(file, INFO_ARCHIVE_FILE) && !strEndsWithZ(file, INFO_ARCHIVE_FILE INFO_COPY_EXT))
{
const InfoArchive *const info = infoArchiveLoadFile(
storageRepo(), strNewFmt(STORAGE_PATH_ARCHIVE "/%s/%s", strZ(stanza), INFO_ARCHIVE_FILE),
cfgCipherSpecMain());
cipherSpec = infoArchiveCipherSpec(info);
cipherSpecMap = infoArchiveCipherSpecMap(info);
}
// Else the file is the archive info, which the repo passphrase opens
else
{
cipherSpec = cfgCipherSpecMain();
fileIsInfo = true;
}
}
// Backup path
@@ -143,16 +145,20 @@ storageGetProcess(IoWrite *const destination)
}
// Error when unable to determine cipher passphrase
if (cipherSpec == NULL)
if (cipherSpec == NULL && cipherSpecMap == NULL)
THROW_FMT(OptionInvalidValueError, "unable to determine cipher passphrase for '%s'", strZ(file));
ASSERT(cipherSpecType(cipherSpec) != cipherTypeNone);
// Add the decryption filter. An info file is read through the filter that reads its header.
// Add the decryption filter. An info file is read through the filter that reads its header and WAL through the
// filter that selects the key by the id in the header.
if (fileIsInfo)
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(source), cipherSpec);
else if (cipherSpecMap != NULL)
cipherBlockFormatFilterGroupReadAddMap(ioReadFilterGroup(source), cipherSpecMap);
else
{
ASSERT(cipherSpecType(cipherSpec) != cipherTypeNone);
cipherBlockFilterGroupAdd(ioReadFilterGroup(source), cipherModeDecrypt, cipherSpec);
}
}
}

View File

@@ -73,7 +73,7 @@ cmdRestore(void)
strNewFmt("%s-%u", strZ(pgVersionToStr(data->pgVersion)), data->pgId), data->pgVersion,
cvtZToUIntBase(strZ(strSubN(data->archiveStart, 0, 8)), 16), pgLsnFromStr(data->lsnStart),
cfgOptionStrNull(cfgOptTargetTimeline), cfgOptionSeq(cfgOptType),
infoArchiveCipherSpec(archiveInfo));
infoArchiveCipherSpecMap(archiveInfo));
}
// Remotes (if any) are no longer needed since the rest of the repository reads will be done by the local processes

View File

@@ -9,7 +9,7 @@ Timeline Management
#include <unistd.h>
#include "command/restore/timeline.h"
#include "common/crypto/cipherBlock.h"
#include "common/format/cipherBlockFormat.h"
#include "common/log.h"
#include "config/config.h"
#include "postgres/interface.h"
@@ -79,13 +79,13 @@ Load history file
static List *
historyLoad(
const Storage *const storageRepo, const String *const archiveId, const unsigned int timeline,
const CipherSpec *const cipherSpecArchive)
const CipherSpecMap *const cipherSpecArchive)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE, storageRepo);
FUNCTION_LOG_PARAM(STRING, archiveId);
FUNCTION_LOG_PARAM(UINT, timeline);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpecArchive);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecArchive);
FUNCTION_LOG_END();
List *result;
@@ -94,7 +94,7 @@ historyLoad(
{
const String *const historyFile = strNewFmt(STORAGE_REPO_ARCHIVE "/%s/%08X.history", strZ(archiveId), timeline);
StorageRead *const storageRead = storageNewReadP(storageRepo, historyFile);
cipherBlockFilterGroupAdd(ioReadFilterGroup(storageReadIo(storageRead)), cipherModeDecrypt, cipherSpecArchive);
cipherBlockFormatFilterGroupReadAddMap(ioReadFilterGroup(storageReadIo(storageRead)), cipherSpecArchive);
const Buffer *const history = storageGetP(storageRead);
TRY_BEGIN()
@@ -158,7 +158,7 @@ FN_EXTERN void
timelineVerify(
const Storage *const storageRepo, const String *const archiveId, const unsigned int pgVersion,
const unsigned int timelineBackup, const uint64_t lsnBackup, const String *timelineTargetStr, const unsigned int recoveryType,
const CipherSpec *const cipherSpecArchive)
const CipherSpecMap *const cipherSpecArchive)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STORAGE, storageRepo);
@@ -168,7 +168,7 @@ timelineVerify(
FUNCTION_LOG_PARAM(UINT64, lsnBackup);
FUNCTION_LOG_PARAM(STRING, timelineTargetStr);
FUNCTION_LOG_PARAM(UINT, recoveryType);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpecArchive);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecArchive);
FUNCTION_LOG_END();
ASSERT(storageRepo != NULL);

View File

@@ -4,7 +4,7 @@ Timeline Management
#ifndef COMMAND_RESTORE_TIMELINE_H
#define COMMAND_RESTORE_TIMELINE_H
#include "common/crypto/spec.h"
#include "common/format/cipherSpecMap.h"
#include "storage/storage.h"
/***********************************************************************************************************************************
@@ -13,6 +13,6 @@ Functions
// Verify that target timeline is valid for a backup
FN_EXTERN void timelineVerify(
const Storage *storageRepo, const String *archiveId, unsigned int pgVersion, unsigned int timelineBackup, uint64_t lsnBackup,
const String *timelineTargetStr, unsigned int recoveryType, const CipherSpec *cipherSpecArchive);
const String *timelineTargetStr, unsigned int recoveryType, const CipherSpecMap *cipherSpecArchive);
#endif

View File

@@ -6,12 +6,14 @@ Stanza Create Command
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "command/check/common.h"
#include "command/control/common.h"
#include "command/stanza/common.h"
#include "command/stanza/create.h"
#include "common/debug.h"
#include "common/format/format.h"
#include "common/log.h"
#include "common/memContext.h"
#include "config/config.h"
@@ -77,11 +79,19 @@ cmdStanzaCreate(void)
// Format for the new stanza
const unsigned int format = cfgOptionIdxUInt(cfgOptRepoFormat, repoIdx);
// If the repo is encrypted, generate a cipher passphrase for encrypting subsequent archive files
// If the repo is encrypted, generate a cipher passphrase for encrypting archive files. Format >= 6 adds it by
// rotating to the first key id and earlier formats store it as the default key with no id.
const CipherSpec *const cipherSpecArchive = cipherSpecGen(cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx), format);
CipherSpecMap *const cipherSpecMapArchive = cipherSpecMapNew();
if (cipherSpecType(cipherSpecArchive) != cipherTypeNone && format < REPOSITORY_FORMAT_6)
cipherSpecMapAdd(cipherSpecMapArchive, CIPHER_SPEC_MAP_ID_DEFAULT_STR, cipherSpecArchive);
// Create and save archive info
infoArchive = infoArchiveNew(pgControl.version, pgControl.systemId, format, cipherSpecArchive);
infoArchive = infoArchiveNew(pgControl.version, pgControl.systemId, format, cipherSpecMapArchive);
if (cipherSpecType(cipherSpecArchive) != cipherTypeNone && format >= REPOSITORY_FORMAT_6)
infoArchiveCipherRotate(infoArchive, cipherSpecArchive, time(NULL));
infoArchiveSaveFile(infoArchive, storageRepoWriteStanza, INFO_ARCHIVE_PATH_FILE_STR, cfgCipherSpecMainIdx(repoIdx));

View File

@@ -6,12 +6,14 @@ Stanza Upgrade Command
#include <inttypes.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include "command/check/common.h"
#include "command/control/common.h"
#include "command/stanza/common.h"
#include "command/stanza/upgrade.h"
#include "common/debug.h"
#include "common/format/format.h"
#include "common/log.h"
#include "common/memContext.h"
#include "config/config.h"
@@ -117,6 +119,15 @@ cmdStanzaUpgrade(void)
infoArchiveFormatSet(infoArchive, format);
infoBackupFormatSet(infoBackup, format);
// A stanza migrated to format 6 keeps the key it already has as id 0 and adds a key for new WAL using rotation
if (formatArchive < REPOSITORY_FORMAT_6)
{
const CipherType cipherType = cfgOptionIdxStrId(cfgOptRepoCipherType, repoIdx);
if (cipherType != cipherTypeNone)
infoArchiveCipherRotate(infoArchive, cipherSpecGen(cipherType, format), time(NULL));
}
infoArchiveUpgrade = true;
infoBackupUpgrade = true;
}

View File

@@ -7,6 +7,7 @@ Verify File
#include "common/crypto/cipherBlock.h"
#include "common/crypto/hash.h"
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/filter/group.h"
#include "common/io/filter/sink.h"
#include "common/io/filter/size.h"
@@ -18,7 +19,7 @@ Verify File
FN_EXTERN VerifyResult
verifyFile(
const String *const filePathName, const uint64_t offset, const Variant *const limit, const CompressType compressType,
const Buffer *const fileChecksum, const uint64_t fileSize, const CipherSpec *const cipherSpec)
const Buffer *const fileChecksum, const uint64_t fileSize, const CipherSpecMap *const cipherSpecMap, const bool formatHeader)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(STRING, filePathName); // Fully qualified file name
@@ -27,7 +28,8 @@ verifyFile(
FUNCTION_LOG_PARAM(ENUM, compressType); // Compression type
FUNCTION_LOG_PARAM(BUFFER, fileChecksum); // Checksum for the file
FUNCTION_LOG_PARAM(UINT64, fileSize); // Size of file
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec); // Cipher spec to access the repo file if encrypted
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecMap); // Cipher keys to access the repo file if encrypted
FUNCTION_LOG_PARAM(BOOL, formatHeader); // May the file begin with a format header?
FUNCTION_LOG_END();
ASSERT(filePathName != NULL);
@@ -45,8 +47,17 @@ verifyFile(
IoFilterGroup *const filterGroup = ioReadFilterGroup(read);
// Add decryption filter
if (cipherSpecType(cipherSpec) != cipherTypeNone)
ioFilterGroupAdd(filterGroup, cipherBlockNewP(cipherModeDecrypt, cipherSpec));
if (cipherSpecMapSize(cipherSpecMap) != 0)
{
// WAL contains a format header with the key id. Backup files contain no header so their key is under the default id.
if (formatHeader)
cipherBlockFormatFilterGroupReadAddMap(filterGroup, cipherSpecMap);
else
{
cipherBlockFilterGroupAdd(
filterGroup, cipherModeDecrypt, cipherSpecMapGet(cipherSpecMap, CIPHER_SPEC_MAP_ID_DEFAULT_STR));
}
}
// Add decompression filter
if (compressType != compressTypeNone)

View File

@@ -5,7 +5,7 @@ Verify File
#define COMMAND_VERIFY_FILE_H
#include "common/compress/helper.h"
#include "common/crypto/spec.h"
#include "common/format/cipherSpecMap.h"
/***********************************************************************************************************************************
File result
@@ -25,6 +25,6 @@ Functions
// Verify a file in the pgBackRest repository
FN_EXTERN VerifyResult verifyFile(
const String *filePathName, uint64_t offset, const Variant *limit, CompressType compressType, const Buffer *fileChecksum,
uint64_t fileSize, const CipherSpec *cipherSpec);
uint64_t fileSize, const CipherSpecMap *cipherSpecMap, bool formatHeader);
#endif

View File

@@ -41,12 +41,13 @@ verifyFileProtocol(PackRead *const param)
const CompressType compressType = (CompressType)pckReadU32P(param);
const Buffer *const fileChecksum = pckReadBinP(param);
const uint64_t fileSize = pckReadU64P(param);
const CipherSpec *const cipherSpec = cipherSpecNewPack(param);
const CipherSpecMap *const cipherSpecMap = cipherSpecMapNewPack(param);
const bool formatHeader = pckReadBoolP(param);
// Return result
pckWriteU32P(
protocolServerResultData(result),
verifyFile(filePathName, offset, limit, compressType, fileChecksum, fileSize, cipherSpec));
verifyFile(filePathName, offset, limit, compressType, fileChecksum, fileSize, cipherSpecMap, formatHeader));
}
MEM_CONTEXT_TEMP_END();

View File

@@ -125,7 +125,7 @@ typedef struct VerifyJobData
const InfoPg *pgHistory; // Database history list
bool backupProcessing; // Are we processing WAL or are we processing backups
const CipherSpec *cipherSpecManifest; // Cipher spec for reading backup manifests
const CipherSpec *cipherSpecArchive; // Cipher spec for reading WAL files
const CipherSpecMap *cipherSpecArchive; // Cipher keys for reading WAL files
unsigned int jobErrorTotal; // Total errors that occurred during the job execution
List *archiveIdResultList; // Archive results
List *backupResultList; // Backup results
@@ -816,7 +816,7 @@ verifyArchive(VerifyJobData *const jobData)
IoFilterGroup *const walFilterGroup = ioReadFilterGroup(storageReadIo(walRead));
// Add decryption filter when required
cipherBlockFilterGroupAdd(walFilterGroup, cipherModeDecrypt, jobData->cipherSpecArchive);
cipherBlockFormatFilterGroupReadAddMap(walFilterGroup, jobData->cipherSpecArchive);
// If the file is compressed, add a decompression filter
if (compressTypeFromName(walFile) != compressTypeNone)
@@ -856,7 +856,8 @@ verifyArchive(VerifyJobData *const jobData)
pckWriteU32P(param, compressTypeFromName(filePathName));
pckWriteBinP(param, checksum);
pckWriteU64P(param, archiveResult->pgWalInfo.size);
cipherSpecPack(param, jobData->cipherSpecArchive);
cipherSpecMapPack(param, jobData->cipherSpecArchive);
pckWriteBoolP(param, true);
// Assign job to result, prepending the archiveId to the key for consistency with backup processing
const String *const jobKey = strNewFmt("%s/%s", strZ(archiveResult->archiveId), strZ(filePathName));
@@ -1090,7 +1091,8 @@ verifyBackup(VerifyJobData *const jobData)
pckWriteU32P(param, compressTypeNone);
pckWriteBinP(param, BUF(fileData.checksumRepoSha1, HASH_TYPE_SHA1_SIZE));
pckWriteU64P(param, fileData.sizeRepo);
cipherSpecPack(param, cipherSpecNewNone());
cipherSpecMapPack(param, cipherSpecMapNew());
pckWriteBoolP(param, false);
}
// Else use the file checksum, which may require additional filters, e.g. decompression
else
@@ -1098,7 +1100,13 @@ verifyBackup(VerifyJobData *const jobData)
pckWriteU32P(param, manifestData(jobData->manifest)->backupOptionCompressType);
pckWriteBinP(param, BUF(fileData.checksumSha1, HASH_TYPE_SHA1_SIZE));
pckWriteU64P(param, fileData.size);
cipherSpecPack(param, manifestCipherSpec(jobData->manifest));
// Backup files contain no key id so their key goes under the default id
CipherSpecMap *const cipherSpecMap = cipherSpecMapNew();
cipherSpecMapAdd(
cipherSpecMap, CIPHER_SPEC_MAP_ID_DEFAULT_STR, manifestCipherSpec(jobData->manifest));
cipherSpecMapPack(param, cipherSpecMap);
pckWriteBoolP(param, false);
}
// Assign job to result (prepend backup label being processed to the key since some files are in a prior
@@ -1616,7 +1624,7 @@ verifyProcess(const bool verboseText)
.walFileList = strLstNew(),
.pgHistory = infoArchivePg(archiveInfo),
.cipherSpecManifest = infoBackupCipherSpec(backupInfo),
.cipherSpecArchive = infoArchiveCipherSpec(archiveInfo),
.cipherSpecArchive = infoArchiveCipherSpecMap(archiveInfo),
.archiveIdResultList = lstNewP(sizeof(VerifyArchiveResult), .comparator = archiveIdComparator),
.backupResultList = lstNewP(sizeof(VerifyBackupResult), .comparator = lstComparatorStr),
};

View File

@@ -31,19 +31,23 @@ 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.
The first three bytes of the header magic are the format and the last is a marker indicating whether a key id follows. The format
comes first so it is always in the same place, which is how a version decides whether it can read the file. The marker is only
checked once the format turns out to be readable, and it must be valid for this version.
If a key id is indicated by the marker, there is first a byte to indicate the length of the key id and then the key id as a string.
***********************************************************************************************************************************/
#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_MARKER_NONE '_'
#define CIPHER_BLOCK_FORMAT_MARKER_KEY 'K'
#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 key id length is encoded as a single byte with a max of 255 characters
#define CIPHER_BLOCK_FORMAT_KEY_SIZE_MAX 255
// Total header length: magic, format, and marker
#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
@@ -54,17 +58,22 @@ static_assert(
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");
// Max possible size of the header, the fixed part plus a length byte and the longest key id it can contain
#define CIPHER_BLOCK_FORMAT_HEADER_SIZE_MAX \
(CIPHER_BLOCK_FORMAT_HEADER_SIZE + 1 + CIPHER_BLOCK_FORMAT_KEY_SIZE_MAX)
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
typedef struct CipherBlockFormat
{
const CipherSpec *cipherSpec; // Cipher spec the content is decrypted with
const CipherSpecMap *cipherSpecMap; // Keys to choose from
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
uint8_t header[CIPHER_BLOCK_FORMAT_HEADER_SIZE_MAX]; // Header bytes held until there are enough to read
size_t headerSize; // Header bytes held so far
size_t headerSizeExpected; // Total header size, which grows as the header is read
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;
@@ -112,9 +121,12 @@ cipherBlockFormatHeaderRead(const uint8_t *const header)
// 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)
// Check that the marker is valid for this version
if (headerZ[CIPHER_BLOCK_FORMAT_HEADER_SIZE - 1] != CIPHER_BLOCK_FORMAT_MARKER_NONE &&
headerZ[CIPHER_BLOCK_FORMAT_HEADER_SIZE - 1] != CIPHER_BLOCK_FORMAT_MARKER_KEY)
{
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)
@@ -124,8 +136,8 @@ cipherBlockFormatHeaderRead(const uint8_t *const header)
}
/***********************************************************************************************************************************
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
Build the block cipher that decrypts the content, which is only possible once the header has been read since the header defines the
format and the key id
***********************************************************************************************************************************/
static void
cipherBlockFormatCipherNew(CipherBlockFormat *const this)
@@ -137,18 +149,33 @@ cipherBlockFormatCipherNew(CipherBlockFormat *const this)
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
// Get the key specified in the header or the default key when none is specified
const CipherSpec *cipherSpec;
MEM_CONTEXT_TEMP_BEGIN()
{
const String *keyId = CIPHER_SPEC_MAP_ID_DEFAULT_STR;
if (this->headerSize > CIPHER_BLOCK_FORMAT_HEADER_SIZE)
{
keyId = strNewZN(
(const char *)this->header + CIPHER_BLOCK_FORMAT_HEADER_SIZE + 1,
this->headerSize - CIPHER_BLOCK_FORMAT_HEADER_SIZE - 1);
}
cipherSpec = cipherSpecMapGet(this->cipherSpecMap, keyId);
}
MEM_CONTEXT_TEMP_END();
// Decrypt raw since the format header has already been parsed
MEM_CONTEXT_OBJ_BEGIN(this)
{
this->cipherBlock = cipherBlockNewP(
cipherModeDecrypt, cipherSpecDupP(this->cipherSpec, .digestDefault = repoFormatDigest(this->format)),
cipherModeDecrypt, cipherSpecDupP(cipherSpec, .digestDefault = repoFormatDigest(this->format)),
.header = cipherBlockHeaderNone);
}
MEM_CONTEXT_OBJ_END();
@@ -175,20 +202,47 @@ cipherBlockFormatProcess(THIS_VOID, const Buffer *const source, Buffer *const de
if (source != NULL)
{
// Hold back the header until there is enough of it to read
// Read until the header is complete. The total size is not known until the marker and key id length have been read.
if (this->cipherBlock == NULL)
{
this->sourceOffset = CIPHER_BLOCK_FORMAT_HEADER_SIZE - this->headerSize;
this->sourceOffset = 0;
if (this->sourceOffset > bufUsed(source))
this->sourceOffset = bufUsed(source);
do
{
size_t copySize = this->headerSizeExpected - this->headerSize;
memcpy(this->header + this->headerSize, bufPtrConst(source), this->sourceOffset);
this->headerSize += this->sourceOffset;
if (copySize > bufUsed(source) - this->sourceOffset)
copySize = bufUsed(source) - this->sourceOffset;
// Nothing can be decrypted until the header is complete
if (this->headerSize < CIPHER_BLOCK_FORMAT_HEADER_SIZE)
FUNCTION_LOG_RETURN_VOID();
memcpy(this->header + this->headerSize, bufPtrConst(source) + this->sourceOffset, copySize);
this->headerSize += copySize;
this->sourceOffset += copySize;
// Nothing can be decrypted until the header is complete
if (this->headerSize < this->headerSizeExpected)
FUNCTION_LOG_RETURN_VOID();
// Parse the fixed part first to reject an invalid header before using the marker
if (this->headerSizeExpected == CIPHER_BLOCK_FORMAT_HEADER_SIZE)
{
this->format = cipherBlockFormatHeaderRead(this->header);
if (this->header[CIPHER_BLOCK_FORMAT_HEADER_SIZE - 1] == CIPHER_BLOCK_FORMAT_MARKER_KEY)
this->headerSizeExpected++;
}
// Else the length is complete so wait for the key id
else if (this->headerSizeExpected == CIPHER_BLOCK_FORMAT_HEADER_SIZE + 1)
{
const size_t keySize = this->header[CIPHER_BLOCK_FORMAT_HEADER_SIZE];
// Marker indicates a key id but the length is zero
if (keySize == 0)
THROW(FormatError, "invalid cipher header");
this->headerSizeExpected += keySize;
}
}
while (this->headerSize < this->headerSizeExpected);
cipherBlockFormatCipherNew(this);
}
@@ -284,24 +338,25 @@ cipherBlockFormatResultPack(THIS_VOID)
/**********************************************************************************************************************************/
FN_EXTERN IoFilter *
cipherBlockFormatNew(const CipherSpec *const cipherSpec, const CipherBlockFormatNewParam param)
cipherBlockFormatNew(const CipherSpecMap *const cipherSpecMap, const CipherBlockFormatNewParam param)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecMap);
FUNCTION_LOG_PARAM(UINT, param.format);
FUNCTION_LOG_END();
FUNCTION_AUDIT_HELPER();
ASSERT(cipherSpec != NULL);
ASSERT(cipherSpecType(cipherSpec) != cipherTypeNone);
ASSERT(cipherSpecMap != NULL);
ASSERT(cipherSpecMapSize(cipherSpecMap) != 0);
OBJ_NEW_BEGIN(CipherBlockFormat, .childQty = MEM_CONTEXT_QTY_MAX)
{
*this = (CipherBlockFormat)
{
.cipherSpec = cipherSpecDupP(cipherSpec),
.cipherSpecMap = cipherSpecMapDup(cipherSpecMap),
.formatExpected = param.format,
.headerSizeExpected = CIPHER_BLOCK_FORMAT_HEADER_SIZE,
};
}
OBJ_NEW_END();
@@ -313,7 +368,7 @@ cipherBlockFormatNew(const CipherSpec *const cipherSpec, const CipherBlockFormat
{
PackWrite *const packWrite = pckWriteNewP();
cipherSpecPack(packWrite, cipherSpec);
cipherSpecMapPack(packWrite, cipherSpecMap);
pckWriteU32P(packWrite, param.format);
pckWriteEndP(packWrite);
@@ -337,10 +392,10 @@ cipherBlockFormatNewPack(const Pack *const paramList)
MEM_CONTEXT_TEMP_BEGIN()
{
PackRead *const paramListPack = pckReadNew(paramList);
const CipherSpec *const cipherSpec = cipherSpecNewPack(paramListPack);
const CipherSpecMap *const cipherSpecMap = cipherSpecMapNewPack(paramListPack);
const unsigned int format = pckReadU32P(paramListPack);
result = ioFilterMove(cipherBlockFormatNewP(cipherSpec, .format = format), memContextPrior());
result = ioFilterMove(cipherBlockFormatNewP(cipherSpecMap, .format = format), memContextPrior());
}
MEM_CONTEXT_TEMP_END();
@@ -360,48 +415,195 @@ cipherBlockFormatResult(PackRead *const packRead)
FUNCTION_TEST_RETURN(UINT, pckReadU32P(packRead));
}
/***********************************************************************************************************************************
Filter to write the header before the content. The header must be plaintext so this filter runs after the block cipher.
***********************************************************************************************************************************/
typedef struct CipherBlockFormatHeader
{
const Buffer *header; // Header to write before any content
size_t headerOffset; // Header bytes written so far
size_t sourceOffset; // Content bytes written from the current source
bool inputSame; // Is the same input required on the next process call?
} CipherBlockFormatHeader;
static void
cipherBlockFormatHeaderProcess(THIS_VOID, const Buffer *const source, Buffer *const destination)
{
THIS(CipherBlockFormatHeader);
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM_P(VOID, this);
FUNCTION_LOG_PARAM(BUFFER, source);
FUNCTION_LOG_PARAM(BUFFER, destination);
FUNCTION_LOG_END();
ASSERT(this != NULL);
ASSERT(source != NULL);
ASSERT(destination != NULL);
// Write the header before any content
if (this->headerOffset < bufUsed(this->header))
{
size_t copySize = bufUsed(this->header) - this->headerOffset;
if (copySize > bufRemains(destination))
copySize = bufRemains(destination);
bufCatC(destination, bufPtrConst(this->header), this->headerOffset, copySize);
this->headerOffset += copySize;
}
// Write the content once the header is complete
if (this->headerOffset == bufUsed(this->header))
{
size_t copySize = bufUsed(source) - this->sourceOffset;
if (copySize > bufRemains(destination))
copySize = bufRemains(destination);
bufCatC(destination, bufPtrConst(source), this->sourceOffset, copySize);
this->sourceOffset += copySize;
}
this->inputSame = this->headerOffset < bufUsed(this->header) || this->sourceOffset < bufUsed(source);
// Reset source offset when source has been completely processed
if (!this->inputSame)
this->sourceOffset = 0;
FUNCTION_LOG_RETURN_VOID();
}
static bool
cipherBlockFormatHeaderInputSame(const THIS_VOID)
{
THIS(const CipherBlockFormatHeader);
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM_P(VOID, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
FUNCTION_TEST_RETURN(BOOL, this->inputSame);
}
static IoFilter *
cipherBlockFormatHeaderNew(const Buffer *const header)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(BUFFER, header);
FUNCTION_TEST_END();
FUNCTION_AUDIT_HELPER();
ASSERT(header != NULL);
ASSERT(!bufEmpty(header));
OBJ_NEW_BEGIN(CipherBlockFormatHeader, .childQty = MEM_CONTEXT_QTY_MAX)
{
*this = (CipherBlockFormatHeader){.header = bufDup(header)};
}
OBJ_NEW_END();
// Create param list
Pack *paramList;
MEM_CONTEXT_TEMP_BEGIN()
{
PackWrite *const packWrite = pckWriteNewP();
pckWriteBinP(packWrite, header);
pckWriteEndP(packWrite);
paramList = pckMove(pckWriteResult(packWrite), memContextPrior());
}
MEM_CONTEXT_TEMP_END();
FUNCTION_TEST_RETURN(
IO_FILTER,
ioFilterNewP(
CIPHER_BLOCK_FORMAT_HEADER_FILTER_TYPE, this, paramList, .inOut = cipherBlockFormatHeaderProcess,
.inputSame = cipherBlockFormatHeaderInputSame));
}
FN_EXTERN IoFilter *
cipherBlockFormatHeaderNewPack(const Pack *const paramList)
{
IoFilter *result = NULL;
MEM_CONTEXT_TEMP_BEGIN()
{
result = ioFilterMove(cipherBlockFormatHeaderNew(pckReadBinP(pckReadNew(paramList))), memContextPrior());
}
MEM_CONTEXT_TEMP_END();
return result;
}
/**********************************************************************************************************************************/
FN_EXTERN void
cipherBlockFormatFilterGroupWriteAdd(
Buffer *const buffer, IoFilterGroup *const filterGroup, const CipherSpec *const cipherSpec, const unsigned int format)
IoFilterGroup *const filterGroup, const CipherSpec *const cipherSpec, const unsigned int format,
const CipherBlockFormatFilterGroupWriteAddParam param)
{
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_PARAM(STRING, param.keyId);
FUNCTION_LOG_END();
FUNCTION_AUDIT_HELPER();
ASSERT(buffer != NULL);
ASSERT(filterGroup != NULL);
ASSERT(cipherSpec != NULL);
ASSERT(format >= REPOSITORY_FORMAT_MIN && format <= REPOSITORY_FORMAT_MAX);
ASSERT(param.keyId == NULL || !strEmpty(param.keyId));
ASSERT(param.keyId == NULL || strSize(param.keyId) <= CIPHER_BLOCK_FORMAT_KEY_SIZE_MAX);
// Key id is only valid for format >= 6
ASSERT(param.keyId == NULL || format >= REPOSITORY_FORMAT_6);
if (cipherSpecType(cipherSpec) != cipherTypeNone)
{
// A format from 6 writes the header in place of the magic, so the content behind it is encrypted raw
// Format >= 6 writes the header in place of the magic and 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, cipherSpecDupP(cipherSpec, .digestDefault = repoFormatDigest(format)),
.header = header ? cipherBlockHeaderNone : cipherBlockHeaderMagic));
// The filter is added after the block cipher so the plaintext header is placed before the cipher output
if (header)
{
MEM_CONTEXT_TEMP_BEGIN()
{
char headerZ[CIPHER_BLOCK_FORMAT_HEADER_SIZE + 1];
// Zero-pad the format and add the marker, depending on whether or not there is a key
snprintf(
headerZ, sizeof(headerZ), CIPHER_BLOCK_FORMAT_MAGIC "%03u%c", format % 1000,
param.keyId == NULL ? CIPHER_BLOCK_FORMAT_MARKER_NONE : CIPHER_BLOCK_FORMAT_MARKER_KEY);
Buffer *const headerBuffer = bufNew(CIPHER_BLOCK_FORMAT_HEADER_SIZE_MAX);
bufCatC(headerBuffer, (const uint8_t *)headerZ, 0, CIPHER_BLOCK_FORMAT_HEADER_SIZE);
// Write the key size and id when present
if (param.keyId != NULL)
{
const uint8_t keySize = (uint8_t)strSize(param.keyId);
bufCatC(headerBuffer, &keySize, 0, sizeof(keySize));
bufCatC(headerBuffer, (const uint8_t *)strZ(param.keyId), 0, strSize(param.keyId));
}
ioFilterGroupAdd(filterGroup, cipherBlockFormatHeaderNew(headerBuffer));
}
MEM_CONTEXT_TEMP_END();
}
}
FUNCTION_LOG_RETURN_VOID();
@@ -422,7 +624,37 @@ cipherBlockFormatFilterGroupReadAdd(IoFilterGroup *const filterGroup, const Ciph
ASSERT(cipherSpec != NULL);
if (cipherSpecType(cipherSpec) != cipherTypeNone)
ioFilterGroupAdd(filterGroup, cipherBlockFormatNewP(cipherSpec));
{
MEM_CONTEXT_TEMP_BEGIN()
{
CipherSpecMap *const cipherSpecMap = cipherSpecMapNew();
cipherSpecMapAdd(cipherSpecMap, CIPHER_SPEC_MAP_ID_DEFAULT_STR, cipherSpec);
ioFilterGroupAdd(filterGroup, cipherBlockFormatNewP(cipherSpecMap));
}
MEM_CONTEXT_TEMP_END();
}
FUNCTION_LOG_RETURN(IO_FILTER_GROUP, filterGroup);
}
/**********************************************************************************************************************************/
FN_EXTERN IoFilterGroup *
cipherBlockFormatFilterGroupReadAddMap(IoFilterGroup *const filterGroup, const CipherSpecMap *const cipherSpecMap)
{
FUNCTION_LOG_BEGIN(logLevelTrace);
FUNCTION_LOG_PARAM(IO_FILTER_GROUP, filterGroup);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecMap);
FUNCTION_LOG_END();
FUNCTION_AUDIT_HELPER();
ASSERT(filterGroup != NULL);
ASSERT(cipherSpecMap != NULL);
// An unencrypted repository has no keys, so there is nothing to decrypt
if (cipherSpecMapSize(cipherSpecMap) != 0)
ioFilterGroupAdd(filterGroup, cipherBlockFormatNewP(cipherSpecMap));
FUNCTION_LOG_RETURN(IO_FILTER_GROUP, filterGroup);
}

View File

@@ -7,18 +7,24 @@ derives the key with. Everything the format defines is here so that the block ci
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.
A file whose key cannot be determined from where the file is located stores the key in its header.
***********************************************************************************************************************************/
#ifndef COMMON_FORMAT_CIPHERBLOCKFORMAT_H
#define COMMON_FORMAT_CIPHERBLOCKFORMAT_H
#include "common/crypto/spec.h"
#include "common/format/cipherSpecMap.h"
#include "common/io/filter/group.h"
/***********************************************************************************************************************************
Filter type constant
Filter type constants
***********************************************************************************************************************************/
#define CIPHER_BLOCK_FORMAT_FILTER_TYPE STRID5("cipher-fmt", 0x28d36e45441230)
// Filter that writes the format header
#define CIPHER_BLOCK_FORMAT_HEADER_FILTER_TYPE STRID5("cipher-hdr", 0x24446e45441230)
/***********************************************************************************************************************************
Constructors
***********************************************************************************************************************************/
@@ -28,12 +34,16 @@ typedef struct CipherBlockFormatNewParam
unsigned int format; // Format the file is expected at, any when zero
} CipherBlockFormatNewParam;
#define cipherBlockFormatNewP(cipherSpec, ...) \
cipherBlockFormatNew(cipherSpec, (CipherBlockFormatNewParam){VAR_PARAM_INIT, __VA_ARGS__})
// Decrypt with the stored key id or with CIPHER_SPEC_MAP_ID_DEFAULT when no key is stored
#define cipherBlockFormatNewP(cipherSpecMap, ...) \
cipherBlockFormatNew(cipherSpecMap, (CipherBlockFormatNewParam){VAR_PARAM_INIT, __VA_ARGS__})
FN_EXTERN IoFilter *cipherBlockFormatNew(const CipherSpec *cipherSpec, CipherBlockFormatNewParam param);
FN_EXTERN IoFilter *cipherBlockFormatNew(const CipherSpecMap *cipherSpecMap, CipherBlockFormatNewParam param);
FN_EXTERN IoFilter *cipherBlockFormatNewPack(const Pack *paramList);
// Create filter that writes the format header from its param list
FN_EXTERN IoFilter *cipherBlockFormatHeaderNewPack(const Pack *paramList);
/***********************************************************************************************************************************
Getters/Setters
***********************************************************************************************************************************/
@@ -43,13 +53,28 @@ 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);
// Write the header the format requires into the buffer the content will be written into and add encryption to the filter group. A
// key id is stored in the header when one is specified. No header is added when the repository is not encrypted.
typedef struct CipherBlockFormatFilterGroupWriteAddParam
{
VAR_PARAM_HEADER;
const String *keyId; // Key id to store in the header, NULL to store no id
} CipherBlockFormatFilterGroupWriteAddParam;
// 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.
#define cipherBlockFormatFilterGroupWriteAddP(filterGroup, cipherSpec, format, ...) \
cipherBlockFormatFilterGroupWriteAdd( \
filterGroup, cipherSpec, format, (CipherBlockFormatFilterGroupWriteAddParam){VAR_PARAM_INIT, __VA_ARGS__})
FN_EXTERN void cipherBlockFormatFilterGroupWriteAdd(
IoFilterGroup *filterGroup, const CipherSpec *cipherSpec, unsigned int format,
CipherBlockFormatFilterGroupWriteAddParam param);
// Add decryption to the filter group for a file with format yet to be determined. The key is stored under
// CIPHER_SPEC_MAP_ID_DEFAULT, so the file must not contain a key id. Nothing is added when the repository is not encrypted.
FN_EXTERN IoFilterGroup *cipherBlockFormatFilterGroupReadAdd(IoFilterGroup *filterGroup, const CipherSpec *cipherSpec);
// Add decryption to the filter group for a file that may contain a key id. An empty map means the repository is not encrypted and
// nothing is added.
FN_EXTERN IoFilterGroup *cipherBlockFormatFilterGroupReadAddMap(IoFilterGroup *filterGroup, const CipherSpecMap *cipherSpecMap);
#endif

View File

@@ -0,0 +1,223 @@
/***********************************************************************************************************************************
Cipher Spec Map
***********************************************************************************************************************************/
#include <build.h>
#include "common/debug.h"
#include "common/format/cipherSpecMap.h"
#include "common/log.h"
STRING_EXTERN(CIPHER_SPEC_MAP_ID_DEFAULT_STR, CIPHER_SPEC_MAP_ID_DEFAULT);
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
struct CipherSpecMap
{
CipherSpecMapPub pub; // Publicly accessible variables
};
/**********************************************************************************************************************************/
FN_EXTERN CipherSpecMap *
cipherSpecMapNew(void)
{
FUNCTION_TEST_VOID();
OBJ_NEW_BEGIN(CipherSpecMap, .childQty = MEM_CONTEXT_QTY_MAX)
{
*this = (CipherSpecMap)
{
.pub = {.list = lstNewP(sizeof(CipherSpecMapItem), .comparator = lstComparatorStr)},
};
}
OBJ_NEW_END();
FUNCTION_TEST_RETURN(CIPHER_SPEC_MAP, this);
}
/**********************************************************************************************************************************/
FN_EXTERN CipherSpecMap *
cipherSpecMapNewPack(PackRead *const packRead)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(PACK_READ, packRead);
FUNCTION_TEST_END();
ASSERT(packRead != NULL);
CipherSpecMap *const this = cipherSpecMapNew();
MEM_CONTEXT_OBJ_BEGIN(this)
{
const unsigned int total = pckReadU32P(packRead);
for (unsigned int keyIdx = 0; keyIdx < total; keyIdx++)
{
const CipherSpecMapItem item =
{
.id = pckReadStrP(packRead),
.cipherSpec = cipherSpecNewPack(packRead),
};
lstAdd(this->pub.list, &item);
}
lstSort(this->pub.list, sortOrderAsc);
this->pub.idCurrent = pckReadStrP(packRead);
}
MEM_CONTEXT_OBJ_END();
// The key with no id is never current
ASSERT(this->pub.idCurrent == NULL || !strEqZ(this->pub.idCurrent, CIPHER_SPEC_MAP_ID_DEFAULT));
FUNCTION_TEST_RETURN(CIPHER_SPEC_MAP, this);
}
/**********************************************************************************************************************************/
FN_EXTERN const CipherSpec *
cipherSpecMapGet(const CipherSpecMap *const this, const String *const id)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_SPEC_MAP, this);
FUNCTION_TEST_PARAM(STRING, id);
FUNCTION_TEST_END();
ASSERT(this != NULL);
ASSERT(id != NULL);
const CipherSpecMapItem *const item = lstFind(this->pub.list, &id);
// Error when request key was not found
if (item == NULL)
THROW_FMT(CryptoError, "unable to find cipher key '%s'", strZ(id));
FUNCTION_TEST_RETURN_CONST(CIPHER_SPEC, item->cipherSpec);
}
/**********************************************************************************************************************************/
FN_EXTERN void
cipherSpecMapAdd(CipherSpecMap *const this, const String *const id, const CipherSpec *const cipherSpec)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_SPEC_MAP, this);
FUNCTION_TEST_PARAM(STRING, id);
FUNCTION_TEST_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_TEST_END();
ASSERT(this != NULL);
ASSERT(id != NULL);
ASSERT(!strEmpty(id));
ASSERT(cipherSpec != NULL);
ASSERT(lstFind(this->pub.list, &id) == NULL);
MEM_CONTEXT_OBJ_BEGIN(this)
{
const CipherSpecMapItem item = {.id = strDup(id), .cipherSpec = cipherSpecDupP(cipherSpec)};
lstAdd(this->pub.list, &item);
lstSort(this->pub.list, sortOrderAsc);
// The default key id is for files that contain no id so it is never current
if (!strEqZ(id, CIPHER_SPEC_MAP_ID_DEFAULT))
this->pub.idCurrent = item.id;
}
MEM_CONTEXT_OBJ_END();
// The key with no id is never current and every other key is current once added
ASSERT(strEqZ(id, CIPHER_SPEC_MAP_ID_DEFAULT) || strEq(cipherSpecMapIdCurrent(this), id));
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN void
cipherSpecMapIdCurrentSet(CipherSpecMap *const this, const String *const id)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_SPEC_MAP, this);
FUNCTION_TEST_PARAM(STRING, id);
FUNCTION_TEST_END();
ASSERT(this != NULL);
ASSERT(id != NULL);
ASSERT(!strEqZ(id, CIPHER_SPEC_MAP_ID_DEFAULT));
const CipherSpecMapItem *const item = lstFind(this->pub.list, &id);
ASSERT(item != NULL);
// Point at the id stored with the key rather than duplicating it
this->pub.idCurrent = item->id;
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN CipherSpecMap *
cipherSpecMapDup(const CipherSpecMap *const this)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(CIPHER_SPEC_MAP, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
CipherSpecMap *const result = cipherSpecMapNew();
for (unsigned int keyIdx = 0; keyIdx < cipherSpecMapSize(this); keyIdx++)
{
const CipherSpecMapItem *const item = cipherSpecMapGetIdx(this, keyIdx);
cipherSpecMapAdd(result, item->id, item->cipherSpec);
}
if (cipherSpecMapIdCurrent(this) != NULL)
cipherSpecMapIdCurrentSet(result, cipherSpecMapIdCurrent(this));
FUNCTION_TEST_RETURN(CIPHER_SPEC_MAP, result);
}
/**********************************************************************************************************************************/
FN_EXTERN void
cipherSpecMapPack(PackWrite *const packWrite, const CipherSpecMap *const this)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(PACK_WRITE, packWrite);
FUNCTION_TEST_PARAM(CIPHER_SPEC_MAP, this);
FUNCTION_TEST_END();
ASSERT(packWrite != NULL);
ASSERT(this != NULL);
pckWriteU32P(packWrite, cipherSpecMapSize(this));
for (unsigned int keyIdx = 0; keyIdx < cipherSpecMapSize(this); keyIdx++)
{
const CipherSpecMapItem *const item = cipherSpecMapGetIdx(this, keyIdx);
pckWriteStrP(packWrite, item->id);
cipherSpecPack(packWrite, item->cipherSpec);
}
pckWriteStrP(packWrite, cipherSpecMapIdCurrent(this));
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN void
cipherSpecMapToLog(const CipherSpecMap *const this, StringStatic *const debugLog)
{
// List only the ids (never the keys themselves)
strStcCat(debugLog, "{ids: [");
for (unsigned int keyIdx = 0; keyIdx < cipherSpecMapSize(this); keyIdx++)
{
if (keyIdx != 0)
strStcCat(debugLog, ", ");
strStcFmt(debugLog, "%s", strZ(cipherSpecMapGetIdx(this, keyIdx)->id));
}
strStcCat(debugLog, "]}");
}

View File

@@ -0,0 +1,118 @@
/***********************************************************************************************************************************
Cipher Spec Map
The keys used to encrypt a repository, stored by id. A file identifies its key by id, either in the header or by where the file is
stored. The meaning of an id is set by the caller. archive.info numbers its keys and backup.info uses the backup set label.
One key is current and new files are encrypted with it. Adding a key makes it current, except for the key stored under
CIPHER_SPEC_MAP_ID_DEFAULT.
***********************************************************************************************************************************/
#ifndef COMMON_FORMAT_CIPHERSPECMAP_H
#define COMMON_FORMAT_CIPHERSPECMAP_H
/***********************************************************************************************************************************
Object type
***********************************************************************************************************************************/
typedef struct CipherSpecMap CipherSpecMap;
#include "common/crypto/spec.h"
#include "common/type/list.h"
#include "common/type/object.h"
#include "common/type/pack.h"
/***********************************************************************************************************************************
Id for the key required by a file that contains no key id. In archive.info this is the key preserved by migration, which is what
files written before key ids existed were encrypted with.
***********************************************************************************************************************************/
#define CIPHER_SPEC_MAP_ID_DEFAULT "0"
STRING_DECLARE(CIPHER_SPEC_MAP_ID_DEFAULT_STR);
/***********************************************************************************************************************************
Constructors
***********************************************************************************************************************************/
FN_EXTERN CipherSpecMap *cipherSpecMapNew(void);
// Create from a pack written by cipherSpecMapPack()
FN_EXTERN CipherSpecMap *cipherSpecMapNewPack(PackRead *packRead);
/***********************************************************************************************************************************
Getters/Setters
***********************************************************************************************************************************/
typedef struct CipherSpecMapItem
{
const String *id; // Id associated with the key
const CipherSpec *cipherSpec; // Key and the digest it derives with
} CipherSpecMapItem;
typedef struct CipherSpecMapPub
{
List *list; // Keys sorted by id
const String *idCurrent; // Current key id
} CipherSpecMapPub;
// Total keys
FN_INLINE_ALWAYS unsigned int
cipherSpecMapSize(const CipherSpecMap *const this)
{
return lstSize(THIS_PUB(CipherSpecMap)->list);
}
// Key by index
FN_INLINE_ALWAYS const CipherSpecMapItem *
cipherSpecMapGetIdx(const CipherSpecMap *const this, const unsigned int idx)
{
return lstGet(THIS_PUB(CipherSpecMap)->list, idx);
}
// Key by id. Errors when the id is not found.
FN_EXTERN const CipherSpec *cipherSpecMapGet(const CipherSpecMap *this, const String *id);
// Current key id, NULL when the map has only the default key with no id
FN_INLINE_ALWAYS const String *
cipherSpecMapIdCurrent(const CipherSpecMap *const this)
{
return THIS_PUB(CipherSpecMap)->idCurrent;
}
// Set the current key id. The id must already be in the map.
FN_EXTERN void cipherSpecMapIdCurrentSet(CipherSpecMap *this, const String *id);
// Duplicate
FN_EXTERN CipherSpecMap *cipherSpecMapDup(const CipherSpecMap *this);
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
// Add a key and make it current, except for a key stored under CIPHER_SPEC_MAP_ID_DEFAULT. The id must be unique.
FN_EXTERN void cipherSpecMapAdd(CipherSpecMap *this, const String *id, const CipherSpec *cipherSpec);
// Write to a pack so it can be passed over a protocol
FN_EXTERN void cipherSpecMapPack(PackWrite *packWrite, const CipherSpecMap *this);
// Move to a new parent mem context
FN_INLINE_ALWAYS CipherSpecMap *
cipherSpecMapMove(CipherSpecMap *const this, MemContext *const parentNew)
{
return objMove(this, parentNew);
}
/***********************************************************************************************************************************
Destructor
***********************************************************************************************************************************/
FN_INLINE_ALWAYS void
cipherSpecMapFree(CipherSpecMap *const this)
{
objFree(this);
}
/***********************************************************************************************************************************
Macros for function logging
***********************************************************************************************************************************/
FN_EXTERN void cipherSpecMapToLog(const CipherSpecMap *this, StringStatic *debugLog);
#define FUNCTION_LOG_CIPHER_SPEC_MAP_TYPE \
CipherSpecMap *
#define FUNCTION_LOG_CIPHER_SPEC_MAP_FORMAT(value, buffer, bufferSize) \
FUNCTION_LOG_OBJECT_FORMAT(value, cipherSpecMapToLog, buffer, bufferSize)
#endif

View File

@@ -146,7 +146,7 @@ Option constants
#define CFGOPT_VERBOSE "verbose"
#define CFGOPT_VERSION "version"
#define CFG_OPTION_TOTAL 201
#define CFG_OPTION_TOTAL 202
/***********************************************************************************************************************************
Option value constants
@@ -587,6 +587,7 @@ typedef enum
cfgOptRepoBundleLimit,
cfgOptRepoBundleSize,
cfgOptRepoCipherPass,
cfgOptRepoCipherRotate,
cfgOptRepoCipherType,
cfgOptRepoFormat,
cfgOptRepoGcsBucket,

View File

@@ -29,6 +29,7 @@ static const StringPubConst parseRuleValueStr[] =
PARSE_RULE_STRPUB("12"), // val/str
PARSE_RULE_STRPUB("128KiB"), // val/str
PARSE_RULE_STRPUB("128MiB"), // val/str
PARSE_RULE_STRPUB("15d"), // val/str
PARSE_RULE_STRPUB("15m"), // val/str
PARSE_RULE_STRPUB("15s"), // val/str
PARSE_RULE_STRPUB("16GiB"), // val/str
@@ -72,8 +73,10 @@ static const StringPubConst parseRuleValueStr[] =
PARSE_RULE_STRPUB("8MiB"), // val/str
PARSE_RULE_STRPUB("9"), // val/str
PARSE_RULE_STRPUB("900"), // val/str
PARSE_RULE_STRPUB("90d"), // val/str
PARSE_RULE_STRPUB("999"), // val/str
PARSE_RULE_STRPUB("9999999"), // val/str
PARSE_RULE_STRPUB("9999999d"), // val/str
PARSE_RULE_STRPUB("accept-new"), // val/str
PARSE_RULE_STRPUB("aes-256-cbc"), // val/str
PARSE_RULE_STRPUB("asc"), // val/str
@@ -168,6 +171,7 @@ typedef enum
parseRuleValStrQT_12_QT, // val/str/enum
parseRuleValStrQT_128KiB_QT, // val/str/enum
parseRuleValStrQT_128MiB_QT, // val/str/enum
parseRuleValStrQT_15d_QT, // val/str/enum
parseRuleValStrQT_15m_QT, // val/str/enum
parseRuleValStrQT_15s_QT, // val/str/enum
parseRuleValStrQT_16GiB_QT, // val/str/enum
@@ -211,8 +215,10 @@ typedef enum
parseRuleValStrQT_8MiB_QT, // val/str/enum
parseRuleValStrQT_9_QT, // val/str/enum
parseRuleValStrQT_900_QT, // val/str/enum
parseRuleValStrQT_90d_QT, // val/str/enum
parseRuleValStrQT_999_QT, // val/str/enum
parseRuleValStrQT_9999999_QT, // val/str/enum
parseRuleValStrQT_9999999d_QT, // val/str/enum
parseRuleValStrQT_accept_DS_new_QT, // val/str/enum
parseRuleValStrQT_aes_DS_256_DS_cbc_QT, // val/str/enum
parseRuleValStrQT_asc_QT, // val/str/enum
@@ -483,6 +489,9 @@ static const int64_t parseRuleValueTime[] =
3600000, // val/time
86400000, // val/time
604800000, // val/time
1296000000, // val/time
7776000000, // val/time
863999913600000, // val/time
};
static const uint8_t parseRuleValueTimeStrMap[] =
@@ -497,6 +506,9 @@ static const uint8_t parseRuleValueTimeStrMap[] =
parseRuleValStrQT_1h_QT, // val/time/strmap
parseRuleValStrQT_1d_QT, // val/time/strmap
parseRuleValStrQT_7d_QT, // val/time/strmap
parseRuleValStrQT_15d_QT, // val/time/strmap
parseRuleValStrQT_90d_QT, // val/time/strmap
parseRuleValStrQT_9999999d_QT, // val/time/strmap
};
typedef enum
@@ -511,6 +523,9 @@ typedef enum
parseRuleValTime1h, // val/time/enum
parseRuleValTime1d, // val/time/enum
parseRuleValTime7d, // val/time/enum
parseRuleValTime15d, // val/time/enum
parseRuleValTime90d, // val/time/enum
parseRuleValTime9999999d, // val/time/enum
} ParseRuleValueTime;
/***********************************************************************************************************************************
@@ -5983,6 +5998,45 @@ static const ParseRuleOption parseRuleOption[CFG_OPTION_TOTAL] =
), // opt/repo-cipher-pass
), // opt/repo-cipher-pass
// -----------------------------------------------------------------------------------------------------------------------------
PARSE_RULE_OPTION // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_OPTION_NAME("repo-cipher-rotate"), // opt/repo-cipher-rotate
PARSE_RULE_OPTION_TYPE(Time), // opt/repo-cipher-rotate
PARSE_RULE_OPTION_RESET(true), // opt/repo-cipher-rotate
PARSE_RULE_OPTION_REQUIRED(true), // opt/repo-cipher-rotate
PARSE_RULE_OPTION_SECTION(Global), // opt/repo-cipher-rotate
PARSE_RULE_OPTION_GROUP_ID(Repo), // opt/repo-cipher-rotate
// opt/repo-cipher-rotate
PARSE_RULE_OPTION_COMMAND_ROLE_MAIN_VALID_LIST // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_OPTION_COMMAND(Backup) // opt/repo-cipher-rotate
PARSE_RULE_OPTION_COMMAND(Expire) // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
// opt/repo-cipher-rotate
PARSE_RULE_OPTIONAL // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_OPTIONAL_GROUP // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_OPTIONAL_DEPEND // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_VAL_OPT(RepoCipherType), // opt/repo-cipher-rotate
PARSE_RULE_VAL_STRID(aes_DS_256_DS_cbc), // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
// opt/repo-cipher-rotate
PARSE_RULE_OPTIONAL_ALLOW_RANGE // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_VAL_TIME(15d), // opt/repo-cipher-rotate
PARSE_RULE_VAL_TIME(9999999d), // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
// opt/repo-cipher-rotate
PARSE_RULE_OPTIONAL_DEFAULT // opt/repo-cipher-rotate
( // opt/repo-cipher-rotate
PARSE_RULE_VAL_TIME(90d), // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
), // opt/repo-cipher-rotate
// -----------------------------------------------------------------------------------------------------------------------------
PARSE_RULE_OPTION // opt/repo-cipher-type
( // opt/repo-cipher-type
PARSE_RULE_OPTION_NAME("repo-cipher-type"), // opt/repo-cipher-type
@@ -12218,6 +12272,7 @@ static const uint8_t optionResolveOrder[] =
cfgOptRepoBlockSizeSuper, // opt-resolve-order
cfgOptRepoBlockSizeSuperFull, // opt-resolve-order
cfgOptRepoCipherPass, // opt-resolve-order
cfgOptRepoCipherRotate, // opt-resolve-order
cfgOptRepoGcsKeyType, // opt-resolve-order
cfgOptRepoHost, // opt-resolve-order
cfgOptRepoHostConfig, // opt-resolve-order

View File

@@ -28,6 +28,7 @@ Object types
struct Info
{
InfoPub pub; // Publicly accessible variables
const CipherSpec *cipherSpecNone; // Returned by infoCipherSpec() when there are no keys
};
struct InfoSave
@@ -124,8 +125,11 @@ 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"
#define INFO_KEY_CIPHER_PASS_CURRENT "cipher-pass-current"
#define INFO_KEY_CIPHER_PASS_ROTATE "cipher-pass-rotate"
#define INFO_KEY_CIPHER_DIGEST "digest"
#define INFO_KEY_CIPHER_KEY "key"
FN_EXTERN Info *
infoNewLoad(
@@ -154,7 +158,6 @@ 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);
@@ -218,26 +221,61 @@ 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
else if (strEqZ(value->key, INFO_KEY_CIPHER_PASS))
if (strEqZ(value->key, INFO_KEY_CIPHER_PASS))
{
MEM_CONTEXT_OBJ_BEGIN(this)
MEM_CONTEXT_TEMP_BEGIN()
{
// 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);
// Dependent files are encrypted with the same cipher type as this one
const CipherType cipherType = cipherSpecType(cipherSpec);
CipherSpecMap *const cipherSpecMap = cipherSpecMapNew();
const Variant *const cipherPass = jsonToVar(value->value);
// Format >= 6 stores every key by id along with the digest it derives with
if (infoFormat(this) >= REPOSITORY_FORMAT_6)
{
const KeyValue *const keyKv = varKv(cipherPass);
const VariantList *const idList = kvKeyList(keyKv);
for (unsigned int idIdx = 0; idIdx < varLstSize(idList); idIdx++)
{
const Variant *const id = varLstGet(idList, idIdx);
const KeyValue *const keyData = varKv(kvGet(keyKv, id));
cipherSpecMapAdd(
cipherSpecMap, varStr(id),
cipherSpecNewP(
cipherType,
BUFSTR(varStr(kvGet(keyData, VARSTRDEF(INFO_KEY_CIPHER_KEY)))),
.digest = strIdFromZ(
strZ(varStr(kvGet(keyData, VARSTRDEF(INFO_KEY_CIPHER_DIGEST)))))));
}
}
// Else there is one key with no id and no digest, which every repository derives with SHA-1
else
{
cipherSpecMapAdd(
cipherSpecMap, CIPHER_SPEC_MAP_ID_DEFAULT_STR,
cipherSpecNewP(cipherType, BUFSTR(varStr(cipherPass)), .digest = hashTypeSha1));
}
infoCipherSpecMapSet(this, cipherSpecMap);
}
MEM_CONTEXT_OBJ_END();
MEM_CONTEXT_TEMP_END();
}
// Current key id
else if (strEqZ(value->key, INFO_KEY_CIPHER_PASS_CURRENT))
{
MEM_CONTEXT_TEMP_BEGIN()
{
cipherSpecMapIdCurrentSet(
this->pub.cipherSpecMap, jsonReadStr(jsonReadNew(value->value)));
}
MEM_CONTEXT_TEMP_END();
}
// Time of the last key rotation
else if (strEqZ(value->key, INFO_KEY_CIPHER_PASS_ROTATE))
this->pub.cipherRotateTime = (time_t)jsonReadInt64(jsonReadNew(value->value));
}
// Else pass to callback for processing
else
@@ -295,8 +333,8 @@ infoNewLoad(
MEM_CONTEXT_TEMP_END();
// A file with no cipher section has no encrypted dependent files
if (this->pub.cipherSpec == NULL)
infoCipherSpecSet(this, NULL);
if (this->pub.cipherSpecMap == NULL)
infoCipherSpecMapSet(this, NULL);
}
OBJ_NEW_END();
@@ -412,33 +450,70 @@ infoSave(Info *const this, IoWrite *const write, InfoSaveCallback *const callbac
infoSaveValue(&data, INFO_SECTION_BACKREST, INFO_KEY_FORMAT, jsonFromVar(VARUINT(infoFormat(this))));
infoSaveValue(&data, INFO_SECTION_BACKREST, INFO_KEY_VERSION, jsonFromVar(VARSTRDEF(PROJECT_VERSION)));
// Add cipher passphrase if defined
if (cipherSpecType(infoCipherSpec(this)) != cipherTypeNone)
// Add cipher keys if defined
const CipherSpecMap *const cipherSpecMap = infoCipherSpecMap(this);
if (cipherSpecMapSize(cipherSpecMap) != 0)
{
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. Format >= 6 must have a
// digest to store. Release builds check this too, instead of silently writing a pass that no reader can derive.
CHECK(
AssertError,
infoFormat(this) >= REPOSITORY_FORMAT_6 ?
cipherSpecDigest(infoCipherSpec(this)) != 0 : cipherSpecDigest(infoCipherSpec(this)) == hashTypeSha1,
"pass must derive with sha1 before format 6 and must have a digest from format 6");
// 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)
MEM_CONTEXT_TEMP_BEGIN()
{
char digestZ[STRID_MAX + 1];
strIdToZ(cipherSpecDigest(infoCipherSpec(this)), digestZ);
JsonWrite *const json = jsonWriteNewP();
infoSaveValue(&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_DIGEST, jsonFromVar(VARSTRZ(digestZ)));
// Format >= 6 stores every key by id along with the digest it derives with
if (infoFormat(this) >= REPOSITORY_FORMAT_6)
{
jsonWriteObjectBegin(json);
for (unsigned int keyIdx = 0; keyIdx < cipherSpecMapSize(cipherSpecMap); keyIdx++)
{
const CipherSpecMapItem *const item = cipherSpecMapGetIdx(cipherSpecMap, keyIdx);
// A key must have a digest to store. Release builds check this too, instead of silently writing a key
// that no reader can derive.
CHECK(AssertError, cipherSpecDigest(item->cipherSpec) != 0, "cipher key must have a digest from format 6");
jsonWriteObjectBegin(jsonWriteKey(json, item->id));
jsonWriteStrId(jsonWriteKeyZ(json, INFO_KEY_CIPHER_DIGEST), cipherSpecDigest(item->cipherSpec));
jsonWriteStr(
jsonWriteKeyZ(json, INFO_KEY_CIPHER_KEY), strNewBuf(cipherSpecPass(item->cipherSpec)));
jsonWriteObjectEnd(json);
}
jsonWriteObjectEnd(json);
}
// Else store only the key
else
{
const CipherSpec *const cipherSpec = infoCipherSpec(this);
CHECK(AssertError, cipherSpecMapSize(cipherSpecMap) == 1, "only one cipher key can be stored before format 6");
CHECK(
AssertError, cipherSpecDigest(cipherSpec) == hashTypeSha1,
"cipher key must derive with sha1 before format 6");
jsonWriteStr(json, strNewBuf(cipherSpecPass(cipherSpec)));
}
infoSaveValue(&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_PASS, jsonWriteResult(json));
// Record the current key id, which only exists at format >= 6
if (cipherSpecMapIdCurrent(cipherSpecMap) != NULL)
{
infoSaveValue(
&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_PASS_CURRENT,
jsonFromVar(VARSTR(cipherSpecMapIdCurrent(cipherSpecMap))));
}
// Record the time of the last key rotation
if (infoCipherRotateTime(this) != 0)
{
infoSaveValue(
&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_PASS_ROTATE, jsonFromVar(VARINT64(infoCipherRotateTime(this))));
}
}
infoSaveValue(
&data, INFO_SECTION_CIPHER, INFO_KEY_CIPHER_PASS,
jsonFromVar(VARSTR(strNewBuf(cipherSpecPass(infoCipherSpec(this))))));
MEM_CONTEXT_TEMP_END();
}
// Flush out any additional sections
@@ -479,6 +554,95 @@ infoFormatSet(Info *const this, const unsigned int format)
FUNCTION_TEST_RETURN_VOID();
}
FN_EXTERN const CipherSpec *
infoCipherSpec(const Info *const this)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(INFO, this);
FUNCTION_TEST_END();
ASSERT(this != NULL);
if (this->cipherSpecNone != NULL)
FUNCTION_TEST_RETURN_CONST(CIPHER_SPEC, this->cipherSpecNone);
FUNCTION_TEST_RETURN_CONST(CIPHER_SPEC, cipherSpecMapGet(infoCipherSpecMap(this), CIPHER_SPEC_MAP_ID_DEFAULT_STR));
}
/**********************************************************************************************************************************/
FN_EXTERN void
infoCipherSpecMapSet(Info *const this, const CipherSpecMap *const cipherSpecMap)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(INFO, this);
FUNCTION_TEST_PARAM(CIPHER_SPEC_MAP, cipherSpecMap);
FUNCTION_TEST_END();
FUNCTION_AUDIT_IF(memContextCurrent() != objMemContext(this)); // Do not audit calls from within the object
ASSERT(this != NULL);
MEM_CONTEXT_OBJ_BEGIN(this)
{
// NULL becomes an empty map so the getter never returns NULL
this->pub.cipherSpecMap = cipherSpecMap == NULL ? cipherSpecMapNew() : cipherSpecMapDup(cipherSpecMap);
// The default key is looked up on demand, so only a caller that asks for one key errors when there is no default
this->cipherSpecNone = cipherSpecMapSize(this->pub.cipherSpecMap) == 0 ? cipherSpecNewNone() : NULL;
}
MEM_CONTEXT_OBJ_END();
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN void
infoCipherSpecAdd(Info *const this, const String *const id, const CipherSpec *const cipherSpec)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(INFO, this);
FUNCTION_TEST_PARAM(STRING, id);
FUNCTION_TEST_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_TEST_END();
FUNCTION_AUDIT_IF(memContextCurrent() != objMemContext(this)); // Do not audit calls from within the object
ASSERT(this != NULL);
ASSERT(id != NULL);
ASSERT(cipherSpec != NULL && cipherSpecType(cipherSpec) != cipherTypeNone);
ASSERT(cipherSpecDigest(cipherSpec) != 0);
MEM_CONTEXT_OBJ_BEGIN(this)
{
CipherSpecMap *const cipherSpecMap = cipherSpecMapDup(this->pub.cipherSpecMap);
cipherSpecMapAdd(cipherSpecMap, id, cipherSpec);
this->pub.cipherSpecMap = cipherSpecMap;
this->cipherSpecNone = NULL;
}
MEM_CONTEXT_OBJ_END();
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN void
infoCipherRotateTimeSet(Info *const this, const time_t cipherRotateTime)
{
FUNCTION_TEST_BEGIN();
FUNCTION_TEST_PARAM(INFO, this);
FUNCTION_TEST_PARAM(TIME, cipherRotateTime);
FUNCTION_TEST_END();
ASSERT(this != NULL);
ASSERT(cipherRotateTime > 0);
this->pub.cipherRotateTime = cipherRotateTime;
FUNCTION_TEST_RETURN_VOID();
}
/**********************************************************************************************************************************/
FN_EXTERN void
infoCipherSpecSet(Info *const this, const CipherSpec *const cipherSpec)
{
@@ -490,14 +654,26 @@ infoCipherSpecSet(Info *const this, const CipherSpec *const cipherSpec)
FUNCTION_AUDIT_IF(memContextCurrent() != objMemContext(this)); // Do not audit calls from within the object
ASSERT(this != NULL);
ASSERT(cipherSpec == NULL || cipherSpecType(cipherSpec) == cipherTypeNone || cipherSpecDigest(cipherSpec) != 0);
MEM_CONTEXT_OBJ_BEGIN(this)
// No keys needed when there is nothing to encrypt
if (cipherSpec == NULL || cipherSpecType(cipherSpec) == cipherTypeNone)
{
// Copy so the caller is free to release what was passed in, and so the getter never returns NULL
this->pub.cipherSpec = cipherSpec == NULL ? cipherSpecNewNone() : cipherSpecDupP(cipherSpec);
infoCipherSpecMapSet(this, NULL);
}
// Else the one key, stored under the default id
else
{
ASSERT(cipherSpecDigest(cipherSpec) != 0);
MEM_CONTEXT_TEMP_BEGIN()
{
CipherSpecMap *const cipherSpecMap = cipherSpecMapNew();
cipherSpecMapAdd(cipherSpecMap, CIPHER_SPEC_MAP_ID_DEFAULT_STR, cipherSpec);
infoCipherSpecMapSet(this, cipherSpecMap);
}
MEM_CONTEXT_TEMP_END();
}
MEM_CONTEXT_OBJ_END();
FUNCTION_TEST_RETURN_VOID();
}

View File

@@ -11,6 +11,7 @@ typedef struct Info Info;
typedef struct InfoSave InfoSave;
#include "common/crypto/spec.h"
#include "common/format/cipherSpecMap.h"
#include "common/ini.h"
#include "common/type/json.h"
#include "storage/storage.h"
@@ -54,7 +55,8 @@ typedef struct InfoPub
{
unsigned int format; // Repository format the file was written with
const String *backrestVersion; // pgBackRest version
const CipherSpec *cipherSpec; // Cipher spec for dependent files
CipherSpecMap *cipherSpecMap; // Cipher keys for dependent files
time_t cipherRotateTime; // Time of the last cipher key rotation
} InfoPub;
// Repository format
@@ -66,17 +68,34 @@ infoFormat(const Info *const this)
FN_EXTERN void infoFormatSet(Info *this, unsigned int format);
// Cipher spec for the files that depend on this one, e.g. the manifest for backup.info. Never NULL, so it can be handed on
// without a check, and none when there is no pass.
FN_INLINE_ALWAYS const CipherSpec *
infoCipherSpec(const Info *const this)
// Cipher keys for the files that require multiple keys, e.g. manifests. Never NULL, empty when there are no keys.
FN_INLINE_ALWAYS const CipherSpecMap *
infoCipherSpecMap(const Info *const this)
{
return THIS_PUB(Info)->cipherSpec;
return THIS_PUB(Info)->cipherSpecMap;
}
// Set cipher spec for dependent files. NULL means they are not encrypted.
// Default cipher spec or none when there are no keys
FN_EXTERN const CipherSpec *infoCipherSpec(const Info *this);
// Set cipher keys for dependent files. NULL means they are not encrypted.
FN_EXTERN void infoCipherSpecMapSet(Info *this, const CipherSpecMap *cipherSpecMap);
// Set a single cipher spec for dependent files under the default id. NULL means they are not encrypted.
FN_EXTERN void infoCipherSpecSet(Info *this, const CipherSpec *cipherSpec);
// Add a cipher key for dependent files under an id and make it current
FN_EXTERN void infoCipherSpecAdd(Info *this, const String *id, const CipherSpec *cipherSpec);
// Time of the last cipher key rotation, zero when none
FN_INLINE_ALWAYS time_t
infoCipherRotateTime(const Info *const this)
{
return THIS_PUB(Info)->cipherRotateTime;
}
FN_EXTERN void infoCipherRotateTimeSet(Info *this, time_t cipherRotateTime);
// pgBackRest version
FN_INLINE_ALWAYS const String *
infoBackrestVersion(const Info *const this)

View File

@@ -10,10 +10,12 @@ Archive Info Handler
#include "common/debug.h"
#include "common/format/cipherBlockFormat.h"
#include "common/format/format.h"
#include "common/ini.h"
#include "common/io/bufferWrite.h"
#include "common/io/io.h"
#include "common/log.h"
#include "common/type/convert.h"
#include "info/infoArchive.h"
#include "info/infoPg.h"
#include "postgres/interface.h"
@@ -58,13 +60,14 @@ infoArchiveNewInternal(void)
/**********************************************************************************************************************************/
FN_EXTERN InfoArchive *
infoArchiveNew(
const unsigned int pgVersion, const uint64_t pgSystemId, const unsigned int format, const CipherSpec *const cipherSpecSub)
const unsigned int pgVersion, const uint64_t pgSystemId, const unsigned int format,
const CipherSpecMap *const cipherSpecMapSub)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(UINT, pgVersion);
FUNCTION_LOG_PARAM(UINT64, pgSystemId);
FUNCTION_LOG_PARAM(UINT, format);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpecSub);
FUNCTION_LOG_PARAM(CIPHER_SPEC_MAP, cipherSpecMapSub);
FUNCTION_LOG_END();
ASSERT(pgVersion > 0 && pgSystemId > 0);
@@ -76,7 +79,8 @@ infoArchiveNew(
this = infoArchiveNewInternal();
// Initialize the pg data
this->pub.infoPg = infoPgNew(infoPgArchive, format, cipherSpecSub);
this->pub.infoPg = infoPgNew(infoPgArchive, format, NULL);
infoCipherSpecMapSet(infoPgInfo(this->pub.infoPg), cipherSpecMapSub);
infoArchivePgSet(this, pgVersion, pgSystemId);
}
OBJ_NEW_END();
@@ -107,6 +111,35 @@ infoArchiveNewLoad(IoRead *const read, const CipherSpec *const cipherSpec)
FUNCTION_LOG_RETURN(INFO_ARCHIVE, this);
}
/**********************************************************************************************************************************/
FN_EXTERN void
infoArchiveCipherRotate(InfoArchive *const this, const CipherSpec *const cipherSpec, const time_t rotateTime)
{
FUNCTION_LOG_BEGIN(logLevelDebug);
FUNCTION_LOG_PARAM(INFO_ARCHIVE, this);
FUNCTION_LOG_PARAM(CIPHER_SPEC, cipherSpec);
FUNCTION_LOG_PARAM(TIME, rotateTime);
FUNCTION_LOG_END();
ASSERT(this != NULL);
ASSERT(infoArchiveFormat(this) >= REPOSITORY_FORMAT_6);
MEM_CONTEXT_TEMP_BEGIN()
{
// The next key id follows the current key id, or is the first key id when there is no current key
const String *const idCurrent = cipherSpecMapIdCurrent(infoArchiveCipherSpecMap(this));
const String *const id = strNewFmt("%u", idCurrent == NULL ? 1 : cvtZToUInt(strZ(idCurrent)) + 1);
Info *const info = infoPgInfo(infoArchivePg(this));
infoCipherSpecAdd(info, id, cipherSpec);
infoCipherRotateTimeSet(info, rotateTime);
}
MEM_CONTEXT_TEMP_END();
FUNCTION_LOG_RETURN_VOID();
}
/**********************************************************************************************************************************/
const String *
infoArchiveIdHistoryMatch(
@@ -339,7 +372,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);
cipherBlockFormatFilterGroupWriteAdd(buffer, ioWriteFilterGroup(write), cipherSpec, infoArchiveFormat(infoArchive));
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(write), cipherSpec, infoArchiveFormat(infoArchive));
infoArchiveSave(infoArchive, write);
// Save the file and make a copy

View File

@@ -30,7 +30,7 @@ STRING_DECLARE(INFO_ARCHIVE_PATH_FILE_COPY_STR);
Constructors
***********************************************************************************************************************************/
FN_EXTERN InfoArchive *infoArchiveNew(
const unsigned int pgVersion, const uint64_t pgSystemId, unsigned int format, const CipherSpec *cipherSpecSub);
const unsigned int pgVersion, const uint64_t pgSystemId, unsigned int format, const CipherSpecMap *cipherSpecMapSub);
// Create new object and load contents from IoRead
FN_EXTERN InfoArchive *infoArchiveNewLoad(IoRead *read, const CipherSpec *cipherSpec);
@@ -67,6 +67,20 @@ infoArchiveCipherSpec(const InfoArchive *const this)
return infoPgCipherSpec(infoArchivePg(this));
}
// Cipher keys for dependent files
FN_INLINE_ALWAYS const CipherSpecMap *
infoArchiveCipherSpecMap(const InfoArchive *const this)
{
return infoCipherSpecMap(infoPgInfo(infoArchivePg(this)));
}
// Time of the last archive key rotation, zero when none
FN_INLINE_ALWAYS time_t
infoArchiveCipherRotateTime(const InfoArchive *const this)
{
return infoCipherRotateTime(infoPgInfo(infoArchivePg(this)));
}
// Repository format
FN_INLINE_ALWAYS unsigned int
infoArchiveFormat(const InfoArchive *const this)
@@ -79,6 +93,10 @@ FN_EXTERN void infoArchiveFormatSet(InfoArchive *this, unsigned int format);
/***********************************************************************************************************************************
Functions
***********************************************************************************************************************************/
// Add a cipher key for WAL at the next key id, make it current, and record the time of the rotation. Key ids are sequential from
// one and are never reused. Id zero is reserved for a key migrated from format 5.
FN_EXTERN void infoArchiveCipherRotate(InfoArchive *this, const CipherSpec *cipherSpec, time_t rotateTime);
// Given a backrest history id and postgres systemId and version, return the archiveId of the best match
FN_EXTERN const String *infoArchiveIdHistoryMatch(
const InfoArchive *this, const unsigned int historyId, const unsigned int pgVersion, const uint64_t pgSystemId);

View File

@@ -938,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);
cipherBlockFormatFilterGroupWriteAdd(buffer, ioWriteFilterGroup(write), cipherSpec, infoBackupFormat(infoBackup));
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(write), cipherSpec, infoBackupFormat(infoBackup));
infoBackupSave(infoBackup, write);
// Save the file and make a copy

View File

@@ -127,6 +127,7 @@ src_pgbackrest = [
'common/exec.c',
'common/fork.c',
'common/format/cipherBlockFormat.c',
'common/format/cipherSpecMap.c',
'common/format/format.c',
'common/ini.c',
'common/io/bufferRead.c',

View File

@@ -377,10 +377,11 @@ unit:
# --------------------------------------------------------------------------------------------------------------------------------
- name: common/format
total: 3
total: 4
coverage:
- common/format/cipherBlockFormat
- common/format/cipherSpecMap
- common/format/format
# --------------------------------------------------------------------------------------------------------------------------------

108
test/spec-rekey.md Normal file
View File

@@ -0,0 +1,108 @@
# Key Rotation
**Working Document!!!** This spec guides development and will be removed before the final commit. Anything durable (user-facing behavior, the format feature table, migration guidance) moves to the user documentation before this file is deleted. The repository format 6 infrastructure this builds on is merged; format 6 carries the SHA-256 key derivation and nothing else, and this branch is what fills it out.
## Goals
- Rotate the archive key, which is the only bulk-data key that never rotates today.
- Give each backup set one key covering both its manifests and its data files, rotating with every full backup.
- Rotate the user passphrase without touching bulk data, when the user asks for it. Later commit.
## Current State (Format 5)
- `repo-cipher-pass` (user-supplied) encrypts `backup.info` and `archive.info`.
- `backup.info` holds one sub-passphrase that encrypts every `backup.manifest` in the stanza.
- Each manifest holds a sub-passphrase that encrypts the data files of its own backup set. A new one is generated per full backup (`src/command/backup/backup.c:244`) and inherited by diff and incr (`src/command/backup/incr.c.inc:149`), so this level already rotates per set and expires with it.
- `archive.info` holds one sub-passphrase that encrypts all WAL across all archive-ids for the life of the stanza. This is the only bulk-data key that never rotates.
- Sub-passphrases are 48 random bytes, base64-encoded to 64 characters (`cipherSpecGen()`, `src/command/stanza/common.c:20`).
## What Format 6 Already Provides
- Each info file and manifest stores the format it was written with, and readers trust the stored value, so a repository holds more than one format at once while older backups and archives expire.
- A stanza is created at a format with `stanza-create --repoN-format` and migrated with `stanza-upgrade --repoN-format`. Once the info files are at format 6 a version that does not support it cannot read the stanza at all, which is the gate that makes the changes below safe.
- Encrypted info files begin with an eight byte plaintext header, `PGBR` plus three format digits plus one more byte, written in place of the OpenSSL `Salted__` magic. `CipherBlockFormat` reads the header and streams the rest through a block cipher built from what the header said.
- A key stores the digest it derives with, so a key outlives the format of the file it is stored in. Keys carried over by migration keep SHA-1; only new keys get SHA-256.
- The eighth byte of the header is reserved, and that byte is where the archive key id goes.
## Key Storage
- At format 6 the `cipher-pass` value in both info files holds every key the file is responsible for rather than the single key it holds today, as an object keyed by the key id.
```
cipher-pass={"1":{"key":"afg...","digest":"sha256"}, ...}
```
- The value is an object rather than the key on its own because the digest has to travel with each key. A flat `{"1":"afg..."}` would mean inferring the digest from the id, which is the inference the stored digest exists to avoid.
- In `archive.info` the id is the sequential key id that the file header names. In `backup.info` it is the full backup label of the set the key belongs to.
- A key inherited from a format 5 migration goes in id `0`: the stanza-wide manifest key in `backup.info`, the stanza-wide archive key in `archive.info`. Format 6 never writes id 0 itself and a backup label cannot collide with it.
- `stanza-create` at format 6 writes a single archive key at id 1. `stanza-upgrade` from format 5 keeps the key it already has at id 0 and adds a new key at id 1, which becomes current, so WAL written before the migration stays readable under the key that wrote it.
- There is no scalar `cipher-pass` at format 6. A stanza created at format 6 has no id 0 entry at all.
- The digest belongs to the entry rather than to the file. An id 0 key derives with SHA-1 and every format 6 key derives with SHA-256, so the scalar `cipher-digest` written by the format 6 infrastructure commit moves into the entries. This also survives a future format that changes the digest again, which a scalar would not.
- Keys are held in memory as a new `CipherSpecMap` in `common/format`, keyed by the id, which is what `CipherBlockFormat` selects from and what the read path packs for the protocol. It lives beside the format rather than in `common/crypto` so the block cipher stays free of repository concepts.
- `Info` holds a `CipherSpecMap` at every format. A format 5 file loads its single scalar key as the one entry, so only the loader and the saver branch on format and nothing above them does.
- Neither info file carries a key id of its own, since both are encrypted with `repo-cipher-pass` and the map holds only the sub-keys for dependent files. Their header stays eight bytes with the `_` marker, so replacing those bytes with `Salted__` still hands the file to `openssl enc` for a user who wants to read it by hand. A format 6 info file needs `-md sha256` where format 5 needed `-md sha1`. WAL is not worth the same treatment: between the id in front and the key map there is no realistic manual path, and there never was much of one.
## Backup Set Key
- Format 6 stores one key per backup set in `backup.info`, keyed by the full backup label, and it encrypts both the set's manifests and its data files. Every full backup generates a fresh entry, so rotation is automatic and needs no option, marker, or age tracking.
- This merges two format 5 levels. The split had no security value, since the data key lives inside a manifest encrypted with the manifest key, so holding the manifest key already unlocked the data transitively. Once the manifest key is per set they are the same key.
- Format 6 manifests drop the `[cipher]` sub-passphrase. Format 5 manifests keep theirs and are read according to their stored format.
- Lookup is exact. A diff or incr label carries its full's label as the prefix before `_`, so the key for any manifest or data file, current or under `backup.history`, comes from parsing the label. No boundary comparison exists anywhere. The prior-manifest inheritance at `src/command/backup/incr.c.inc:149` is no longer needed for format 6 sets, though the format inheritance beside it stays.
- Keying by label rather than by a sequential id is deliberate. `repo-retention-history` keeps manifests under `backup.history` after their entry leaves `[backup:current]`, so an id recorded against the backup entry would disappear while the manifest it unlocks survives. The label is in the path either way.
- The entry is recorded when the label is assigned, before the first manifest copy is saved, so a resumed backup finds its key by the normal lookup with no special case. An entry left by an aborted full is inert and is pruned once no manifest references it.
- Every data key consumer already holds the manifest and reaches the key through `manifestCipherSpec()`. At format 6 that getter is filled in from the `backup.info` entry once the manifest loads, so verify, restore, expire, `repo get`, resume, incr, and `backup/db` are unchanged.
- A migrated stanza keeps its format 5 stanza-wide manifest key as id 0 until the last format 5 manifest expires. Format 5 sets keep reading their data keys from their manifests as always.
## Archive Key
- `archive.info` holds a map of keys. One is current and encrypts new WAL; the rest are retained only so WAL already written can still be read.
- Key ids are sequential from 1 and are never reused. Which one is current is recorded in `cipher-pass-current` beside the map rather than inferred, since the map is keyed by an opaque id and sorts as text, so the highest id is not the last entry once there are ten of them.
- Only the most recent rotation is dated, in a `cipher-pass-rotate` key beside the map, as seconds since the epoch. Rotation compares the current key's age and nothing looks at when a retired key was made, so dating every entry would store what nothing reads.
- Rotation appends a key and makes it current. No existing file is rewritten and nothing else in the repository moves.
### Key id in the file
- The eighth byte of the format header says whether a key id follows. `_` means nothing follows, which is what the info files write since they are encrypted with the user passphrase and there is only ever one of those. `K` means a key id follows immediately: one byte of length, then that many bytes of id, then the salt.
- The id is stored as bytes rather than as a number, so it is whatever the map is keyed by and needs no conversion at either end. Archive ids happen to be decimal text today, but an id that is not a number, e.g. a backup label, costs nothing to support later.
- A single length byte allows 255 bytes of id, which is ample: a backup label is under 40 and a decimal id would have to pass 10^255 to overflow.
- The reader therefore holds back a variable number of bytes rather than a fixed eight. `cipherBlockFormatProcess()` reads the fixed header, then the length byte when the marker is `K`, then that many bytes, growing its held-back size as each part arrives.
- WAL already carries the `Salted__` magic, since `src/command/archive/push/file.c:273` encrypts with the default header, so the fixed part of the header replaces it for free and only the length byte and the id are new. The files that omit the magic to save space are bundled and block-incremental backup files, and those are keyed by label, so they keep no header at all.
- The key id in the file is what makes rotation need no coordination with running archivers. A pusher still holding a previous `archive.info` keeps writing under the previous key, and the file records which key that was, so a reader finds it without guessing. There is no boundary to compare against and no trial decryption anywhere in the design.
### Reading
- archive-get caches one cipher spec per repo (`src/command/archive/get/get.c:419`) and packs it into the per-file protocol payload (`src/command/archive/get/get.c:927`). The id is not known until the file is open, so the key map goes down to the local process instead and `CipherBlockFormat` selects from it. verify (`src/command/verify/verify.c:859`) and restore's timeline read (`src/command/restore/timeline.c:97`) change the same way.
- The payload is per file, so the whole map repeats for every segment requested. That is accepted: there is no easy way to move the payload to job level today, and a healthy repository has only a few active keys. Trimming the map by some heuristic before packing it is a later option if it ever matters.
- Pruning at expire is what keeps the map small. An unpruned entry is only wasted bytes, so pruning is never required for correctness.
- Archive-id handling is unaffected. archive-push writes only to the current archive-id and archive-get already searches every archive-id matching the current version and system-id (`src/command/archive/get/get.c:430`), and neither cares which key a file used.
### Rotation schedule
- A new option, `repo-cipher-rotate`, gives the rotation period as a time and defaults to 90 days, with a minimum of 15 days. Setting it to the max value effectively disables rotation. It is repo indexed and applies only to the archive key: the backup set key rotates with every full backup, and the user passphrase is user-supplied so it cannot rotate on its own.
- Expire performs the rotation, since it writes `archive.info` on every run and runs regularly, typically daily through backup's expire phase. When the current key is older than the period, expire appends a new key and makes it current. Rotation latency is bounded by expire cadence, which is negligible against a period measured in weeks. If expire never runs then neither does rotation.
- A dry run logs the rotation but does not save it. At format 5 the option is ignored, with a warning when it is not the default, since there is only the one key.
- `stanza-create` at format 6 and a migration to format 6 both add their key through rotation, which assigns the next id and sets `cipher-pass-rotate`, so the clock starts there. The next id is the current id plus one, or 1 when there is no current key.
## Key Pruning at Expire
- Archive: a key is dropped once no WAL that references it remains. Expire already removes WAL, so it knows when the last file for a key is gone. The current key is never dropped.
- Backup set: an entry serves exactly one set, so it is dropped when the last manifest of that set is removed, current or under `backup.history`. Expire performs both removals. The entry for the newest set is never dropped.
- Both happen in expire's existing info file rewrites under the backup lock, so no additional locking is required.
## Info File Write Coordination
- Every info file writer, backup and expire for `backup.info`, expire for `archive.info`, and the stanza commands for both, requires the backup lock and sets `lock-remote-required` (`build/config.yaml`), so the repository host's lock directory is the single serialization point regardless of which host each command runs on.
- archive-push and archive-get never write `archive.info`, so rotation needs no coordination with them beyond the atomic info save. A reader sees the old file or the new one and both are valid, per the running-archiver rule above.
- Backup used to resave `archive.info` at completion only to freshen its timestamp so object-store lifecycle settings do not remove it early. That resave moved to expire, which saves `archive.info` on every run that is not a dry run, so backup no longer writes `archive.info`. With `expire-auto` disabled the timestamp is freshened only when expire is run separately.
- With directly-reached storage (object stores, shared NFS) and writers invoked from several hosts there is no common lock point. This predates this work and applies to backup and expire today. The documented practice remains to run backup and expire against a given repo from one place.
## Later Commits
- User passphrase rotation. `repo-cipher-pass` wraps only the info files, so rotating it rewrites four small objects (`backup.info`, `archive.info`, and their copies) and never touches bulk data. The plan is a config-first flow: set `repo-cipher-pass` to the new passphrase and `repo-cipher-pass-old` to the old one, then run a command that converges each info file. Every command retries an info file load with the old passphrase and warns when the fallback is used, so config can be updated across hosts non-atomically.
- A `stanza-rekey` command to drive that convergence and to force an archive key rotation on demand. It is repo-only by design, with no cluster access, so keys can be rotated during incident response with the cluster down or unreachable. That is why it is not a flag on `stanza-upgrade`, which requires cluster access for `pgValidate()`.
## Documentation Plan
- The features per format version table in the `repo-format` option reference is a stub today. This work fills in the format 6 entries.
- The `repo-cipher-rotate` option reference, and the `stanza-rekey` command reference when that lands.
- Release notes announce archive key rotation and the per backup set key.

View File

@@ -152,7 +152,7 @@ hrnInfoPut(const Storage *const storage, const char *const file, const char *con
// If a file with a header
if (param.header)
{
cipherBlockFormatFilterGroupWriteAdd(encrypted, ioWriteFilterGroup(write), param.cipherSpec, param.format);
cipherBlockFormatFilterGroupWriteAddP(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
@@ -195,7 +195,7 @@ harnessInfoEncrypt(const Buffer *const content, const CipherSpec *const cipherSp
if (param.format == 0)
cipherBlockFilterGroupAdd(ioWriteFilterGroup(write), cipherModeEncrypt, cipherSpec);
else
cipherBlockFormatFilterGroupWriteAdd(result, ioWriteFilterGroup(write), cipherSpec, param.format);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(write), cipherSpec, param.format);
ioWriteOpen(write);
ioWrite(write, content);

View File

@@ -1,6 +1,7 @@
/***********************************************************************************************************************************
Test Archive Get Command
***********************************************************************************************************************************/
#include "common/io/bufferWrite.h"
#include "common/io/fdRead.h"
#include "common/io/fdWrite.h"
@@ -1095,6 +1096,60 @@ testRun(void)
TEST_RESULT_UINT(storageInfoP(storagePg(), STRDEF("pg_wal/RECOVERYXLOG")).size, 16 * 1024 * 1024, "check size");
TEST_STORAGE_LIST(storagePgWrite(), "pg_wal", "RECOVERYXLOG\n", .remove = true);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("get encrypted format 6 WAL segment");
HRN_INFO_PUT(
storageRepoIdxWrite(1), INFO_ARCHIVE_PATH_FILE,
"[cipher]\n"
"cipher-pass={\"1\":{\"digest\":\"sha256\",\"key\":\"" TEST_CIPHER_PASS_ARCHIVE "\"}}\n"
"cipher-pass-current=\"1\"\n"
"\n"
"[db]\n"
"db-id=1\n"
"\n"
"[db:history]\n"
"1={\"db-id\":" HRN_PG_SYSTEMID_10_Z ",\"db-version\":\"10\"}",
.format = REPOSITORY_FORMAT_6, .header = true, .cipherSpec = TEST_CIPHER_SPEC);
// Store the WAL as format 6 with the key id in the header
Buffer *const walFormat6 = bufNew(0);
IoWrite *const walFormat6Write = ioBufferWriteNew(walFormat6);
ioFilterGroupAdd(ioWriteFilterGroup(walFormat6Write), compressFilterP(compressTypeGz, 1));
cipherBlockFormatFilterGroupWriteAddP(
ioWriteFilterGroup(walFormat6Write), TEST_CIPHER_SPEC_PASS(TEST_CIPHER_PASS_ARCHIVE), REPOSITORY_FORMAT_6,
.keyId = STRDEF("1"));
ioWriteOpen(walFormat6Write);
ioWrite(walFormat6Write, buffer);
ioWriteClose(walFormat6Write);
TEST_RESULT_STR_Z(strNewZN((const char *)bufPtrConst(walFormat6), 8), "PGBR006K", "header contains a key id");
HRN_STORAGE_PUT(
storageRepoIdxWrite(1),
STORAGE_REPO_ARCHIVE "/10-1/01ABCDEF01ABCDEF01ABCDEF-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa.gz", walFormat6);
TEST_RESULT_INT(cmdArchiveGet(), 0, "get");
TEST_RESULT_LOG(
"P00 WARN: repo1: [FileMissingError] unable to load info file '" TEST_PATH "/repo-bogus/archive/test1/archive.info'"
" or '" TEST_PATH "/repo-bogus/archive/test1/archive.info.copy':\n"
" FileMissingError: unable to open missing file '" TEST_PATH "/repo-bogus/archive/test1/archive.info'"
" for read\n"
" FileMissingError: unable to open missing file '" TEST_PATH "/repo-bogus/archive/test1/archive.info.copy'"
" for read\n"
" HINT: archive.info cannot be opened but is required to push/get WAL segments.\n"
" HINT: is archive_command configured correctly in postgresql.conf?\n"
" HINT: has a stanza-create been performed?\n"
" HINT: use --no-archive-check to disable archive checks during backup if you have an alternate"
" archiving scheme.\n"
"P00 INFO: found 01ABCDEF01ABCDEF01ABCDEF in the repo2: 10-1 archive");
TEST_RESULT_UINT(storageInfoP(storagePg(), STRDEF("pg_wal/RECOVERYXLOG")).size, 16 * 1024 * 1024, "check size");
TEST_STORAGE_LIST(storagePgWrite(), "pg_wal", "RECOVERYXLOG\n", .remove = true);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("repo1 has info but bad permissions");

View File

@@ -567,6 +567,39 @@ testRun(void)
storageTest, zNewFmt("repo3/archive/test/11-1/0000000100000001/000000010000000100000002-%s", walBuffer2Sha1),
.remove = true, .comment = "check repo3 for WAL file then remove");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("encrypted format 6 WAL segment begins with the format header");
HRN_INFO_PUT(
storageRepoIdxWrite(0), INFO_ARCHIVE_PATH_FILE,
"[cipher]\n"
"cipher-pass={\"1\":{\"digest\":\"sha256\",\"key\":\"badsubpassphrase\"}}\n"
"cipher-pass-current=\"1\"\n"
"\n"
"[db]\n"
"db-id=1\n"
"\n"
"[db:history]\n"
"1={\"db-id\":" HRN_PG_SYSTEMID_11_Z ",\"db-version\":\"11\"}",
.format = REPOSITORY_FORMAT_6, .header = true, .cipherSpec = TEST_CIPHER_SPEC_PASS("badpassphrase"));
TEST_RESULT_VOID(cmdArchivePush(), "push the WAL segment");
TEST_RESULT_LOG("P00 INFO: pushed WAL file '000000010000000100000002' to the archive");
const Buffer *const walFormat6 = storageGetP(
storageNewReadP(
storageTest,
strNewFmt("repo2/archive/test/11-1/0000000100000001/000000010000000100000002-%s", walBuffer2Sha1)));
TEST_RESULT_STR_Z(strNewZN((const char *)bufPtrConst(walFormat6), 8), "PGBR006K", "header contains the format");
TEST_RESULT_UINT(bufPtrConst(walFormat6)[8], 1, "key id is one byte");
TEST_RESULT_STR_Z(strNewZN((const char *)bufPtrConst(walFormat6) + 9, 1), "1", "header contains the key id");
HRN_STORAGE_REMOVE(
storageTest, zNewFmt("repo2/archive/test/11-1/0000000100000001/000000010000000100000002-%s", walBuffer2Sha1));
HRN_STORAGE_REMOVE(
storageTest, zNewFmt("repo3/archive/test/11-1/0000000100000001/000000010000000100000002-%s", walBuffer2Sha1));
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("write error on one repo but other repo succeeds");

View File

@@ -2798,27 +2798,11 @@ testRun(void)
// Remove halted backup so there's no resume
HRN_STORAGE_PATH_REMOVE(storageRepoWrite(), STORAGE_REPO_BACKUP "/20191016-042640F", .recurse = true);
// Set archive.info/copy to an older timestamp so we can be sure it was updated as part of backup
time_t archiveInfoOldTimestamp = 967746268;
HRN_STORAGE_TIME(storageRepo(), INFO_ARCHIVE_PATH_FILE, archiveInfoOldTimestamp);
HRN_STORAGE_TIME(storageRepo(), INFO_ARCHIVE_PATH_FILE_COPY, archiveInfoOldTimestamp);
// Get a copy of archive.info
const String *archiveInfoContent = strNewBuf(storageGetP(storageNewReadP(storageRepo(), INFO_ARCHIVE_PATH_FILE_STR)));
// Run backup
hrnBackupPqScriptP(
PG_VERSION_96, backupTimeStart, .backupStandby = true, .walCompressType = compressTypeGz, .startFast = true);
TEST_RESULT_VOID(hrnCmdBackup(), "backup");
// Check archive.info/copy timestamp was updated but contents were not
TEST_RESULT_INT_NE(
storageInfoP(storageRepo(), INFO_ARCHIVE_PATH_FILE_STR).timeModified, archiveInfoOldTimestamp, "time updated");
TEST_STORAGE_GET(storageRepo(), INFO_ARCHIVE_PATH_FILE, strZ(archiveInfoContent));
TEST_RESULT_INT_NE(
storageInfoP(storageRepo(), INFO_ARCHIVE_PATH_FILE_COPY_STR).timeModified, archiveInfoOldTimestamp, "time updated");
TEST_STORAGE_GET(storageRepo(), INFO_ARCHIVE_PATH_FILE_COPY, strZ(archiveInfoContent));
// Set log level back to detail
harnessLogLevelSet(logLevelDetail);

View File

@@ -1110,11 +1110,19 @@ testRun(void)
hrnCfgArgRawZ(argList, cfgOptRepo, "1");
HRN_CFG_LOAD(cfgCmdExpire, argList);
// Set archive.info to an older timestamp so we can be sure it was updated as part of expire
const time_t archiveInfoOldTimestamp = 967746268;
HRN_STORAGE_TIME(storageRepo(), INFO_ARCHIVE_PATH_FILE, archiveInfoOldTimestamp);
TEST_RESULT_VOID(cmdExpire(), "expire last backup in archive sub path and remove sub path");
TEST_RESULT_BOOL(
storagePathExistsP(storageRepo(), STRDEF(STORAGE_REPO_ARCHIVE "/9.4-1/0000000100000000")), false,
"archive sub path removed repo1");
// Check archive.info timestamp was updated
TEST_RESULT_INT_NE(
storageInfoP(storageRepo(), INFO_ARCHIVE_PATH_FILE_STR).timeModified, archiveInfoOldTimestamp, "time updated");
TEST_RESULT_LOG(
"P00 INFO: repo1: expire full backup 20181119-152138F\n"
"P00 INFO: repo1: remove expired backup 20181119-152138F\n"
@@ -1145,10 +1153,13 @@ testRun(void)
storageTest, TEST_PATH "/repo/backup/db/" INFO_BACKUP_FILE INFO_COPY_EXT,
TEST_PATH "/repo/backup/db/" INFO_BACKUP_FILE INFO_COPY_EXT ".save");
// Rename archive.info file on repo2 to cause error
// Rename archive.info files on repo2 to cause error
HRN_STORAGE_MOVE(
storageTest, TEST_PATH "/repo2/archive/db/" INFO_ARCHIVE_FILE,
TEST_PATH "/repo2/archive/db/" INFO_ARCHIVE_FILE ".save");
HRN_STORAGE_MOVE(
storageTest, TEST_PATH "/repo2/archive/db/" INFO_ARCHIVE_FILE INFO_COPY_EXT,
TEST_PATH "/repo2/archive/db/" INFO_ARCHIVE_FILE INFO_COPY_EXT ".save");
// Configure dry-run
argList2 = strLstDup(argList);
@@ -1179,8 +1190,11 @@ testRun(void)
" HINT: use --no-archive-check to disable archive checks during backup if you have an alternate"
" archiving scheme.");
// Restore saved archive.info file
// Restore saved archive.info files
HRN_STORAGE_MOVE(storageTest, "repo2/archive/db/" INFO_ARCHIVE_FILE ".save", "repo2/archive/db/" INFO_ARCHIVE_FILE);
HRN_STORAGE_MOVE(
storageTest, "repo2/archive/db/" INFO_ARCHIVE_FILE INFO_COPY_EXT ".save",
"repo2/archive/db/" INFO_ARCHIVE_FILE INFO_COPY_EXT);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("expire command - multi-repo, continue to next repo after error");
@@ -1220,10 +1234,10 @@ testRun(void)
TEST_RESULT_VOID(cmdExpire(), "expire (dry-run) - log expired backups and archive path to remove");
TEST_STORAGE_LIST(
storageRepo(), STORAGE_REPO_ARCHIVE, "10-2/\n9.4-1/\narchive.info\n", .noRecurse = true,
storageRepo(), STORAGE_REPO_ARCHIVE, "10-2/\n9.4-1/\narchive.info\narchive.info.copy\n", .noRecurse = true,
.comment = "repo1: 9.4-1 archive path not removed");
TEST_STORAGE_LIST(
storageRepoIdx(1), STORAGE_REPO_ARCHIVE, "10-2/\n9.4-1/\narchive.info\n", .noRecurse = true,
storageRepoIdx(1), STORAGE_REPO_ARCHIVE, "10-2/\n9.4-1/\narchive.info\narchive.info.copy\n", .noRecurse = true,
.comment = "repo2: 9.4-1 archive path not removed");
TEST_STORAGE_LIST(
storageRepoIdx(1), STORAGE_REPO_ARCHIVE "/9.4-1/",
@@ -1289,10 +1303,10 @@ testRun(void)
TEST_RESULT_VOID(cmdExpire(), "expire backups and remove archive path");
TEST_STORAGE_LIST(
storageRepo(), STORAGE_REPO_ARCHIVE, "10-2/\narchive.info\n", .noRecurse = true,
storageRepo(), STORAGE_REPO_ARCHIVE, "10-2/\narchive.info\narchive.info.copy\n", .noRecurse = true,
.comment = "repo1: retention-archive-type=full so 9.4-1 archive path removed");
TEST_STORAGE_LIST(
storageRepoIdx(1), STORAGE_REPO_ARCHIVE, "10-2/\n9.4-1/\narchive.info\n", .noRecurse = true,
storageRepoIdx(1), STORAGE_REPO_ARCHIVE, "10-2/\n9.4-1/\narchive.info\narchive.info.copy\n", .noRecurse = true,
.comment = "repo2: retention-archive-type=diff so 9.4-1 archive path not removed");
TEST_STORAGE_LIST(
storageRepoIdx(1), STORAGE_REPO_ARCHIVE "/9.4-1/",
@@ -2710,6 +2724,104 @@ testRun(void)
storageInfoP(storageRepoIdx(1), STRDEF(STORAGE_REPO_BACKUP "/latest")).linkDestination, STRDEF("20181119-152850F"),
"latest link updated, repo2");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("archive key rotation set at format 5");
argList = strLstDup(argListAvoidWarn);
hrnCfgArgRawZ(argList, cfgOptRepo, "2");
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 2, TEST_PATH "/repo2");
hrnCfgArgKeyRawZ(argList, cfgOptRepoRetentionFull, 2, "1");
hrnCfgArgKeyRawStrId(argList, cfgOptRepoCipherType, 2, cipherTypeAes256Cbc);
hrnCfgArgKeyRawZ(argList, cfgOptRepoCipherRotate, 2, "30d");
HRN_CFG_LOAD(cfgCmdExpire, argList);
TEST_RESULT_VOID(cmdExpire(), "expire");
TEST_RESULT_LOG(
"P00 DETAIL: repo2: 12-2 archive retention on backup 20181119-152850F, start = 000000010000000000000002\n"
"P00 INFO: repo2: 12-2 no archive to remove\n"
"P00 WARN: option 'repo2-cipher-rotate' requires repository format 6 or higher");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("archive key not due for rotation");
const time_t timeRotate = time(NULL);
#define TEST_ARCHIVE_INFO_FORMAT_6(rotateTime) \
zNewFmt( \
"[cipher]\n" \
"cipher-pass={\"1\":{\"digest\":\"sha256\",\"key\":\"" TEST_CIPHER_PASS_ARCHIVE "\"}}\n" \
"cipher-pass-current=\"1\"\n" \
"cipher-pass-rotate=%" PRId64 "\n" \
"\n" \
"[db]\n" \
"db-id=2\n" \
"db-system-id=6626363367545678089\n" \
"db-version=\"12\"\n" \
"\n" \
"[db:history]\n" \
"1={\"db-id\":6625592122879095702,\"db-version\":\"9.4\"}\n" \
"2={\"db-id\":6626363367545678089,\"db-version\":\"12\"}", \
(int64_t)(rotateTime))
HRN_INFO_PUT(
storageRepoIdxWrite(1), INFO_ARCHIVE_PATH_FILE, TEST_ARCHIVE_INFO_FORMAT_6(timeRotate - 29 * SEC_PER_DAY),
.format = REPOSITORY_FORMAT_6, .header = true, .cipherSpec = TEST_CIPHER_SPEC);
TEST_RESULT_VOID(cmdExpire(), "expire");
TEST_RESULT_LOG(
"P00 DETAIL: repo2: 12-2 archive retention on backup 20181119-152850F, start = 000000010000000000000002\n"
"P00 INFO: repo2: 12-2 no archive to remove");
InfoArchive *infoArchive = NULL;
TEST_ASSIGN(
infoArchive, infoArchiveLoadFile(storageRepoIdx(1), INFO_ARCHIVE_PATH_FILE_STR, TEST_CIPHER_SPEC), "load archive info");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(infoArchiveCipherSpecMap(infoArchive)), "1", "current key not rotated");
TEST_RESULT_INT(infoArchiveCipherRotateTime(infoArchive), timeRotate - 29 * SEC_PER_DAY, "rotation time unchanged");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("archive key due for rotation, dry run");
HRN_INFO_PUT(
storageRepoIdxWrite(1), INFO_ARCHIVE_PATH_FILE, TEST_ARCHIVE_INFO_FORMAT_6(timeRotate - 31 * SEC_PER_DAY),
.format = REPOSITORY_FORMAT_6, .header = true, .cipherSpec = TEST_CIPHER_SPEC);
StringList *argListDryRun = strLstDup(argList);
hrnCfgArgRawBool(argListDryRun, cfgOptDryRun, true);
HRN_CFG_LOAD(cfgCmdExpire, argListDryRun);
TEST_RESULT_VOID(cmdExpire(), "expire");
TEST_RESULT_LOG(
"P00 DETAIL: [DRY-RUN] repo2: 12-2 archive retention on backup 20181119-152850F, start = 000000010000000000000002\n"
"P00 INFO: [DRY-RUN] repo2: 12-2 no archive to remove\n"
"P00 INFO: [DRY-RUN] repo2: rotate archive key to id 2");
TEST_ASSIGN(
infoArchive, infoArchiveLoadFile(storageRepoIdx(1), INFO_ARCHIVE_PATH_FILE_STR, TEST_CIPHER_SPEC), "load archive info");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(infoArchiveCipherSpecMap(infoArchive)), "1", "current key not rotated");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("archive key due for rotation");
HRN_CFG_LOAD(cfgCmdExpire, argList);
TEST_RESULT_VOID(cmdExpire(), "expire");
TEST_RESULT_LOG(
"P00 DETAIL: repo2: 12-2 archive retention on backup 20181119-152850F, start = 000000010000000000000002\n"
"P00 INFO: repo2: 12-2 no archive to remove\n"
"P00 INFO: repo2: rotate archive key to id 2");
TEST_ASSIGN(
infoArchive, infoArchiveLoadFile(storageRepoIdx(1), INFO_ARCHIVE_PATH_FILE_STR, TEST_CIPHER_SPEC), "load archive info");
const CipherSpecMap *const cipherSpecMapRotate = infoArchiveCipherSpecMap(infoArchive);
TEST_RESULT_UINT(cipherSpecMapSize(cipherSpecMapRotate), 2, "prior key kept");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(cipherSpecMapRotate), "2", "new key is current");
TEST_RESULT_UINT(
cipherSpecDigest(cipherSpecMapGet(cipherSpecMapRotate, STRDEF("2"))), hashTypeSha256, "new key derives with sha256");
TEST_RESULT_BOOL(infoArchiveCipherRotateTime(infoArchive) >= timeRotate, true, "rotation time updated");
// Cleanup
hrnCfgEnvKeyRemoveRaw(cfgOptRepoCipherPass, 2);
harnessLogLevelReset();
@@ -3061,6 +3173,17 @@ testRun(void)
"1={\"db-catalog-version\":202506291,\"db-control-version\":1800,\"db-system-id\":7577015877005525116"
",\"db-version\":\"18\"}\n");
// Create archive info
HRN_INFO_PUT(
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE,
"[db]\n"
"db-id=1\n"
"db-system-id=7577015877005525116\n"
"db-version=\"18\"\n"
"\n"
"[db:history]\n"
"1={\"db-id\":7577015877005525116,\"db-version\":\"18\"}");
TEST_RESULT_VOID(cmdExpire(), "no backups to expire using --oldest");
TEST_RESULT_LOG(
"P00 WARN: repo1: expire oldest requested but no eligible full backup to expire");
@@ -3183,17 +3306,6 @@ testRun(void)
HRN_STORAGE_PUT_EMPTY(storageRepoWrite(), STORAGE_REPO_BACKUP "/20251127-101503F/" BACKUP_MANIFEST_FILE);
HRN_STORAGE_PUT_EMPTY(storageRepoWrite(), STORAGE_REPO_BACKUP "/20251127-101503F_20251127-101508D/" BACKUP_MANIFEST_FILE);
// Create archive info
HRN_INFO_PUT(
storageRepoWrite(), INFO_ARCHIVE_PATH_FILE,
"[db]\n"
"db-id=1\n"
"db-system-id=7577015877005525116\n"
"db-version=\"18\"\n"
"\n"
"[db:history]\n"
"1={\"db-id\":7577015877005525116,\"db-version\":\"18\"}");
// Create archive directories and generate archive
archiveGenerate(storageRepoWrite(), STORAGE_REPO_ARCHIVE, 1, 32, "18-1", "0000000100000000");

View File

@@ -1,6 +1,7 @@
/***********************************************************************************************************************************
Test Repo Commands
***********************************************************************************************************************************/
#include "common/format/cipherBlockFormat.h"
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "storage/posix/storage.h"
@@ -620,6 +621,51 @@ testRun(void)
TEST_RESULT_INT(storageGetProcess(ioBufferWriteNew(writeBuffer)), 0, "get");
TEST_RESULT_BOOL(bufEq(writeBuffer, archiveFileBuffer), true, "get matches put");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("get encrypted format 6 WAL archive file");
// Format 6 archive.info with the migrated key at id 0 and the current key at id 1
HRN_INFO_PUT(
storageFixture, STORAGE_PATH_ARCHIVE "/test/" INFO_ARCHIVE_FILE,
"[cipher]\n"
"cipher-pass={\"0\":{\"digest\":\"sha1\",\"key\":\"custom\"},\"1\":{\"digest\":\"sha256\",\"key\":\"custom6\"}}\n"
"cipher-pass-current=\"1\"\n"
"\n"
"[db]\n"
"db-id=1\n"
"db-system-id=6846378200844646865\n"
"db-version=\"12\"\n"
"\n"
"[db:history]\n"
"1={\"db-id\":6846378200844646865,\"db-version\":\"12\"}",
.format = REPOSITORY_FORMAT_6, .header = true, .cipherSpec = TEST_CIPHER_SPEC);
// Store the WAL as format 6 with the key id in the header
Buffer *const archiveFileBuffer6 = bufNew(0);
IoWrite *const archiveFileWrite6 = ioBufferWriteNew(archiveFileBuffer6);
cipherBlockFormatFilterGroupWriteAddP(
ioWriteFilterGroup(archiveFileWrite6), TEST_CIPHER_SPEC_PASS("custom6"), REPOSITORY_FORMAT_6, .keyId = STRDEF("1"));
ioWriteOpen(archiveFileWrite6);
ioWrite(archiveFileWrite6, archiveFileBuffer);
ioWriteClose(archiveFileWrite6);
HRN_STORAGE_PUT(
storageFixture, STORAGE_PATH_ARCHIVE "/test/12-1/000000010000000100000002-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
archiveFileBuffer6);
argList = strLstNew();
hrnCfgArgRawZ(argList, cfgOptRepoPath, TEST_PATH "/repo");
hrnCfgArgRawStrId(argList, cfgOptRepoCipherType, cipherTypeAes256Cbc);
strLstAddFmt(
argList, "%s/repo/" STORAGE_PATH_ARCHIVE "/test/12-1/000000010000000100000002-bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
TEST_PATH);
HRN_CFG_LOAD(cfgCmdRepoGet, argList);
writeBuffer = bufNew(0);
TEST_RESULT_INT(storageGetProcess(ioBufferWriteNew(writeBuffer)), 0, "get");
TEST_RESULT_BOOL(bufEq(writeBuffer, archiveFileBuffer), true, "get matches put");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("get encrypted backup.manifest");

View File

@@ -2072,44 +2072,44 @@ testRun(void)
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_11, 1, 0xA1, NULL, CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow current timeline because of version");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_11, 1, 0xA1, STRDEF("latest"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow latest timeline as requested");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 1, 0xA1, NULL, CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow latest timeline because of version");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 1, 0xA1, STRDEF("current"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow current timeline as requested");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 1, 0xA1, STRDEF("1"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow requested timeline (same as current)");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 0x10, 0xA1, STRDEF("0x10"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow requested hex timeline (same as current)");
TEST_ERROR(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 0x10, 0xA1, STRDEF("bogus"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
DbMismatchError, "invalid target timeline 'bogus'");
HRN_STORAGE_PUT_Z(storageTest, "repo/archive/test1/17-1/00000009.history", "8");
TEST_ERROR(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 8, 0xA1, STRDEF("9"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
FormatError,
"invalid timeline '9' at '" TEST_PATH "/repo/archive/test1/17-1/00000009.history':"
" invalid history line format '8'");
@@ -2122,27 +2122,27 @@ testRun(void)
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 10, 0x4FFFFFF, NULL, CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow current timeline");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 9, 0x4FFFFFF, NULL, CFGOPTVAL_RESTORE_TYPE_IMMEDIATE,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow current timeline (based on type immediate)");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 9, 0x4FFFFFF, NULL, CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"follow latest timeline");
TEST_RESULT_VOID(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 9, 0x4FFFFFF, STRDEF("10"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
"target timeline found");
TEST_ERROR(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 9, 0x6000000, STRDEF("10"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
DbMismatchError,
"target timeline A forked from backup timeline 9 at 0/5000000 which is before backup lsn of 0/6000000\n"
"HINT: was the target timeline created by accidentally promoting a standby?\n"
@@ -2156,7 +2156,7 @@ testRun(void)
TEST_ERROR(
timelineVerify(
storageRepoIdx(0), STRDEF("17-1"), PG_VERSION_12, 6, 0x4FFFFFF, STRDEF("11"), CFGOPTVAL_RESTORE_TYPE_DEFAULT,
cipherSpecNewNone()),
cipherSpecMapNew()),
DbMismatchError, "backup timeline 6, lsn 0/4ffffff is not in the history of target timeline B\n"
"HINT: was the target timeline created by promoting from a timeline < latest?");
}
@@ -2334,9 +2334,13 @@ testRun(void)
TEST_CIPHER_PASS_MANIFEST "\"\n\n" TEST_RESTORE_BACKUP_INFO_DB, .cipherSpec = TEST_CIPHER_SPEC);
// Write archive.info to the encrypted repo
InfoArchive *infoArchive = infoArchiveNew(
PG_VERSION_11, 6569239123849665679, REPOSITORY_FORMAT_DEFAULT,
CipherSpecMap *const cipherSpecMapArchive = cipherSpecMapNew();
cipherSpecMapAdd(
cipherSpecMapArchive, CIPHER_SPEC_MAP_ID_DEFAULT_STR,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS_ARCHIVE), .digest = hashTypeSha1));
InfoArchive *infoArchive = infoArchiveNew(
PG_VERSION_11, 6569239123849665679, REPOSITORY_FORMAT_DEFAULT, cipherSpecMapArchive);
infoArchiveSaveFile(
infoArchive, storageRepoIdxWrite(1), INFO_ARCHIVE_PATH_FILE_STR,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF(TEST_CIPHER_PASS)));

View File

@@ -125,9 +125,12 @@ testRun(void)
hrnCfgArgKeyRawZ(argList, cfgOptRepoFormat, 1, "6");
hrnCfgArgKeyRawZ(argList, cfgOptRepoPath, 4, TEST_PATH "/repo4");
hrnCfgArgKeyRawStrId(argList, cfgOptRepoCipherType, 4, cipherTypeAes256Cbc);
hrnCfgArgKeyRawZ(argList, cfgOptRepoFormat, 4, "6");
hrnCfgEnvKeyRawZ(cfgOptRepoCipherPass, 4, "87654321");
HRN_CFG_LOAD(cfgCmdStanzaCreate, argList);
const time_t timeCreate = time(NULL);
TEST_RESULT_VOID(cmdStanzaCreate(), "stanza create - files already exist on repo1 and both are valid");
TEST_RESULT_LOG(
"P00 INFO: stanza-create for stanza 'db' on repo1\n"
@@ -184,13 +187,27 @@ testRun(void)
TEST_RESULT_UINT(
infoArchiveFormat(infoArchive), REPOSITORY_FORMAT_DEFAULT, "archive info on existing stanza still at default format");
// Confirm other repo encrypted with different password
// Confirm other repo encrypted with different password. Format 6 stores the archive key at the first key id.
TEST_ASSIGN(
infoArchive,
infoArchiveLoadFile(
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");
const CipherSpecMap *const cipherSpecMapCreate = infoArchiveCipherSpecMap(infoArchive);
TEST_RESULT_UINT(cipherSpecMapSize(cipherSpecMapCreate), 1, "one archive key");
TEST_RESULT_STR_Z(cipherSpecMapGetIdx(cipherSpecMapCreate, 0)->id, "1", "archive key stored at id 1");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(cipherSpecMapCreate), "1", "archive key at id 1 is current");
TEST_RESULT_BOOL(
infoArchiveCipherRotateTime(infoArchive) >= timeCreate &&
infoArchiveCipherRotateTime(infoArchive) <= time(NULL),
true, "rotation time is the create time");
TEST_RESULT_UINT(
cipherSpecDigest(cipherSpecMapGet(cipherSpecMapCreate, STRDEF("1"))), hashTypeSha256,
"archive key derives with sha256");
TEST_RESULT_UINT(
bufSize(cipherSpecPass(cipherSpecMapGet(cipherSpecMapCreate, STRDEF("1")))), 64, "archive key is 64 characters");
TEST_ASSIGN(
infoBackup,
@@ -1233,6 +1250,8 @@ testRun(void)
hrnCfgArgKeyRawZ(argList, cfgOptRepoFormat, 1, "6");
HRN_CFG_LOAD(cfgCmdStanzaUpgrade, argList);
const time_t timeMigrate = time(NULL);
TEST_RESULT_VOID(cmdStanzaUpgrade(), "stanza upgrade - format 6 on encrypted repo");
TEST_RESULT_LOG(
"P00 INFO: stanza-upgrade for stanza 'db' on repo1\n"
@@ -1245,6 +1264,22 @@ testRun(void)
"load archive info after migration");
TEST_RESULT_UINT(infoArchiveFormat(infoArchiveMigrate), REPOSITORY_FORMAT_6, "archive info at format 6");
// The migrated key stays at id 0 and a key for new WAL is added at id 1
const CipherSpecMap *const cipherSpecMapMigrate = infoArchiveCipherSpecMap(infoArchiveMigrate);
TEST_RESULT_UINT(cipherSpecMapSize(cipherSpecMapMigrate), 2, "migrated and new archive key");
TEST_RESULT_UINT(
cipherSpecDigest(cipherSpecMapGet(cipherSpecMapMigrate, CIPHER_SPEC_MAP_ID_DEFAULT_STR)), hashTypeSha1,
"migrated key derives with sha1");
TEST_RESULT_UINT(
cipherSpecDigest(cipherSpecMapGet(cipherSpecMapMigrate, STRDEF("1"))), hashTypeSha256,
"new key derives with sha256");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(cipherSpecMapMigrate), "1", "new key is current");
TEST_RESULT_BOOL(
infoArchiveCipherRotateTime(infoArchiveMigrate) >= timeMigrate &&
infoArchiveCipherRotateTime(infoArchiveMigrate) <= time(NULL),
true, "rotation time is the migration time");
StorageRead *const walRead = storageNewReadP(storageRepoIdx(0), STRDEF(TEST_WAL_MIGRATE));
cipherBlockFilterGroupAdd(

View File

@@ -895,7 +895,7 @@ testRun(void)
TEST_RESULT_LOG("");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("encrypted info files at format 6");
TEST_TITLE("encrypted format 6 info files");
HRN_INFO_PUT(
storageRepoWrite(), INFO_BACKUP_PATH_FILE, TEST_NO_CURRENT_BACKUP, .format = REPOSITORY_FORMAT_6, .header = true,
@@ -934,6 +934,13 @@ testRun(void)
// *****************************************************************************************************************************
if (testBegin("verifyFile()"))
{
// Backup files contain no key id, so the key goes under the default id
CipherSpecMap *const cipherSpecMapNone = cipherSpecMapNew();
CipherSpecMap *const cipherSpecMapPass = cipherSpecMapNew();
cipherSpecMapAdd(
cipherSpecMapPass, CIPHER_SPEC_MAP_ID_DEFAULT_STR,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("pass"), .digest = hashTypeSha1));
// Load Parameters
StringList *argList = strLstDup(argListBase);
HRN_CFG_LOAD(cfgCmdVerify, argList);
@@ -944,7 +951,7 @@ testRun(void)
String *filePathName = strNewZ(STORAGE_REPO_ARCHIVE "/testfile");
HRN_STORAGE_PUT_EMPTY(storageRepoWrite(), strZ(filePathName));
TEST_RESULT_UINT(
verifyFile(filePathName, 0, NULL, compressTypeNone, HASH_TYPE_SHA1_ZERO_BUF, 0, cipherSpecNewNone()),
verifyFile(filePathName, 0, NULL, compressTypeNone, HASH_TYPE_SHA1_ZERO_BUF, 0, cipherSpecMapNone, false),
verifyOk, "file ok");
// -------------------------------------------------------------------------------------------------------------------------
@@ -952,7 +959,7 @@ testRun(void)
HRN_STORAGE_PUT_Z(storageRepoWrite(), strZ(filePathName), fileContents);
TEST_RESULT_UINT(
verifyFile(filePathName, 0, NULL, compressTypeNone, fileChecksum, 0, cipherSpecNewNone()),
verifyFile(filePathName, 0, NULL, compressTypeNone, fileChecksum, 0, cipherSpecMapNone, false),
verifySizeInvalid, "file size invalid");
// -------------------------------------------------------------------------------------------------------------------------
@@ -960,7 +967,8 @@ testRun(void)
TEST_RESULT_UINT(
verifyFile(
strNewFmt(STORAGE_REPO_ARCHIVE "/missingFile"), 0, NULL, compressTypeNone, fileChecksum, 0, cipherSpecNewNone()),
strNewFmt(STORAGE_REPO_ARCHIVE "/missingFile"), 0, NULL, compressTypeNone, fileChecksum, 0, cipherSpecMapNone,
false),
verifyFileMissing, "file missing");
// -------------------------------------------------------------------------------------------------------------------------
@@ -976,12 +984,12 @@ testRun(void)
TEST_RESULT_UINT(
verifyFile(
filePathName, 0, NULL, compressTypeGz, fileChecksum, fileSize,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("pass"), .digest = hashTypeSha1)),
cipherSpecMapPass, false),
verifyOk, "file encrypted compressed ok");
TEST_RESULT_UINT(
verifyFile(
filePathName, 0, NULL, compressTypeGz, bufNewDecode(encodingHex, STRDEF("aa")), fileSize,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("pass"), .digest = hashTypeSha1)),
cipherSpecMapPass, false),
verifyChecksumMismatch, "file encrypted compressed checksum mismatch");
}

View File

@@ -51,6 +51,9 @@ testRun(void)
const Buffer *const testPass = BUFSTRDEF(TEST_PASS);
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, testPass);
CipherSpecMap *const keyMapDefault = cipherSpecMapNew();
cipherSpecMapAdd(keyMapDefault, CIPHER_SPEC_MAP_ID_DEFAULT_STR, cipherSpec);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("nothing is added when the repository is not encrypted");
@@ -58,7 +61,7 @@ testRun(void)
IoWrite *plainWrite = ioBufferWriteNew(plain);
TEST_RESULT_VOID(
cipherBlockFormatFilterGroupWriteAdd(plain, ioWriteFilterGroup(plainWrite), cipherSpecNewNone(), REPOSITORY_FORMAT_6),
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(plainWrite), cipherSpecNewNone(), REPOSITORY_FORMAT_6),
"add write filter for no cipher");
ioWriteOpen(plainWrite);
ioWrite(plainWrite, testPlainText);
@@ -77,14 +80,14 @@ testRun(void)
Buffer *headerBuffer = bufNew(0);
IoWrite *headerWrite = ioBufferWriteNew(headerBuffer);
cipherBlockFormatFilterGroupWriteAdd(headerBuffer, ioWriteFilterGroup(headerWrite), cipherSpec, REPOSITORY_FORMAT_6);
cipherBlockFormatFilterGroupWriteAddP(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");
"header contains 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);
@@ -113,7 +116,7 @@ testRun(void)
Buffer *const piecesBuffer = bufNew(0);
IoWrite *const piecesWrite = ioBufferWriteNew(piecesBuffer);
cipherBlockFormatFilterGroupWriteAdd(piecesBuffer, ioWriteFilterGroup(piecesWrite), cipherSpec, REPOSITORY_FORMAT_6);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(piecesWrite), cipherSpec, REPOSITORY_FORMAT_6);
ioWriteOpen(piecesWrite);
ioWrite(piecesWrite, piecesPlainText);
ioWriteClose(piecesWrite);
@@ -165,7 +168,7 @@ testRun(void)
Buffer *magicBuffer = bufNew(0);
IoWrite *magicWrite = ioBufferWriteNew(magicBuffer);
cipherBlockFormatFilterGroupWriteAdd(magicBuffer, ioWriteFilterGroup(magicWrite), cipherSpec, REPOSITORY_FORMAT_5);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(magicWrite), cipherSpec, REPOSITORY_FORMAT_5);
ioWriteOpen(magicWrite);
ioWrite(magicWrite, testPlainText);
ioWriteClose(magicWrite);
@@ -189,12 +192,12 @@ testRun(void)
REPOSITORY_FORMAT_5, "filter reports the format before the header");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("a format given on decrypt must be the one the header names");
TEST_TITLE("a format given on decrypt must be the one the header contains");
IoWrite *const headerMismatch = ioBufferWriteNew(bufNew(0));
ioFilterGroupAdd(
ioWriteFilterGroup(headerMismatch), cipherBlockFormatNewP(cipherSpec, .format = REPOSITORY_FORMAT_5));
ioWriteFilterGroup(headerMismatch), cipherBlockFormatNewP(keyMapDefault, .format = REPOSITORY_FORMAT_5));
ioWriteOpen(headerMismatch);
TEST_ERROR(ioWrite(headerMismatch, headerBuffer), FormatError, "expected repository format 5 but found 6");
@@ -208,7 +211,7 @@ testRun(void)
ioFilterGroupAdd(
ioWriteFilterGroup(headerRead),
cipherBlockFormatNewPack(
ioFilterParamList(cipherBlockFormatNewP(cipherSpec, .format = REPOSITORY_FORMAT_6))));
ioFilterParamList(cipherBlockFormatNewP(keyMapDefault, .format = REPOSITORY_FORMAT_6))));
ioWriteOpen(headerRead);
ioWrite(headerRead, headerBuffer);
ioWriteClose(headerRead);
@@ -266,6 +269,247 @@ testRun(void)
TEST_HEADER_DAMAGE(0, 'X', CryptoError, "cipher header invalid");
#undef TEST_HEADER_DAMAGE
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("read with the key id stored in the header");
const CipherSpec *const keySpecOld = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("oldkey"), .digest = hashTypeSha1);
const CipherSpec *const keySpecNew = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("newkey"));
CipherSpecMap *const keyMap = cipherSpecMapNew();
cipherSpecMapAdd(keyMap, STRDEF(CIPHER_SPEC_MAP_ID_DEFAULT), keySpecOld);
cipherSpecMapAdd(keyMap, STRDEF("7"), keySpecNew);
Buffer *const keyBuffer = bufNew(0);
IoRead *const keyWrite = ioBufferReadNew(testPlainText);
Buffer *const keyPiece = bufNew(4);
// Build the write filters from their param lists, as the remote protocol does
IoFilterGroup *const keyWriteGroup = ioFilterGroupNew();
cipherBlockFormatFilterGroupWriteAddP(keyWriteGroup, keySpecNew, REPOSITORY_FORMAT_6, .keyId = STRDEF("7"));
PackRead *const keyWriteParam = pckReadNew(ioFilterGroupParamAll(keyWriteGroup));
TEST_RESULT_UINT(pckReadStrIdP(keyWriteParam), CIPHER_BLOCK_FILTER_TYPE, "block cipher filter");
ioFilterGroupAdd(ioReadFilterGroup(keyWrite), cipherBlockNewPack(pckReadPackP(keyWriteParam)));
TEST_RESULT_UINT(pckReadStrIdP(keyWriteParam), CIPHER_BLOCK_FORMAT_HEADER_FILTER_TYPE, "header filter");
ioFilterGroupAdd(ioReadFilterGroup(keyWrite), cipherBlockFormatHeaderNewPack(pckReadPackP(keyWriteParam)));
// Take the result in pieces smaller than the header so the header is written in more than one part
ioBufferSizeSet(4);
ioReadOpen(keyWrite);
while (!ioReadEof(keyWrite))
{
bufUsedZero(keyPiece);
ioRead(keyWrite, keyPiece);
bufCat(keyBuffer, keyPiece);
}
ioReadClose(keyWrite);
ioBufferSizeSet(TEST_BUFFER_SIZE);
TEST_RESULT_BOOL(
memcmp(bufPtrConst(keyBuffer), CIPHER_BLOCK_FORMAT_MAGIC "006K\0017", CIPHER_BLOCK_FORMAT_HEADER_SIZE + 2) == 0, true,
"header contains the key after its length");
Buffer *keyResult = bufNew(0);
IoWrite *keyRead = ioBufferWriteNew(keyResult);
cipherBlockFormatFilterGroupReadAddMap(ioWriteFilterGroup(keyRead), keyMap);
ioWriteOpen(keyRead);
ioWrite(keyRead, keyBuffer);
ioWriteClose(keyRead);
TEST_RESULT_STR_Z(strNewBuf(keyResult), TEST_PLAINTEXT, "content decrypted with the key the header contains");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("key id split across input, from a pack");
keyResult = bufNew(0);
keyRead = ioBufferWriteNew(keyResult);
// Build from a pack, as the remote protocol does
ioFilterGroupAdd(
ioWriteFilterGroup(keyRead), cipherBlockFormatNewPack(ioFilterParamList(cipherBlockFormatNewP(keyMap))));
// Split so the length byte and the key id each arrive separately
ioBufferSizeSet(4);
ioWriteOpen(keyRead);
ioWrite(keyRead, BUF(bufPtrConst(keyBuffer), CIPHER_BLOCK_FORMAT_HEADER_SIZE));
ioWrite(keyRead, BUF(bufPtrConst(keyBuffer) + CIPHER_BLOCK_FORMAT_HEADER_SIZE, 1));
ioWrite(keyRead, BUF(bufPtrConst(keyBuffer) + CIPHER_BLOCK_FORMAT_HEADER_SIZE + 1, 1));
ioWrite(
keyRead,
BUF(
bufPtrConst(keyBuffer) + CIPHER_BLOCK_FORMAT_HEADER_SIZE + 2,
bufUsed(keyBuffer) - CIPHER_BLOCK_FORMAT_HEADER_SIZE - 2));
ioWriteClose(keyRead);
ioBufferSizeSet(TEST_BUFFER_SIZE);
TEST_RESULT_STR_Z(strNewBuf(keyResult), TEST_PLAINTEXT, "content decrypted from split input");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("key digest is used, not the digest of the format");
// A key kept by migration derives with SHA-1 even in a file written at a format that derives with SHA-256
Buffer *const digestBuffer = bufNew(0);
IoWrite *const digestWrite = ioBufferWriteNew(digestBuffer);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(digestWrite), keySpecOld, REPOSITORY_FORMAT_5);
ioWriteOpen(digestWrite);
ioWrite(digestWrite, testPlainText);
ioWriteClose(digestWrite);
// Replace the magic with a format 6 header, which leaves content that only the SHA-1 key can decrypt
memcpy(bufPtr(digestBuffer), CIPHER_BLOCK_FORMAT_MAGIC "006_", CIPHER_BLOCK_FORMAT_HEADER_SIZE);
Buffer *const digestResult = bufNew(0);
IoWrite *const digestRead = ioBufferWriteNew(digestResult);
cipherBlockFormatFilterGroupReadAddMap(ioWriteFilterGroup(digestRead), keyMap);
ioWriteOpen(digestRead);
ioWrite(digestRead, digestBuffer);
ioWriteClose(digestRead);
TEST_RESULT_STR_Z(strNewBuf(digestResult), TEST_PLAINTEXT, "content decrypted with the digest of the key");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("file with no key id uses the migrated key");
Buffer *const migratedBuffer = bufNew(0);
IoWrite *const migratedWrite = ioBufferWriteNew(migratedBuffer);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(migratedWrite), keySpecOld, REPOSITORY_FORMAT_5);
ioWriteOpen(migratedWrite);
ioWrite(migratedWrite, testPlainText);
ioWriteClose(migratedWrite);
Buffer *const migratedResult = bufNew(0);
IoWrite *const migratedRead = ioBufferWriteNew(migratedResult);
cipherBlockFormatFilterGroupReadAddMap(ioWriteFilterGroup(migratedRead), keyMap);
ioWriteOpen(migratedRead);
ioWrite(migratedRead, migratedBuffer);
ioWriteClose(migratedRead);
TEST_RESULT_STR_Z(strNewBuf(migratedResult), TEST_PLAINTEXT, "content decrypted with the migrated key");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("key id not in the map");
CipherSpecMap *const keyMapShort = cipherSpecMapNew();
cipherSpecMapAdd(keyMapShort, STRDEF(CIPHER_SPEC_MAP_ID_DEFAULT), keySpecOld);
IoWrite *const keyMissing = ioBufferWriteNew(bufNew(0));
cipherBlockFormatFilterGroupReadAddMap(ioWriteFilterGroup(keyMissing), keyMapShort);
ioWriteOpen(keyMissing);
TEST_ERROR(ioWrite(keyMissing, keyBuffer), CryptoError, "unable to find cipher key '7'");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("key id in a file read with a single key");
IoWrite *const keyUnexpected = ioBufferWriteNew(bufNew(0));
cipherBlockFormatFilterGroupReadAdd(ioWriteFilterGroup(keyUnexpected), keySpecNew);
ioWriteOpen(keyUnexpected);
TEST_ERROR(ioWrite(keyUnexpected, keyBuffer), CryptoError, "unable to find cipher key '7'");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("key id with zero length");
Buffer *const keyEmpty = bufDup(keyBuffer);
bufPtr(keyEmpty)[CIPHER_BLOCK_FORMAT_HEADER_SIZE] = 0;
IoWrite *const keyEmptyRead = ioBufferWriteNew(bufNew(0));
cipherBlockFormatFilterGroupReadAddMap(ioWriteFilterGroup(keyEmptyRead), keyMap);
ioWriteOpen(keyEmptyRead);
TEST_ERROR(ioWrite(keyEmptyRead, keyEmpty), FormatError, "invalid cipher header");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("no filter added when the map is empty");
IoWrite *const keyNone = ioBufferWriteNew(bufNew(0));
TEST_RESULT_PTR_NE(
cipherBlockFormatFilterGroupReadAddMap(ioWriteFilterGroup(keyNone), cipherSpecMapNew()), NULL,
"add read filter for no keys");
TEST_RESULT_UINT(ioFilterGroupSize(ioWriteFilterGroup(keyNone)), 0, "no filter added");
}
// *****************************************************************************************************************************
if (testBegin("cipherSpecMapNew()"))
{
const CipherSpec *const cipherSpec1 = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("key1"), .digest = hashTypeSha256);
const CipherSpec *const cipherSpec2 = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("key2"), .digest = hashTypeSha1);
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("add and get keys");
CipherSpecMap *map = NULL;
TEST_ASSIGN(map, cipherSpecMapNew(), "new map");
TEST_RESULT_UINT(cipherSpecMapSize(map), 0, "no keys");
// Add out of order to check that the map sorts
TEST_RESULT_VOID(cipherSpecMapAdd(map, STRDEF("2"), cipherSpec2), "add 2");
TEST_RESULT_VOID(cipherSpecMapAdd(map, STRDEF("0"), cipherSpec1), "add 0");
TEST_RESULT_UINT(cipherSpecMapSize(map), 2, "two keys");
TEST_RESULT_STR_Z(cipherSpecMapGetIdx(map, 0)->id, "0", "first id");
TEST_RESULT_STR_Z(cipherSpecMapGetIdx(map, 1)->id, "2", "second id");
// Each key keeps its own digest
TEST_RESULT_UINT(cipherSpecDigest(cipherSpecMapGet(map, STRDEF("0"))), hashTypeSha256, "key 0 digest");
TEST_RESULT_UINT(cipherSpecDigest(cipherSpecMapGet(map, STRDEF("2"))), hashTypeSha1, "key 2 digest");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(cipherSpecMapGet(map, STRDEF("2")))), "key2", "key 2 pass");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(map), "2", "adding the key with no id did not make it current");
// A map with only the key that has no id has no current key
CipherSpecMap *const mapDefault = cipherSpecMapNew();
TEST_RESULT_VOID(cipherSpecMapAdd(mapDefault, CIPHER_SPEC_MAP_ID_DEFAULT_STR, cipherSpec1), "add key with no id");
TEST_RESULT_PTR(cipherSpecMapIdCurrent(mapDefault), NULL, "no current key");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("duplicate keeps the current key");
TEST_RESULT_PTR(cipherSpecMapIdCurrent(cipherSpecMapDup(cipherSpecMapNew())), NULL, "dup empty map");
CipherSpecMap *mapDup = NULL;
TEST_ASSIGN(mapDup, cipherSpecMapDup(map), "dup map");
TEST_RESULT_UINT(cipherSpecMapSize(mapDup), 2, "two keys");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(mapDup), "2", "current key id");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("key id not found");
TEST_ERROR(cipherSpecMapGet(map, STRDEF("1")), CryptoError, "unable to find cipher key '1'");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("pack round trip");
PackWrite *const packWrite = pckWriteNewP();
TEST_RESULT_VOID(cipherSpecMapPack(packWrite, map), "pack map");
pckWriteEndP(packWrite);
CipherSpecMap *mapPack = NULL;
TEST_ASSIGN(mapPack, cipherSpecMapNewPack(pckReadNew(pckWriteResult(packWrite))), "unpack map");
TEST_RESULT_UINT(cipherSpecMapSize(mapPack), 2, "two keys");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(cipherSpecMapGet(mapPack, STRDEF("0")))), "key1", "key 0 pass");
TEST_RESULT_UINT(cipherSpecDigest(cipherSpecMapGet(mapPack, STRDEF("2"))), hashTypeSha1, "key 2 digest");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(mapPack), "2", "current key id");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("log ids but not keys");
char logBuf[STACK_TRACE_PARAM_MAX];
TEST_RESULT_VOID(FUNCTION_LOG_OBJECT_FORMAT(map, cipherSpecMapToLog, logBuf, sizeof(logBuf)), "cipherSpecMapToLog");
TEST_RESULT_Z(logBuf, "{ids: [0, 2]}", "check log");
TEST_RESULT_VOID(cipherSpecMapFree(map), "free map");
}
FUNCTION_HARNESS_RETURN_VOID();

View File

@@ -83,13 +83,16 @@ testRun(void)
TEST_TITLE("create with cipher");
// Recreate from scratch with cipher
CipherSpecMap *const cipherSpecMapSub = cipherSpecMapNew();
cipherSpecMapAdd(
cipherSpecMapSub, CIPHER_SPEC_MAP_ID_DEFAULT_STR,
cipherSpecNewP(
cipherTypeAes256Cbc, BUFSTRDEF("zWa/6Xtp-IVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO"),
.digest = hashTypeSha1));
TEST_ASSIGN(
info,
infoArchiveNew(
PG_VERSION_10, 6569239123849665999, REPOSITORY_FORMAT_DEFAULT,
cipherSpecNewP(
cipherTypeAes256Cbc, BUFSTRDEF("zWa/6Xtp-IVZC5444yXB+cgFDFl7MxGlgkZSaoPvTGirhPygu4jOKOXf9LO4vjfO"),
.digest = hashTypeSha1)),
infoArchiveNew(PG_VERSION_10, 6569239123849665999, REPOSITORY_FORMAT_DEFAULT, cipherSpecMapSub),
"infoArchiveNew() - cipher sub");
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"));
@@ -97,7 +100,7 @@ testRun(void)
contentSave = bufNew(0);
IoWrite *write = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(write), cipherSpec, REPOSITORY_FORMAT_DEFAULT);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(write), cipherSpec, REPOSITORY_FORMAT_DEFAULT);
TEST_RESULT_VOID(infoArchiveSave(info, write), "save new with cipher");
TEST_RESULT_BOOL(
@@ -124,6 +127,26 @@ testRun(void)
TEST_RESULT_INT(infoPgData.version, PG_VERSION_96, "version set");
TEST_RESULT_UINT(infoPgData.systemId, 6569239123849665679, "systemId set");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("rotate cipher key");
const CipherSpec *const cipherSpecRotate = cipherSpecNewP(
cipherTypeAes256Cbc, BUFSTRDEF("rotate"), .digest = hashTypeSha256);
TEST_RESULT_VOID(infoArchiveFormatSet(info, REPOSITORY_FORMAT_6), "set format 6");
TEST_RESULT_INT(infoArchiveCipherRotateTime(info), 0, "no rotation time");
TEST_RESULT_VOID(infoArchiveCipherRotate(info, cipherSpecRotate, 1700000000), "rotate after migrated key");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(infoArchiveCipherSpecMap(info)), "1", "first key id");
TEST_RESULT_INT(infoArchiveCipherRotateTime(info), 1700000000, "rotation time");
// Ids increment as numbers rather than as text
TEST_RESULT_VOID(infoCipherSpecAdd(infoPgInfo(infoArchivePg(info)), STRDEF("9"), cipherSpecRotate), "add key 9");
TEST_RESULT_VOID(infoArchiveCipherRotate(info, cipherSpecRotate, 1700086400), "rotate after key 9");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(infoArchiveCipherSpecMap(info)), "10", "key id after 9");
TEST_RESULT_UINT(cipherSpecMapSize(infoArchiveCipherSpecMap(info)), 4, "all keys kept");
TEST_RESULT_INT(infoArchiveCipherRotateTime(info), 1700086400, "rotation time");
// -------------------------------------------------------------------------------------------------------------------------
TEST_TITLE("object free");

View File

@@ -98,7 +98,7 @@ testRun(void)
contentSave = bufNew(0);
IoWrite *write = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(write), cipherSpec, REPOSITORY_FORMAT_6);
cipherBlockFormatFilterGroupWriteAddP(ioWriteFilterGroup(write), cipherSpec, REPOSITORY_FORMAT_6);
TEST_RESULT_VOID(infoBackupSave(infoBackup, write), "save new with cipher sub");
TEST_RESULT_STR_Z(

View File

@@ -38,7 +38,7 @@ testRun(void)
TEST_ASSIGN(infoPg, infoPgNew(infoPgBackup, REPOSITORY_FORMAT_DEFAULT, NULL), "infoPgNew(cipherTypeNone, NULL)");
TEST_RESULT_INT(infoPgDataTotal(infoPg), 0, " 0 history");
TEST_RESULT_UINT(cipherSpecType(infoCipherSpec(infoPgInfo(infoPg))), cipherTypeNone, " cipher spec none");
TEST_RESULT_UINT(cipherSpecType(infoPgCipherSpec(infoPg)), cipherTypeNone, " cipher spec none");
TEST_RESULT_INT(infoPgDataCurrentId(infoPg), 0, " 0 historyCurrent");
TEST_ASSIGN(
@@ -48,7 +48,7 @@ testRun(void)
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"), .digest = hashTypeSha1)),
"infoPgNew(cipherTypeAes256Cbc, 123xyz)");
TEST_RESULT_INT(infoPgDataTotal(infoPg), 0, " 0 history");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(infoPgInfo(infoPg)))), "123xyz", " cipherPass set");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoPgCipherSpec(infoPg))), "123xyz", " cipherPass set");
TEST_RESULT_INT(infoPgDataCurrentId(infoPg), 0, " 0 historyCurrent");
// -------------------------------------------------------------------------------------------------------------------------
@@ -76,7 +76,7 @@ testRun(void)
TEST_RESULT_UINT(pgData.systemId, 6569239123849665999, " system-id updated");
TEST_RESULT_UINT(pgData.version, PG_VERSION_18, " version updated");
TEST_RESULT_UINT(pgData.catalogVersion, 0, " catalog version not set for archive");
TEST_RESULT_UINT(cipherSpecType(infoCipherSpec(infoPgInfo(infoPg))), cipherTypeNone, " cipher spec none");
TEST_RESULT_UINT(cipherSpecType(infoPgCipherSpec(infoPg)), cipherTypeNone, " cipher spec none");
// -------------------------------------------------------------------------------------------------------------------------
TEST_ASSIGN(
@@ -94,7 +94,7 @@ testRun(void)
TEST_RESULT_UINT(pgData.systemId, 6569239123849665679, " system-id set");
TEST_RESULT_UINT(pgData.version, PG_VERSION_18, " version set");
TEST_RESULT_UINT(pgData.catalogVersion, 202506291, " catalog version updated");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(infoPgInfo(infoPg)))), "123xyz", " cipherPass set");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoPgCipherSpec(infoPg))), "123xyz", " cipherPass set");
}
// *****************************************************************************************************************************

View File

@@ -21,6 +21,15 @@ testInfoReadHeader(const Buffer *const buffer, const CipherSpec *const cipherSpe
return result;
}
/***********************************************************************************************************************************
The key an info file holds under the default id
***********************************************************************************************************************************/
static const CipherSpec *
testInfoCipherSpec(const Info *const info)
{
return infoCipherSpec(info);
}
/***********************************************************************************************************************************
Test load callback
***********************************************************************************************************************************/
@@ -96,10 +105,13 @@ testRun(void)
infoNew(
REPOSITORY_FORMAT_DEFAULT, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"), .digest = hashTypeSha1)),
"infoNew(cipher)");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(info))), "123xyz", " cipherPass is set");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(testInfoCipherSpec(info))), "123xyz", " cipherPass is set");
TEST_ASSIGN(info, infoNew(REPOSITORY_FORMAT_DEFAULT, NULL), "infoNew(NULL)");
TEST_RESULT_UINT(cipherSpecType(infoCipherSpec(info)), cipherTypeNone, " cipher spec is none");
TEST_RESULT_UINT(cipherSpecType(testInfoCipherSpec(info)), cipherTypeNone, " cipher spec is none");
TEST_ASSIGN(info, infoNew(REPOSITORY_FORMAT_DEFAULT, cipherSpecNewNone()), "infoNew(none)");
TEST_RESULT_UINT(cipherSpecMapSize(infoCipherSpecMap(info)), 0, " no cipher keys");
}
// *****************************************************************************************************************************
@@ -203,7 +215,7 @@ testRun(void)
info, infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
"info with other cipher");
TEST_RESULT_STR_Z(callbackContent, "", " check callback content");
TEST_RESULT_UINT(cipherSpecType(infoCipherSpec(info)), cipherTypeNone, " check cipher pass not set");
TEST_RESULT_UINT(cipherSpecType(testInfoCipherSpec(info)), cipherTypeNone, " check cipher pass not set");
// Base file with content
// -------------------------------------------------------------------------------------------------------------------------
@@ -220,7 +232,7 @@ testRun(void)
info, infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
"info with content");
TEST_RESULT_STR_Z(callbackContent, "[c] key=1\n[d] key=1\n", " check callback content");
TEST_RESULT_UINT(cipherSpecType(infoCipherSpec(info)), cipherTypeNone, " check cipher pass not set");
TEST_RESULT_UINT(cipherSpecType(testInfoCipherSpec(info)), cipherTypeNone, " check cipher pass not set");
Buffer *contentSave = bufNew(0);
@@ -281,8 +293,8 @@ testRun(void)
info, infoNewLoad(readNoHeader, cipherSpecNoHeader, 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(strNewBuf(cipherSpecPass(testInfoCipherSpec(info))), "somepass", " check cipher pass set");
TEST_RESULT_UINT(cipherSpecDigest(testInfoCipherSpec(info)), hashTypeSha1, " check cipher sub digest");
TEST_RESULT_STR_Z(infoBackrestVersion(info), PROJECT_VERSION, " check backrest version");
contentSave = bufNew(0);
@@ -298,7 +310,8 @@ testRun(void)
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");
strstr(strZ(strNewBuf(contentSave)), "cipher-pass={\"0\":{\"digest\":\"sha1\",\"key\":\"somepass\"}}") != NULL, true,
" check key stored by id with its digest");
TEST_ASSIGN(
info,
@@ -307,7 +320,7 @@ testRun(void)
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");
TEST_RESULT_UINT(cipherSpecDigest(testInfoCipherSpec(info)), hashTypeSha1, " check cipher sub digest unchanged");
// Header
// -------------------------------------------------------------------------------------------------------------------------
@@ -315,7 +328,7 @@ testRun(void)
contentSave = bufNew(0);
IoWrite *const writeNone = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(writeNone), cipherSpecNewNone(), REPOSITORY_FORMAT_6);
cipherBlockFormatFilterGroupWriteAddP(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");
@@ -324,8 +337,7 @@ testRun(void)
REPOSITORY_FORMAT_6,
STRDEF(
"[cipher]\n"
"cipher-digest=\"sha256\"\n"
"cipher-pass=\"somepass\"\n"));
"cipher-pass={\"0\":{\"digest\":\"sha256\",\"key\":\"somepass\"}}\n"));
callbackContent = strNew();
@@ -336,7 +348,7 @@ testRun(void)
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");
TEST_RESULT_UINT(cipherSpecDigest(testInfoCipherSpec(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
@@ -344,8 +356,7 @@ testRun(void)
REPOSITORY_FORMAT_6,
STRDEF(
"[cipher]\n"
"cipher-digest=\"sha1\"\n"
"cipher-pass=\"somepass\"\n"));
"cipher-pass={\"0\":{\"digest\":\"sha1\",\"key\":\"somepass\"}}\n"));
TEST_ASSIGN(
info,
@@ -353,13 +364,56 @@ testRun(void)
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");
TEST_RESULT_UINT(cipherSpecDigest(testInfoCipherSpec(info)), hashTypeSha1, " check cipher sub digest");
// Two keys, as rotation stores them. The last key added is current.
CipherSpecMap *const cipherSpecMapMulti = cipherSpecMapNew();
cipherSpecMapAdd(
cipherSpecMapMulti, CIPHER_SPEC_MAP_ID_DEFAULT_STR,
cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("oldpass"), .digest = hashTypeSha1));
TEST_RESULT_VOID(infoCipherSpecMapSet(info, cipherSpecMapMulti), "set migrated key");
TEST_RESULT_VOID(
infoCipherSpecAdd(
info, STRDEF("9"), cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("newpass"), .digest = hashTypeSha256)),
"add second key");
TEST_RESULT_VOID(infoCipherRotateTimeSet(info, 1700000000), "set rotation time");
contentSave = bufNew(0);
TEST_RESULT_VOID(infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "save two keys");
TEST_RESULT_BOOL(
strstr(
strZ(strNewBuf(contentSave)),
"cipher-pass={\"0\":{\"digest\":\"sha1\",\"key\":\"oldpass\"}"
",\"9\":{\"digest\":\"sha256\",\"key\":\"newpass\"}}\n"
"cipher-pass-current=\"9\"\n"
"cipher-pass-rotate=1700000000\n") != NULL,
true, " check both keys stored by id");
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentSave, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, strNew()),
"load two keys");
const CipherSpecMap *const cipherSpecMapLoad = infoCipherSpecMap(info);
TEST_RESULT_UINT(cipherSpecMapSize(cipherSpecMapLoad), 2, " check two keys");
TEST_RESULT_STR_Z(cipherSpecMapIdCurrent(cipherSpecMapLoad), "9", " check current key id");
TEST_RESULT_INT(infoCipherRotateTime(info), 1700000000, " check rotation time");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(testInfoCipherSpec(info))), "oldpass", " check default key");
TEST_RESULT_UINT(cipherSpecDigest(testInfoCipherSpec(info)), hashTypeSha1, " check default key digest");
TEST_RESULT_STR_Z(
strNewBuf(cipherSpecPass(cipherSpecMapGet(cipherSpecMapLoad, STRDEF("9")))), "newpass", " check key 9");
TEST_RESULT_UINT(
cipherSpecDigest(cipherSpecMapGet(cipherSpecMapLoad, STRDEF("9"))), hashTypeSha256, " check key 9 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));
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(infoRead), cipherSpec);
ioReadOpen(infoRead);
TEST_RESULT_STR(strNewBuf(ioReadBuf(infoRead)), strNewBuf(contentLoad), "info content read");

View File

@@ -1453,8 +1453,7 @@ testRun(void)
"pg_data={\"path\":\"/pg/base\",\"type\":\"path\"}\n"
"\n"
"[cipher]\n"
"cipher-digest=\"sha256\"\n"
"cipher-pass=\"somepass\"\n"
"cipher-pass={\"0\":{\"digest\":\"sha256\",\"key\":\"somepass\"}}\n"
"\n"
"[target:file]\n"
"pg_data/PG_VERSION={\"checksum\":\"184473f470864e067ee3a22e64b47b0a1c356f29\",\"reference\":\"20190808-163540F\""