QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
GpgIdGeneration Class Reference

Rollback detection for signed .gpg-id files. More...

#include <gpgidgeneration.h>

Classes

struct  Header
 What a list's header says. More...

Public Types

enum class  Verdict {
  Accepted , Rollback , Unbound , Conflict ,
  WrongFolder , WrongStore , Malformed , RecordUnavailable
}
 The outcome of accept(). Only Accepted means the list may be used; the others say why not, and Rollback alone is an authentic list of this folder. More...

Static Public Member Functions

static auto parse (const QByteArray &contents, QString *error=nullptr) -> std::optional< Header >
 The header a .gpg-id's bytes declare.
static auto header (qint64 generation, const QString &folder, const QString &store={}) -> QByteArray
 The header lines for a list.
static auto withHeader (qint64 generation, const QString &folder, const QByteArray &recipients, const QString &store={}) -> QByteArray
 A complete list: the header in front of the recipients.
static auto storeIdFor (const QString &storeRoot, const std::optional< QString > &onDisk, QString *error=nullptr) -> std::optional< QString >
 The store id to write into a list of storeRoot: the one this device knows the store by; else onDisk, the store line of the verified list being replaced, so a device joining a store takes its id over; else a new random one. Remembered before it is returned.
static auto folderOf (const QString &gpgIdFile, const QString &storeRoot) -> std::optional< QString >
 The store-relative folder of gpgIdFile, as written in and compared against the folder line: . for the store root, /-separated, from canonical paths, lower-cased on Windows.
static auto remembered (const QString &gpgIdFile, QString *error=nullptr) -> std::optional< qint64 >
 The highest generation this device has accepted or written for the .gpg-id at gpgIdFile. Unreadable is not "never seen": a signed list must then be refused, not compared against 0.
static auto accept (const QString &gpgIdFile, const QByteArray &contents, const QString &storeRoot, QString *error=nullptr) -> Verdict
 Decide about a verified list: its folder against where it sits, its generation against the remembered one, its bytes against the remembered digest when the generations are equal, and remember it when it passes. One transaction under the lock: read, compare, write through.
static auto reserveNext (const QString &gpgIdFile, std::optional< qint64 > verifiedOnDisk, QString *error=nullptr) -> std::optional< qint64 >
 Reserve the generation to write next for gpgIdFile: one above both what this device remembers and verifiedOnDisk, recorded as remembered before it is returned. One transaction under the lock, so two writers in this process cannot get the same number.
static auto recordWritten (const QString &gpgIdFile, const QByteArray &contents, QString *error=nullptr) -> bool
 Record the bytes just written for gpgIdFile, so that this device recognises its own list afterwards and refuses another list of the same generation. Called after the file is on disk; the generation in contents must be the one reserveNext() gave.
static auto digest (const QByteArray &contents) -> QString
 The digest of a list as the record keeps it: SHA-256 of the exact bytes, hexadecimal, so that two authentic lists of one generation can be told apart.
static auto key (const QString &gpgIdFile) -> QString
 The settings key for a .gpg-id: a hash of its canonical path, so path spelling (case, separators, links on the way) and QSettings' treatment of / do not matter.
static auto recordFile () -> QString
 The file the accepted generations are recorded in, so a refusal can name it: removing it forgets what was accepted, which is the manual way out for a device that cannot save the list itself.
static auto storeRecordFile () -> QString
 The file the store IDs are kept in, next to recordFile() and under its lock, so a QtPass from before store IDs, rewriting the record, cannot drop them. Removing it forgets the IDs.

Static Public Attributes

static const QByteArray kGenerationPrefix
static const QByteArray kFolderPrefix
static const QByteArray kStorePrefix
static const qint64 kMaxGeneration = 999999999999999999LL

Static Private Member Functions

static auto boundToItsFolder (const QString &gpgIdFile, const Header &header, const QString &storeRoot, QString *error) -> bool
 Whether header's folder line, if it has one, names the folder the list sits in: a pair copied elsewhere is that folder's first list, a rollback in disguise.
static auto ofThisStore (const QString &gpgIdFile, const Header &header, const QString &pinned, bool alreadyAccepted, const QString &recordPath, QString *error) -> bool
 Whether header fits the store this device knows by pinned: its store line names that id, or it has none and is either generation 0 (pass) or a list this device accepted before it knew the id.
static auto wayThrough (qint64 last, const QString &recordPath, const QString &saving) -> QString
 How a holder of the signing key gets through a refusal, in words: saving the recipients writes the next generation, unless the record is already at the ceiling of the grammar, where it has to be removed first.
static auto staleList (const QString &gpgIdFile, const Header &header, qint64 last, const QString &recordPath, QString *error) -> Verdict
 The refusal for a list older than the one accepted here: a rollback, or an unbound list that could be any folder's history.

Detailed Description

Rollback detection for signed .gpg-id files.

A signature proves that a recipient list is authentic and unmodified; it does not prove that it is the current one, nor that it was written for the folder it sits in. Whoever can write to a shared store can put back an older, genuinely signed list that still names a member since removed, or copy such a pair into a folder that never had a list of its own, and every later encryption there would include them again.

So a .gpg-id QtPass writes for a store with a signing key starts with three comment lines, # QtPass-GpgId-Generation: N, # QtPass-GpgId-Folder: <store-relative folder> and # QtPass-GpgId-Store: <Store tree and entries id>, which pass (1.7.4 and later), QtPass (1.8 and later) and QtPass's own recipient parser ignore and the signature covers. The store id binds the list to its store, as the folder line binds it to its folder: one signing key may sign several stores, and a list of one copied into another at the same folder would verify there too (#2050). Each device remembers the id it first saw per store and refuses a list of another store, or a new list without the line. QtPass remembers, per .gpg-id file, the highest generation it has accepted or written, and refuses a signed list whose generation is lower or whose folder is not the one it sits in. Stores without a signing key get plain lists, as before: nothing checks their freshness, and older clients (pass up to 1.7.3, Android Password Store) take a comment for a recipient.

What this is and is not: rollback detection on a device that has seen the newer list. A device seeing a store for the first time, or one that was offline, has nothing to compare against. The generation is monotonic per observer, not a distributed sequence number: two devices can both produce generation 19 from 18, and Git is what sorts that out; a device that accepted one of the two then meets the other as the same generation with different bytes, which is a conflict, not a rollback, and is refused until a holder of the signing key saves the recipients once more. For that the record keeps, next to the generation, a digest of the exact bytes it accepted or wrote. A list without the lines (written by pass, or by QtPass before 2.0) is generation 0 and is refused once anything higher was accepted; a holder of the signing key saving the recipients from QtPass writes the next generation. Without a readable and writable record nothing is accepted or written: unknown state is not "never seen".

Definition at line 57 of file gpgidgeneration.h.

Member Enumeration Documentation

◆ Verdict

enum class GpgIdGeneration::Verdict
strong

The outcome of accept(). Only Accepted means the list may be used; the others say why not, and Rollback alone is an authentic list of this folder.

Enumerator
Accepted 

Folder, generation and record all agree; remembered.

Rollback 

Authentic and bound to this folder, but older than a generation this device accepted.

Unbound 

Older than a generation this device accepted and without a folder line: authentic, but written for who knows which folder (pass, or QtPass before 2.0, wrote it), so not this folder's to recover.

Conflict 

The generation this device accepted last, but not the bytes: two devices saved at once (Git shows that as a conflict), or an authentic list of that generation was swapped in.

WrongFolder 

Written for another folder of the store.

WrongStore 

Written for another store (its store line names another id than this device knows the store by), or new to this device and without a store line while the store has an id.

Malformed 

A header line malformed or duplicated.

RecordUnavailable 

The record could not be read or written.

Definition at line 163 of file gpgidgeneration.h.

Member Function Documentation

◆ accept()

auto GpgIdGeneration::accept ( const QString & gpgIdFile,
const QByteArray & contents,
const QString & storeRoot,
QString * error = nullptr ) -> Verdict
static

Decide about a verified list: its folder against where it sits, its generation against the remembered one, its bytes against the remembered digest when the generations are equal, and remember it when it passes. One transaction under the lock: read, compare, write through.

Parameters
gpgIdFileThe .gpg-id the bytes came from.
contentsThe verified bytes.
storeRootThe configured store, for the folder check.
errorReceives the reason for a refusal, if not null. Where the way through is saving the list again, the text says so, and that the recipients the dialog then preselects are this list's.
Returns
Accepted when the list may be used; otherwise why not. Without established freshness state nothing is accepted.

Definition at line 679 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ boundToItsFolder()

auto GpgIdGeneration::boundToItsFolder ( const QString & gpgIdFile,
const Header & header,
const QString & storeRoot,
QString * error ) -> bool
staticprivate

Whether header's folder line, if it has one, names the folder the list sits in: a pair copied elsewhere is that folder's first list, a rollback in disguise.

Parameters
gpgIdFileThe .gpg-id the bytes came from.
headerWhat its header says.
storeRootThe configured store.
errorReceives why not, if not null.
Returns
Whether the list belongs where it is.

Definition at line 601 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ digest()

auto GpgIdGeneration::digest ( const QByteArray & contents) -> QString
static

The digest of a list as the record keeps it: SHA-256 of the exact bytes, hexadecimal, so that two authentic lists of one generation can be told apart.

Parameters
contentsThe bytes.
Returns
64 hexadecimal characters.

Definition at line 587 of file gpgidgeneration.cpp.

Here is the caller graph for this function:

◆ folderOf()

auto GpgIdGeneration::folderOf ( const QString & gpgIdFile,
const QString & storeRoot ) -> std::optional< QString >
static

The store-relative folder of gpgIdFile, as written in and compared against the folder line: . for the store root, /-separated, from canonical paths, lower-cased on Windows.

Parameters
gpgIdFileThe .gpg-id.
storeRootThe configured store.
Returns
The folder, or nothing when gpgIdFile is not under the store or a folder name on the way holds a control character, a line break among them (the folder line could not record it).

Definition at line 558 of file gpgidgeneration.cpp.

Here is the caller graph for this function:

◆ header()

auto GpgIdGeneration::header ( qint64 generation,
const QString & folder,
const QString & store = {} ) -> QByteArray
static

The header lines for a list.

Parameters
generationThe generation to declare.
folderThe store-relative folder the list is written for.
storeThe store's id (storeIdFor()); empty writes no store line.
Returns
The lines, newlines included.

Definition at line 542 of file gpgidgeneration.cpp.

Here is the caller graph for this function:

◆ key()

auto GpgIdGeneration::key ( const QString & gpgIdFile) -> QString
static

The settings key for a .gpg-id: a hash of its canonical path, so path spelling (case, separators, links on the way) and QSettings' treatment of / do not matter.

Parameters
gpgIdFileThe .gpg-id, existing or about to be written.
Returns
64 hexadecimal characters.

Definition at line 564 of file gpgidgeneration.cpp.

Here is the caller graph for this function:

◆ ofThisStore()

auto GpgIdGeneration::ofThisStore ( const QString & gpgIdFile,
const Header & header,
const QString & pinned,
bool alreadyAccepted,
const QString & recordPath,
QString * error ) -> bool
staticprivate

Whether header fits the store this device knows by pinned: its store line names that id, or it has none and is either generation 0 (pass) or a list this device accepted before it knew the id.

Parameters
gpgIdFileThe .gpg-id the bytes came from.
headerWhat its header says.
pinnedThe store id this device knows the store by.
alreadyAcceptedWhether these exact bytes are the generation this device accepted last for the file.
recordPathThe store IDs' file, which the refusal names.
errorReceives why not, if not null.
Returns
Whether the list is this store's.

Definition at line 760 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ parse()

auto GpgIdGeneration::parse ( const QByteArray & contents,
QString * error = nullptr ) -> std::optional< Header >
static

The header a .gpg-id's bytes declare.

Parameters
contentsThe file, as verified.
errorReceives why the metadata is unusable, if not null.
Returns
The header (generation 0 and no folder for a list written before generations existed, or by pass); nothing when a line is malformed, appears twice, a generation line comes without a folder line or a store line without a generation line, which is not a list to trust.

Definition at line 500 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ recordFile()

auto GpgIdGeneration::recordFile ( ) -> QString
static

The file the accepted generations are recorded in, so a refusal can name it: removing it forgets what was accepted, which is the manual way out for a device that cannot save the list itself.

Returns
The record's path.

Definition at line 583 of file gpgidgeneration.cpp.

◆ recordWritten()

auto GpgIdGeneration::recordWritten ( const QString & gpgIdFile,
const QByteArray & contents,
QString * error = nullptr ) -> bool
static

Record the bytes just written for gpgIdFile, so that this device recognises its own list afterwards and refuses another list of the same generation. Called after the file is on disk; the generation in contents must be the one reserveNext() gave.

Parameters
gpgIdFileThe .gpg-id just written.
contentsThe exact bytes written.
errorReceives why the record could not be written, if not null.
Returns
Whether the digest was recorded. A list written but not digested is still recognised by its generation; only a second list of that generation would pass until it is read once.

Definition at line 846 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ remembered()

auto GpgIdGeneration::remembered ( const QString & gpgIdFile,
QString * error = nullptr ) -> std::optional< qint64 >
static

The highest generation this device has accepted or written for the .gpg-id at gpgIdFile. Unreadable is not "never seen": a signed list must then be refused, not compared against 0.

Parameters
gpgIdFileThe .gpg-id.
errorReceives why the record could not be read, if not null.
Returns
0 when this device has never seen the list, the generation otherwise; nothing when the record cannot be read.

Definition at line 592 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ reserveNext()

auto GpgIdGeneration::reserveNext ( const QString & gpgIdFile,
std::optional< qint64 > verifiedOnDisk,
QString * error = nullptr ) -> std::optional< qint64 >
static

Reserve the generation to write next for gpgIdFile: one above both what this device remembers and verifiedOnDisk, recorded as remembered before it is returned. One transaction under the lock, so two writers in this process cannot get the same number.

Reserving before the file is written is what makes a failed write harmless (the record is ahead, the old list stays refused until a save succeeds) and a failed record fatal for the write: a list written but not recorded would let the previous one back in.

Parameters
gpgIdFileThe .gpg-id about to be written.
verifiedOnDiskThe generation of the list currently on disk, if its signature verified; nothing otherwise. An unverified number is not taken: planted at the top of the grammar it would exhaust the counter, planted high it would make every other device see a jump.
errorReceives why nothing could be reserved, if not null.
Returns
The generation to write, or nothing when the record could not be read or written or the counter is exhausted; the caller must not write the list then.

Definition at line 819 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ staleList()

auto GpgIdGeneration::staleList ( const QString & gpgIdFile,
const Header & header,
qint64 last,
const QString & recordPath,
QString * error ) -> Verdict
staticprivate

The refusal for a list older than the one accepted here: a rollback, or an unbound list that could be any folder's history.

Parameters
gpgIdFileThe .gpg-id the bytes came from.
headerWhat its header says.
lastThe generation this device accepted before.
recordPathThe record file, which the text names.
errorReceives the reason, if not null.
Returns
Rollback for a list that says which folder it is for, Unbound for one that does not.

Definition at line 642 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ storeIdFor()

auto GpgIdGeneration::storeIdFor ( const QString & storeRoot,
const std::optional< QString > & onDisk,
QString * error = nullptr ) -> std::optional< QString >
static

The store id to write into a list of storeRoot: the one this device knows the store by; else onDisk, the store line of the verified list being replaced, so a device joining a store takes its id over; else a new random one. Remembered before it is returned.

Parameters
storeRootThe configured store.
onDiskThe store line of the verified list on disk, if any.
errorReceives why the record could not be read or written.
Returns
The id, or nothing when the record is unavailable; the caller must not write the list then.

Definition at line 797 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

◆ storeRecordFile()

auto GpgIdGeneration::storeRecordFile ( ) -> QString
static

The file the store IDs are kept in, next to recordFile() and under its lock, so a QtPass from before store IDs, rewriting the record, cannot drop them. Removing it forgets the IDs.

Returns
The file's path.

Definition at line 585 of file gpgidgeneration.cpp.

◆ wayThrough()

auto GpgIdGeneration::wayThrough ( qint64 last,
const QString & recordPath,
const QString & saving ) -> QString
staticprivate

How a holder of the signing key gets through a refusal, in words: saving the recipients writes the next generation, unless the record is already at the ceiling of the grammar, where it has to be removed first.

Parameters
lastThe generation this device accepted before.
recordPathThe record file, which the text names.
savingWhat the user does in the Users dialog, in this refusal's terms ("checking the recipients and saving").
Returns
The sentence to append to a refusal.

Definition at line 624 of file gpgidgeneration.cpp.

Here is the caller graph for this function:

◆ withHeader()

auto GpgIdGeneration::withHeader ( qint64 generation,
const QString & folder,
const QByteArray & recipients,
const QString & store = {} ) -> QByteArray
static

A complete list: the header in front of the recipients.

Parameters
generationThe generation to declare.
folderThe store-relative folder the list is written for.
recipientsThe recipient lines, one per line, no header.
storeThe store's id (storeIdFor()); empty writes no store line.
Returns
The bytes to write.

Definition at line 552 of file gpgidgeneration.cpp.

Here is the call graph for this function:
Here is the caller graph for this function:

Member Data Documentation

◆ kFolderPrefix

const QByteArray GpgIdGeneration::kFolderPrefix
static
Initial value:
=
QByteArrayLiteral("# QtPass-GpgId-Folder: ")

The folder line: this prefix and the store-relative folder the list was written for, . for the store root, /-separated.

Definition at line 64 of file gpgidgeneration.h.

◆ kGenerationPrefix

const QByteArray GpgIdGeneration::kGenerationPrefix
static
Initial value:
=
QByteArrayLiteral("# QtPass-GpgId-Generation: ")

The generation line. The grammar is strict: this prefix, one ASCII decimal number of at most 18 digits, nothing else on the line.

Definition at line 61 of file gpgidgeneration.h.

◆ kMaxGeneration

const qint64 GpgIdGeneration::kMaxGeneration = 999999999999999999LL
static

The largest generation the grammar admits (18 nines). reserveNext() never goes above it: a header QtPass writes must be one it reads back.

Definition at line 71 of file gpgidgeneration.h.

◆ kStorePrefix

const QByteArray GpgIdGeneration::kStorePrefix
static
Initial value:
=
QByteArrayLiteral("# QtPass-GpgId-Store: ")

The store line: this prefix and the store's id, 32 lowercase hex digits (a random 128-bit number given when the store's first signed list is written).

Definition at line 68 of file gpgidgeneration.h.


The documentation for this class was generated from the following files: