1
0
mirror of https://github.com/pgbackrest/pgbackrest.git synced 2026-10-23 21:28:56 +03:00
Files
pgbackrest/test/src/module/info/infoTest.c
David Steele 700722b8b5 Derive the cipher key with SHA-256 at repository format 6.
Format 6 derives encryption keys from the passphrase with SHA-256 rather than SHA-1. Older repositories keep SHA-1, since the migration does not rewrite the files they already encrypted. The digest is picked when a passphrase is generated rather than at each use, so it is stored with the passphrase in CipherSpec and defaults to SHA-256.

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

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

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

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

472 lines
22 KiB
C

/***********************************************************************************************************************************
Test Info Handler
***********************************************************************************************************************************/
#include "common/crypto/cipherBlock.h"
#include "common/format/cipherBlockFormat.h"
#include "common/io/bufferRead.h"
#include "common/io/bufferWrite.h"
#include "storage/posix/storage.h"
#include "harness/info.h"
/***********************************************************************************************************************************
Read a buffer through the filter that reads the header, as a caller loading a file that has one does
***********************************************************************************************************************************/
static IoRead *
testInfoReadHeader(const Buffer *const buffer, const CipherSpec *const cipherSpec)
{
IoRead *const result = ioBufferReadNew(buffer);
cipherBlockFormatFilterGroupReadAdd(ioReadFilterGroup(result), cipherSpec);
return result;
}
/***********************************************************************************************************************************
Test load callback
***********************************************************************************************************************************/
typedef struct TestInfoLoad
{
unsigned int test;
} TestInfoLoad;
static bool
testInfoLoadCallback(void *data, unsigned int try)
{
TestInfoLoad *testInfoLoad = (TestInfoLoad *)data;
if (testInfoLoad->test == 1)
{
if (try == 0)
THROW(ChecksumError, "checksum error");
else
return false;
}
if (testInfoLoad->test == 2)
{
if (try < 2)
THROW(FormatError, "format error");
else
return false;
}
if (testInfoLoad->test == 3)
{
if (try == 0)
THROW(FileMissingError, "file missing error");
else if (try == 1)
THROW(ChecksumError, "checksum error\nHINT: have you checked the thing?");
else if (try == 2)
THROW(FormatError, "format error");
else if (try == 3)
THROW(FileMissingError, "file missing error");
else
return false;
}
return true;
}
/***********************************************************************************************************************************
Test save callbacks
***********************************************************************************************************************************/
static void
testInfoSaveCallback(void *data, const String *sectionNext, InfoSave *infoSaveData)
{
if (infoSaveSection(infoSaveData, "c", sectionNext))
infoSaveValue(infoSaveData, "c", "key", (String *)data);
if (infoSaveSection(infoSaveData, "d", sectionNext))
infoSaveValue(infoSaveData, "d", "key", (String *)data);
}
/***********************************************************************************************************************************
Test Run
***********************************************************************************************************************************/
static void
testRun(void)
{
// *****************************************************************************************************************************
if (testBegin("infoNew() and infoNewInternal()"))
{
Info *info = NULL;
TEST_ASSIGN(
info, infoNew(REPOSITORY_FORMAT_DEFAULT, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("123xyz"))),
"infoNew(cipher)");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(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");
}
// *****************************************************************************************************************************
if (testBegin("infoNewLoad() and infoSave()"))
{
// Format error
// -------------------------------------------------------------------------------------------------------------------------
const Buffer *contentLoad = BUFSTRDEF(
"[backrest]\n"
"backrest-format=4\n");
String *callbackContent = strNew();
TEST_ERROR(
infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
FormatError,
"repository format 4 is no longer supported by pgBackRest\n"
"HINT: pgBackRest " PROJECT_VERSION " supports repository format 5 to 6.");
TEST_RESULT_STR_Z(callbackContent, "", " check callback content");
// Format newer than supported
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = BUFSTRDEF(
"[backrest]\n"
"backrest-format=7\n");
TEST_ERROR(
infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
FormatError,
"repository format 7 requires a newer version of pgBackRest\n"
"HINT: pgBackRest " PROJECT_VERSION " supports repository format 5 to 6.");
// Checksum not found
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = BUFSTRDEF(
"[backrest]\n"
"backrest-format=5\n");
TEST_ERROR(
infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
ChecksumError, "invalid checksum, actual 'a3765a8c2c1e5d35274a0b0ce118f4031faff0bd' but no checksum found");
TEST_RESULT_STR_Z(callbackContent, "", " check callback content");
// Checksum invalid
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = BUFSTRDEF(
"[backrest]\n"
"backrest-checksum=\"BOGUS\"\n"
"backrest-format=5\n"
"backrest-version=\"2.17\"\n"
"bogus=\"BOGUS\"\n");
TEST_ERROR(
infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
ChecksumError, "invalid checksum, actual 'fe989a75dcf7a0261e57d210707c0db741462763' but expected 'BOGUS'");
TEST_RESULT_STR_Z(callbackContent, "", " check callback content");
// Format not found. The checksum must be valid since it is verified before the format is checked.
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = BUFSTRDEF(
"[backrest]\n"
"backrest-checksum=\"be4f04bf9a8346d387bb254c48aeee2cbb5d46d0\"\n"
"backrest-version=\"2.17\"\n");
TEST_ERROR(
infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
FormatError,
"repository format not found\n"
"HINT: is this a valid pgBackRest info file?");
TEST_RESULT_STR_Z(callbackContent, "", " check callback content");
// Crypto expected
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = BUFSTRDEF(
"[backrest]\n"
"backrest-checksum=\"BOGUS\"\n"
"backrest-format=5\n"
"backrest-version=\"2.17\"\n");
IoRead *read = ioBufferReadNew(contentLoad);
ioFilterGroupAdd(
ioReadFilterGroup(read),
cipherBlockNewP(cipherModeDecrypt, cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("X"))));
TEST_ERROR(
infoNewLoad(read, cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent), CryptoError,
"cipher header invalid\n"
"HINT: is or was the repo encrypted?");
TEST_RESULT_STR_Z(callbackContent, "", " check callback content");
// Base file with other content in cipher (this is to test that future additions don't break the code)
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = harnessInfoChecksumZ(
"[cipher]\n"
"cipher-other=1\n");
Info *info = NULL;
callbackContent = strNew();
TEST_ASSIGN(
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");
// Base file with content
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = harnessInfoChecksumZ(
"[c]\n"
"key=1\n"
"\n"
"[d]\n"
"key=1\n");
callbackContent = strNew();
TEST_ASSIGN(
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");
Buffer *contentSave = bufNew(0);
TEST_RESULT_VOID(infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "info save");
TEST_RESULT_STR(strNewBuf(contentSave), strNewBuf(contentLoad), " check save");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_DEFAULT, " check format");
// The format is read from the file rather than assumed, and written back out as read
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = harnessInfoChecksumFormat(
REPOSITORY_FORMAT_6,
STRDEF(
"[c]\n"
"key=1\n"
"\n"
"[d]\n"
"key=1\n"));
callbackContent = strNew();
TEST_ASSIGN(
info, infoNewLoad(ioBufferReadNew(contentLoad), cipherSpecNewNone(), harnessInfoLoadNewCallback, callbackContent),
"info format 6");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_6, " check format");
contentSave = bufNew(0);
TEST_RESULT_VOID(infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "info save");
TEST_RESULT_STR(strNewBuf(contentSave), strNewBuf(contentLoad), " check save preserves format");
// Set the format
// -------------------------------------------------------------------------------------------------------------------------
TEST_RESULT_VOID(infoFormatSet(info, REPOSITORY_FORMAT_5), "set format");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_5, " check format");
// File with content and cipher
// -------------------------------------------------------------------------------------------------------------------------
contentLoad = harnessInfoChecksumZ(
"[c]\n"
"key=1\n"
"\n"
"[cipher]\n"
"cipher-pass=\"somepass\"\n"
"\n"
"[d]\n"
"key=1\n");
callbackContent = strNew();
const CipherSpec *const cipherSpec = cipherSpecNewP(cipherTypeAes256Cbc, BUFSTRDEF("x"));
// A file with no header, e.g. a manifest, is decrypted with the spec as it was given since nothing defines the digest
// as anything else
IoRead *const readNoHeader = ioBufferReadNew(harnessInfoEncryptP(contentLoad, cipherSpec));
cipherBlockFilterGroupAdd(ioReadFilterGroup(readNoHeader), cipherModeDecrypt, cipherSpec);
TEST_ASSIGN(
info, infoNewLoad(readNoHeader, cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info with content and cipher");
TEST_RESULT_STR_Z(callbackContent, "[c] key=1\n[d] key=1\n", " check callback content");
TEST_RESULT_STR_Z(strNewBuf(cipherSpecPass(infoCipherSpec(info))), "somepass", " check cipher pass set");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha1, " check cipher sub digest");
TEST_RESULT_STR_Z(infoBackrestVersion(info), PROJECT_VERSION, " check backrest version");
contentSave = bufNew(0);
TEST_RESULT_VOID(infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "info save");
TEST_RESULT_STR(strNewBuf(contentSave), strNewBuf(contentLoad), " check save");
// Migrating to a format that stores the digest does not change the digest the pass derives with, since the files the pass
// encrypted before the migration are not rewritten
contentSave = bufNew(0);
TEST_RESULT_VOID(infoFormatSet(info, REPOSITORY_FORMAT_6), "migrate to format 6");
TEST_RESULT_VOID(
infoSave(info, ioBufferWriteNew(contentSave), testInfoSaveCallback, strNewZ("1")), "save migrated info");
TEST_RESULT_BOOL(
strstr(strZ(strNewBuf(contentSave)), "cipher-digest=\"sha1\"") != NULL, true, " check digest stored with pass");
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentSave, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, strNew()),
"load migrated info");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_6, " check format");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha1, " check cipher sub digest unchanged");
// Header
// -------------------------------------------------------------------------------------------------------------------------
// An unencrypted file has no header no matter the format, since the format is read from the content
contentSave = bufNew(0);
IoWrite *const writeNone = ioBufferWriteNew(contentSave);
cipherBlockFormatFilterGroupWriteAdd(contentSave, ioWriteFilterGroup(writeNone), cipherSpecNewNone(), REPOSITORY_FORMAT_6);
TEST_RESULT_VOID(infoSave(info, writeNone, testInfoSaveCallback, strNewZ("1")), "info save");
TEST_RESULT_BOOL(strBeginsWithZ(strNewBuf(contentSave), "PGBR"), false, " check no header");
contentLoad = harnessInfoChecksumFormat(
REPOSITORY_FORMAT_6,
STRDEF(
"[cipher]\n"
"cipher-digest=\"sha256\"\n"
"cipher-pass=\"somepass\"\n"));
callbackContent = strNew();
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info with header");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_6, " check format");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha256, " check cipher sub digest");
// A pass migrated from a format that could not store the digest keeps deriving with SHA-1, so the files it encrypted
// before the migration are still readable
const Buffer *const contentMigrated = harnessInfoChecksumFormat(
REPOSITORY_FORMAT_6,
STRDEF(
"[cipher]\n"
"cipher-digest=\"sha1\"\n"
"cipher-pass=\"somepass\"\n"));
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentMigrated, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info migrated to the format that stores the digest");
TEST_RESULT_UINT(cipherSpecDigest(infoCipherSpec(info)), hashTypeSha1, " check cipher sub digest");
// The content on its own, which is how a caller that wants the file rather than the values in it reads an info file
IoRead *const infoRead = ioBufferReadNew(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6));
ioFilterGroupAdd(
ioReadFilterGroup(infoRead), cipherBlockFormatNewP(cipherSpec));
ioReadOpen(infoRead);
TEST_RESULT_STR(strNewBuf(ioReadBuf(infoRead)), strNewBuf(contentLoad), "info content read");
// A file written before the header existed is read as the format that had none
contentLoad = harnessInfoChecksumZ("[c]\nkey=1\n");
TEST_ASSIGN(
info,
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_5), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
"info with no header");
TEST_RESULT_UINT(infoFormat(info), REPOSITORY_FORMAT_5, " check format");
// A file too short to hold a header cannot have one
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(BUFSTRDEF("PGBR"), cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
CryptoError,
"cipher header missing\n"
"HINT: is or was the repo encrypted?");
// A file with neither header was never encrypted, which is reported the way the cipher reports it
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(BUFSTRDEF("[backrest]\nbackrest-format=5\n"), cipherSpec), cipherSpec,
harnessInfoLoadNewCallback, callbackContent),
CryptoError,
"cipher header invalid\n"
"HINT: is or was the repo encrypted?");
// Header uses the byte held back for later, so this version does not know what it is looking at
Buffer *contentHeader = harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6);
bufPtr(contentHeader)[7] = 'X';
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(contentHeader, cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError, "invalid cipher header");
// Header damaged where the format should be
contentHeader = harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6);
bufPtr(contentHeader)[5] = 'X';
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(contentHeader, cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError, "invalid cipher header");
// Header names a format this version cannot read, which is reported before anything is decrypted
contentHeader = harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6);
bufPtr(contentHeader)[6] = '7';
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(contentHeader, cipherSpec), cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError,
"repository format 7 requires a newer version of pgBackRest\n"
"HINT: pgBackRest " PROJECT_VERSION " supports repository format 5 to 6.");
// Header and content disagree about the format
TEST_ERROR(
infoNewLoad(
testInfoReadHeader(harnessInfoEncryptP(contentLoad, cipherSpec, .format = REPOSITORY_FORMAT_6), cipherSpec),
cipherSpec, harnessInfoLoadNewCallback, callbackContent),
FormatError, "repository format 5 does not match header format 6");
}
// *****************************************************************************************************************************
if (testBegin("infoLoad()"))
{
// One error
// -------------------------------------------------------------------------------------------------------------------------
TestInfoLoad testInfoLoad = {.test = 1};
TEST_ERROR(
infoLoad(STRDEF("unable to load info file"), testInfoLoadCallback, &testInfoLoad), ChecksumError,
"unable to load info file:\n"
"ChecksumError: checksum error");
// Two errors (same error)
// -------------------------------------------------------------------------------------------------------------------------
testInfoLoad = (TestInfoLoad){.test = 2};
TEST_ERROR(
infoLoad(STRDEF("unable to load info file(s)"), testInfoLoadCallback, &testInfoLoad), FormatError,
"unable to load info file(s):\n"
"FormatError: format error\n"
"FormatError: format error");
// Four errors (mixed)
// -------------------------------------------------------------------------------------------------------------------------
testInfoLoad = (TestInfoLoad){.test = 3};
TEST_ERROR(
infoLoad(STRDEF("unable to load info file(s)"), testInfoLoadCallback, &testInfoLoad), FileOpenError,
"unable to load info file(s):\n"
"FileMissingError: file missing error\n"
"ChecksumError: checksum error\n"
"HINT: have you checked the thing?\n"
"FormatError: format error\n"
"FileMissingError: file missing error");
// Success
// -------------------------------------------------------------------------------------------------------------------------
testInfoLoad = (TestInfoLoad){0};
infoLoad(STRDEF("SHOULD BE NO ERROR"), testInfoLoadCallback, &testInfoLoad);
}
}