protondrive: test against re-keyed files written by Proton's own clients
TestGoldenCapture (-capture DIR -capture-label NAME -remote NAME:PATH) captures a folder another client wrote, re-keyed onto keys made for the tests: blocks and signature-free SEIPD packets kept byte for byte, key packets re-wrapped, signatures re-made with the same parameters (names, hash keys and extended attributes encrypted again with their own session keys, as their signatures are embedded), IDs and emails replaced. TestGoldenFixtures reads every fixture in testdata/golden through the backend from the fake server with every signature checked. The fake server can now seed stored folders and keeps the times of seeded nodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
502a32b5f3
commit
a2dc6f71f1
5 files changed
+1373
-5
No files matched your search
File diff suppressed because it is too large.
Load diff
@@ -876,9 +876,9 @@ func (u *User) ClaimSize(id string, size int64) {
|
||||
}
|
||||
|
||||
// insertLink adds an active link l under parentID and returns its new
|
||||
// ID, created now unless l has a creation time. It panics if parentID
|
||||
// has an active or draft child with the same name hash, which a seeding
|
||||
// test does not mean to create.
|
||||
// ID, created and modified now unless l has those times. It panics if
|
||||
// parentID has an active or draft child with the same name hash, which
|
||||
// a seeding test does not mean to create.
|
||||
func (u *User) insertLink(parentID string, l *link) string {
|
||||
u.srv.mu.Lock()
|
||||
defer u.srv.mu.Unlock()
|
||||
@@ -895,7 +895,10 @@ func (u *User) insertLink(parentID string, l *link) string {
|
||||
if l.createTime == 0 {
|
||||
l.createTime = now
|
||||
}
|
||||
l.id, l.parentID, l.state, l.modifyTime = randomID(), parentID, linkStateActive, now
|
||||
if l.modifyTime == 0 {
|
||||
l.modifyTime = now
|
||||
}
|
||||
l.id, l.parentID, l.state = randomID(), parentID, linkStateActive
|
||||
d.addLink(l)
|
||||
return l.id
|
||||
}
|
||||
|
||||
@@ -26,6 +26,8 @@ type StoredLink struct {
|
||||
MediaType string
|
||||
ContentKeyPacket string // base64
|
||||
ContentKeyPacketSignature string
|
||||
CreateTime int64 // Unix seconds; the time of seeding if zero
|
||||
ModifyTime int64 // Unix seconds; the time of seeding if zero
|
||||
}
|
||||
|
||||
// StoredRevision is a revision of a file node.
|
||||
@@ -33,6 +35,7 @@ type StoredRevision struct {
|
||||
ManifestSignature string
|
||||
XAttr string // armored; empty if none
|
||||
SignatureEmail string
|
||||
CreateTime int64 // Unix seconds; the time of seeding if zero
|
||||
Blocks []StoredBlock // in index order, from 1
|
||||
}
|
||||
|
||||
@@ -69,11 +72,14 @@ func (u *User) StoredFile(id string) StoredFile {
|
||||
MediaType: l.mediaType,
|
||||
ContentKeyPacket: l.contentKeyPacket,
|
||||
ContentKeyPacketSignature: l.contentKeyPacketSignature,
|
||||
CreateTime: l.createTime,
|
||||
ModifyTime: l.modifyTime,
|
||||
},
|
||||
Revision: StoredRevision{
|
||||
ManifestSignature: rev.manifestSignature,
|
||||
XAttr: rev.xattr,
|
||||
SignatureEmail: rev.signatureEmail,
|
||||
CreateTime: rev.createTime,
|
||||
},
|
||||
}
|
||||
for _, b := range rev.blocks {
|
||||
@@ -93,7 +99,7 @@ func (u *User) SeedStoredFile(parentID string, f StoredFile) (id string) {
|
||||
rev := &revision{
|
||||
id: randomID(),
|
||||
state: revisionStateActive,
|
||||
createTime: time.Now().Unix(),
|
||||
createTime: f.Revision.CreateTime,
|
||||
manifestSignature: f.Revision.ManifestSignature,
|
||||
xattr: f.Revision.XAttr,
|
||||
signatureEmail: f.Revision.SignatureEmail,
|
||||
@@ -107,6 +113,9 @@ func (u *User) SeedStoredFile(parentID string, f StoredFile) (id string) {
|
||||
signatureEmail: b.SignatureEmail,
|
||||
})
|
||||
}
|
||||
if rev.createTime == 0 {
|
||||
rev.createTime = time.Now().Unix()
|
||||
}
|
||||
return u.insertLink(parentID, &link{
|
||||
typ: linkTypeFile,
|
||||
name: f.Link.Name,
|
||||
@@ -118,10 +127,38 @@ func (u *User) SeedStoredFile(parentID string, f StoredFile) (id string) {
|
||||
mediaType: f.Link.MediaType,
|
||||
contentKeyPacket: f.Link.ContentKeyPacket,
|
||||
contentKeyPacketSignature: f.Link.ContentKeyPacketSignature,
|
||||
createTime: f.Link.CreateTime,
|
||||
modifyTime: f.Link.ModifyTime,
|
||||
revisions: []*revision{rev},
|
||||
})
|
||||
}
|
||||
|
||||
// StoredFolder is a folder node as the server stores it. Its Link has
|
||||
// no file members.
|
||||
type StoredFolder struct {
|
||||
Link StoredLink
|
||||
HashKey string // armored, encrypted to the node key
|
||||
XAttr string // armored; empty if none
|
||||
}
|
||||
|
||||
// SeedStoredFolder creates a folder in parentID from f, stored as given
|
||||
// without any check, and returns its ID. It panics as insertLink does.
|
||||
func (u *User) SeedStoredFolder(parentID string, f StoredFolder) (id string) {
|
||||
return u.insertLink(parentID, &link{
|
||||
typ: linkTypeFolder,
|
||||
name: f.Link.Name,
|
||||
nameHash: f.Link.NameHash,
|
||||
nodeKey: f.Link.NodeKey,
|
||||
nodePass: sealedPassphrase{armored: f.Link.NodePassphrase, signature: f.Link.NodePassphraseSignature},
|
||||
signatureEmail: f.Link.SignatureEmail,
|
||||
nameSignatureEmail: f.Link.NameSignatureEmail,
|
||||
createTime: f.Link.CreateTime,
|
||||
modifyTime: f.Link.ModifyTime,
|
||||
hashKey: f.HashKey,
|
||||
xattr: f.XAttr,
|
||||
})
|
||||
}
|
||||
|
||||
// FixedKeys is key material for AddUserWithKeys, in place of keys the
|
||||
// fake would generate.
|
||||
type FixedKeys struct {
|
||||
|
||||
@@ -5,6 +5,42 @@ it with a new one that links back. One entry per decision a future maintainer
|
||||
would otherwise have to reverse-engineer.
|
||||
No index: `grep -n '^## ' docs/DECISIONS.md` lists every entry.
|
||||
|
||||
## 2026-11-12 - Proton Drive golden fixtures are re-keyed captures
|
||||
|
||||
**Status:** accepted
|
||||
**Context:** Plan Task 21 asks for fixtures of nodes written by Proton's
|
||||
web app and official CLI. Captured as they are, they would carry the test
|
||||
account's keys, IDs and emails. The owner chose to re-encrypt them under
|
||||
keys made for the tests.
|
||||
**Decision:** `TestGoldenCapture` (`-capture DIR -capture-label NAME
|
||||
-remote NAME:PATH`) writes `backend/protondrive/testdata/golden/<label>/`,
|
||||
read by `TestGoldenFixtures` from the fake server with every signature
|
||||
checked. Preserved: every block byte for byte; every SEIPD packet which
|
||||
holds no signature (re-wrapped session key only); the session key, cipher,
|
||||
SEIPD version, AEAD mode and chunk size, compression algorithm and literal
|
||||
data packet (format, file name, date, content) of names, hash keys and
|
||||
extended attributes; signature version, type, hash, creation time,
|
||||
lifetime and notations; key algorithm, version, creation time and AEAD
|
||||
support; timestamps, sizes, flags, media types, block sizes and whether
|
||||
they are recorded. Replaced: all keys (address, node, the captured
|
||||
folder's parent) by new ones; every PKESK, re-wrapped in the same version;
|
||||
every signature, re-made by the key replacing its signer (an embedded one
|
||||
forces its SEIPD packet to be encrypted again with the same session key
|
||||
and parameters, as a signature can't change in place); node passphrases,
|
||||
new ones in a message of the same format; the captured folder's name hash;
|
||||
intended recipients and signer user IDs; emails by
|
||||
`golden@example.invalid`; IDs by placeholders. Dropped: block signatures
|
||||
(the backend doesn't read them), block URLs and tokens. Each node lists
|
||||
what was done to it in `Rekeyed`. Files with thumbnails are refused (the
|
||||
fake can't serve them); the address key passphrase is sealed to a user key
|
||||
the fake generates, so no user key is written.
|
||||
**Alternatives:** Raw captures (account secrets in the repository);
|
||||
fixtures re-made with gopenpgp from the decrypted content (loses the
|
||||
other clients' packet formats, the point of the exercise).
|
||||
**Consequences:** The fixtures test the formats of names, hash keys and
|
||||
extended attributes as other clients lay them out, but their signature
|
||||
bytes are ours. Packet headers are re-serialised with fixed lengths.
|
||||
|
||||
## 2026-11-12 - Proton Drive is a native port of Proton's Drive SDK
|
||||
|
||||
**Status:** accepted
|
||||
|
||||
@@ -10,6 +10,11 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
done. Start from `docs/plans/001-protondrive-tier1-HANDOFF.md`.
|
||||
Remaining: Task 21 (live verification, paid account never signed in
|
||||
to), then the whole-branch review.
|
||||
- [ ] protondrive: Task 21 golden fixtures - the capture
|
||||
(`TestGoldenCapture`) and `TestGoldenFixtures` are in; the live capture
|
||||
of the web app and CLI folders into `testdata/golden/` is the owner's to
|
||||
run, inspect and commit (command in
|
||||
`.superpowers/sdd/001-protondrive-tier1/task-21g-report.md`).
|
||||
|
||||
## Next
|
||||
|
||||
@@ -53,6 +58,10 @@ Small, specific, independently actionable. Remove only when actually done.
|
||||
recorded (`bytes=0-N`, N at or past the end) is read as a ranged read,
|
||||
so its manifest isn't checked; only a known size identifies a range
|
||||
covering the whole file.
|
||||
- [ ] protondrive: `TestGoldenCapture` refuses files with thumbnails (the
|
||||
fake server serves none, and the manifest covers them) and keys other
|
||||
than RSA, EdDSA, Ed25519 and Ed448; images and photos from Proton's apps
|
||||
can't become golden fixtures until the fake serves thumbnails.
|
||||
- [ ] protondrive: below the root, a node signed under another user's
|
||||
email (a shared folder's members) is verified against public keys the
|
||||
server supplies (`account.publicKeys`), so a hostile server can forge
|
||||
|
||||
Reference in new issue
Block a user