|
QtPass 2.0.0
Developer documentation
|
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. | |
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.
|
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.
|
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.
| gpgIdFile | The .gpg-id the bytes came from. |
| contents | The verified bytes. |
| storeRoot | The configured store, for the folder check. |
| error | Receives 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. |
Definition at line 679 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id the bytes came from. |
| header | What its header says. |
| storeRoot | The configured store. |
| error | Receives why not, if not null. |
Definition at line 601 of file gpgidgeneration.cpp.
|
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.
| contents | The bytes. |
Definition at line 587 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id. |
| storeRoot | The configured store. |
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.
|
static |
The header lines for a list.
| generation | The generation to declare. |
| folder | The store-relative folder the list is written for. |
| store | The store's id (storeIdFor()); empty writes no store line. |
Definition at line 542 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id, existing or about to be written. |
Definition at line 564 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id the bytes came from. |
| header | What its header says. |
| pinned | The store id this device knows the store by. |
| alreadyAccepted | Whether these exact bytes are the generation this device accepted last for the file. |
| recordPath | The store IDs' file, which the refusal names. |
| error | Receives why not, if not null. |
Definition at line 760 of file gpgidgeneration.cpp.
|
static |
The header a .gpg-id's bytes declare.
| contents | The file, as verified. |
| error | Receives why the metadata is unusable, if not null. |
Definition at line 500 of file gpgidgeneration.cpp.
|
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.
Definition at line 583 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id just written. |
| contents | The exact bytes written. |
| error | Receives why the record could not be written, if not null. |
Definition at line 846 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id. |
| error | Receives why the record could not be read, if not null. |
Definition at line 592 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id about to be written. |
| verifiedOnDisk | The 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. |
| error | Receives why nothing could be reserved, if not null. |
Definition at line 819 of file gpgidgeneration.cpp.
|
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.
| gpgIdFile | The .gpg-id the bytes came from. |
| header | What its header says. |
| last | The generation this device accepted before. |
| recordPath | The record file, which the text names. |
| error | Receives the reason, if not null. |
Definition at line 642 of file gpgidgeneration.cpp.
|
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.
| storeRoot | The configured store. |
| onDisk | The store line of the verified list on disk, if any. |
| error | Receives why the record could not be read or written. |
Definition at line 797 of file gpgidgeneration.cpp.
|
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.
Definition at line 585 of file gpgidgeneration.cpp.
|
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.
| last | The generation this device accepted before. |
| recordPath | The record file, which the text names. |
| saving | What the user does in the Users dialog, in this refusal's terms ("checking the recipients and saving"). |
Definition at line 624 of file gpgidgeneration.cpp.
|
static |
A complete list: the header in front of the recipients.
| generation | The generation to declare. |
| folder | The store-relative folder the list is written for. |
| recipients | The recipient lines, one per line, no header. |
| store | The store's id (storeIdFor()); empty writes no store line. |
Definition at line 552 of file gpgidgeneration.cpp.
|
static |
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.
|
static |
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.
|
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.
|
static |
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.