Commit Graph
121 Commits
Author SHA1 Message Date
nvrl 48dd410e41 chore(VER) bumped to 0.10.1 2026-10-31 13:27:43 +01:00
nvrl fe42f6c972 fix(account): open address tokens with inactive account keys, skip a broken address
An address key's token was decrypted with the active account keys only.
A token encrypted to an account key that is now inactive never opened,
although that key still opens with the password, so the address failed
with "has no primary key". proton-drive-linux issue #40 shows this as a
mount that retries "missing key" forever. Tokens are now tried with
every account key that opens, active ones first; signing still uses
active keys only. The key report walks the same chain.

addresses() also failed as a whole when one address did not open. That
address is now left out with a warning and the rest work; the call fails
only when none opens. address_private_keys and the new address(id) name
the reason for a left-out address, and default_address fails with it
when the left-out address is the default rather than signing with
another one.
2026-10-31 13:07:09 +01:00
nvrl 5c29ef88fc chore(VER) bumped to 0.10.0 2026-10-26 16:57:50 +01:00
nvrl 2c5d62e9cf feat(sharing): expose a public link's custom password
get_public_link recovers the custom password along with the URL fragment,
so a caller can keep it while update_public_link changes the role or the
expiry. Debug for PublicLink and Bookmark redacts the URL and the custom
password.
2026-10-25 22:01:13 +01:00
nvrl 781301a77b feat(sharing,public-link): update public links, name bookmarks, walk shared trees
- update_public_link sets the role, custom password and expiry of a node's
  public link and keeps its URL (TS updateSharedLink). Create and update
  share the sealing of the share key and SRP verifier.
- list_bookmarks decrypts the bookmarked item's name with the share key the
  stored password unlocks, and returns the custom password in the new
  Bookmark::custom_password. The URL fragment is now only the generated
  password.
- ProtonDrivePublicLinkClient::enumerate_subtree walks a shared folder
  breadth first, several folders of a level at a time.
- Add CHANGELOG.md, update the README feature table for this and the
  previous unreleased commits, and record the TS ports in UPSTREAM_SYNC.
2026-10-25 21:27:05 +01:00
nvrl 3da18b9309 feat(photos): port the C# album capabilities
Ports upstream a36cc423. add_photos_to_album now uses the timeline
transfer driver, generalised over a PhotoTransferTarget. A photo on
another volume, from an album shared with us, is copied into the album
instead of failing. A photo whose group the server reports incomplete
is sent once more with the related photos it named. Outcomes stay in
input order.

New set_album_cover, and delete_album_saving_photos, which saves the
photos that exist only in the album (code 200302, now
AlbumContainsPhotosNotInTimeline) before deleting it.
remove_photos_from_album fails a photo on another volume without
sending it. AlbumUpdateRequest leaves out a missing cover or name, as
C# does.

Pins upstream to b68d64bd. The search commit e861ef99 is not ported:
its engine source is not public.
2026-10-25 20:48:22 +01:00
nvrl c30b107382 feat(drive): copy a node on the server
ProtonDriveClient::copy_node copies a file with its content, or a folder
with its subtree, under a new parent without downloading it, ported from
the TS SDK's copyNode (POST volumes/{vid}/links/{lid}/copy). The crypto
is the move's: the passphrase is rewrapped to the new parent, so the
node and content keys stay the same, and the name is encrypted and
signed to it. An optional name renames the copy.

The new parent may be on another volume, and the returned uid takes the
target's volume, where TS uses the source's. CopyPhotoResponse is now
the shared CopyLinkResponse.

UPSTREAM_SYNC.md logs this, get_folder_size and the read retries.
2026-10-25 20:37:51 +01:00
nvrl 8611989364 feat(drive): report a folder's size from the server
ProtonDriveClient::get_folder_size returns the total size and number of
a folder's descendants without walking the tree, ported from the TS
SDK's getFolderSize (GET volumes/{vid}/folders/{lid}/calculate-
descendents-size). Trashed descendants and folders count, and the size
is encrypted bytes on storage; the live test pins both, which the TS
docs leave open.
2026-10-25 20:33:29 +01:00
nvrl ee16f4326e feat(http): send a read again when its connection drops
A GET, or a POST that only reads, is now retried when the connection
drops after the request left: before the response arrives or partway
through its body. A phone does this when the OS closes a pooled
connection or moves from Wi-Fi to mobile data, and until now the SDK
only retried timeouts and failed connects, so every caller that walks a
tree needed its own retry loop.

send_retrying reads the body inside the loop and takes a Replay mode.
Writes keep the old rule: a request that may have reached the server is
not sent again, as a second attempt could create a duplicate.

ApiHttpClient::post_query is the POST for endpoints that only read.
Link details, thumbnail URLs, photo duplicates and checkAvailableHashes
use it, on both the authenticated and the public-link paths. Block
downloads from storage are retried as reads too.
2026-10-25 20:33:29 +01:00
nvrl 0194f87856 chore(VER) bumped to 0.9.2 2026-10-17 12:16:48 +02:00
nvrl bd27f7862b feat(account,drive): report which keys open and which key each token is for
AccountClient::key_report walks the user and address keys like the
unlock path, but records each step instead of skipping what fails:
whether each user key opens (inactive ones included), and the
recipients of each address key token. ProtonDriveClient::
my_files_share_recipients names the keys the My Files share passphrase
is encrypted to without decrypting anything. Together they trace a
"missing key" to the key it was meant for (proton-drive-linux#40).

crypto::recipients and crypto::key_ids read OpenPGP identifiers from
messages and keys, so a recipient can be matched to a key.
2026-10-17 12:13:27 +02:00
nvrl 155bf23615 chore(VER) bumped to 0.9.1 2026-10-15 22:58:11 +02:00
nvrl 92772e0ef0 fix(drive): replace a locked My Files volume instead of failing on it
A password reset gives the account new keys and locks the volume whose
share was encrypted to the old ones. v2/shares/my-files still returns
that share, so decrypting its passphrase failed with "missing key" on
every call and a client could never reach My Files
(proton-drive-linux#40).

The web client handles this by listing volumes and creating a new one
when no active regular volume is left. The locked volume stays
restorable from the web. ensure_my_files now does the same when the
share key fails to decrypt, and only once GET volumes confirms there is
no active regular volume. Any other decryption failure is returned as
before.
2026-10-15 22:57:57 +02:00
nvrl eb0e5aaf57 chore(VER) bumped to 0.9.0 2026-10-15 19:50:35 +02:00
nvrl 1b8abf70f8 feat(drive): port upstream C# SDK up to 8ffae007
Port af391cbb (require node type for getting an available name);
c6b45188 (file-backed transfer buffers) is C# memory plumbing and a
no-op here.

- get_available_name takes an AvailableNameTarget. A file keeps its
  extension last (name (1).ext); a folder appends to the whole name
  (name (1)), so a folder named v1.2 no longer resolves to v1 (1).2.
- Split alternate_names into alternate_file_names and
  alternate_folder_names, mirroring AlternateFileNameGenerator.

Breaking: get_available_name gains a parameter.
2026-10-15 19:50:17 +02:00
nvrl 9e4d2fb036 chore(VER) bumped to 0.8.3 2026-10-13 20:23:24 +02:00
nvrl 92c6dffb5a feat(api): name response code 2028 as Blocked
Proton answers a blocked sign-in on auth/v4 with HTTP 422 and code 2028,
"feature is disabled, rate-limited or blocked because of abuse". Its
Error text says the app version is no longer supported, which is wrong
for a current client. The code fell through to Unknown, so a client
could not tell it from a real OutdatedApp and showed the misleading text.
2026-10-13 20:22:55 +02:00
nvrl 56d7b15115 chore(VER) bumped to 0.8.2 2026-10-09 11:45:49 +02:00
nvrl 43e513924c fix(http): a token refresh survives its caller, and is never repeated
Proton refresh tokens are single-use. refresh_access_token ran inside
the caller's future, so a timeout around the request could drop it after
Proton rotated the tokens but before the client stored them, and the
session was gone. It now runs on its own task: the tokens are stored and
the callback fired whether or not anyone still waits.

auth/v4/refresh went through send_retrying, which repeats on timeouts,
408, 429 and 5xx. Any of those may come after the server rotated, and the
retry then spends the old token again and gets InvalidRefreshToken. The
refresh is now retried only when the connection failed.
2026-10-09 11:45:36 +02:00
nvrl 2fbed3f959 chore(VER) bumped to 0.8.1 2026-10-08 15:03:28 +02:00
nvrl d3f4090772 feat(http): log the raw response code of failed API requests
An envelope code that ResponseCode does not know deserializes to Unknown, so
the error a caller sees has lost the number Proton sent. Log path, HTTP status
and the raw code at debug level before mapping it.
2026-10-08 15:02:57 +02:00
nvrl 325c326c49 feat(account): hold key passphrases as wiped secrets and report a locked account as KeysLocked
A wrong mailbox password surfaced as InvalidOperation("no active user key could
be unlocked"), which a caller could only recognise by its text. It is now
ProtonError::KeysLocked, and AccountClient::unlock() checks a secret with a
single core/v4/users request instead of loading every address.

Derived passphrases, the mailbox password and the cache encryption key were
plain Vec<u8>s, cloned freely and never wiped. They are Zeroizing now, and the
passphrase map is KeyPassphrases: wiped on drop, redacted in Debug, with
derive_secret() for a client that wants a key only an unlocked account can
reproduce. with_key_passphrases takes it.

Breaking: ProtonError gains a variant, key_passphrases() and
with_key_passphrases() change type, and derive_key_passphrase returns
Zeroizing<Vec<u8>>.
2026-10-07 02:24:51 +02:00
nvrl 909eefd06e chore(VER) bumped to 0.7.4 2026-10-06 23:49:58 +02:00
nvrl 32014b1f07 feat(drive,photos): report recently accessed nodes
Port upstream 0aabea59: report_recently_accessed on ProtonDriveClient and
ProtonPhotosClient, batches of 50. Also read the ThirdParty/Sdk flags from
f257c20e on link and revision DTOs. Pin bumped to df3a717b.
2026-10-06 23:49:58 +02:00
nvrl 7950eff337 chore(deps) upgraded dependencies 2026-10-06 14:10:09 +02:00
nvrl 771bc443fa chore(VER) bumped to 0.7.3 2026-10-06 13:59:12 +02:00
nvrl 6eb8b206a7 feat(account,session): resume from derived key passphrases and report second-factor methods
`AccountClient::key_passphrases` is now public, and
`AccountClient::with_key_passphrases` / `ProtonDriveClient::with_key_passphrases`
build a client from the per-key passphrases alone. A client that persists a
session can store those instead of the mailbox password, which on a
single-password account is the login password itself.

`ProtonApiSession::second_factor_methods` exposes the login's 2FA bitmask
(TOTP, FIDO2), so a client without WebAuthn can tell an account that only has
a security key apart from a wrong code.
2026-10-06 13:59:10 +02:00
nvrl 1bd5fe8a07 chore(VER) bumped to 0.7.2 2026-10-01 18:07:46 +02:00
nvrl 1477fe85d5 fix(drive): give each share root its own parent key in batch enumeration
enumerate_nodes and enumerate_nodes_light cached parent keys per batch by
the link's parent id. Every parentless link in a volume landed on None, so
when several share roots were enumerated together, the first root's share
key was used for the rest and they were dropped with "missing key".

Name a parent key by where it comes from: Parent(folder) for a link with a
parent, Root(link) for a parentless one. The batch cache and the
parent-key single-flight now both use that identity. The single-flight
previously keyed a root and its children on the same link id, although a
root resolves to its share key and its children to its node key, so two
concurrent cold loads could hand one of them the wrong key.

Fixes #2
2026-10-01 18:06:58 +02:00
nvrl 4a0453c66f chore(VER) bumped to 0.7.1 2026-09-29 21:14:35 +02:00
nvrl a01e3a5cbd fix(http): keep idle connections alive and bound the connect phase
Every client built by the SDK now shares one builder. It sends TCP
keepalives and HTTP/2 PING frames every 15 seconds, also while the
connection is idle, and drops a connection whose PING goes unanswered for
10 seconds. A connection that a NAT or middlebox silently dropped is then
noticed before a request waits on it for the full request timeout.

Connecting is capped at 15 seconds, or the request timeout if that is
shorter, so an unreachable host fails fast and the retry loop moves on.
2026-09-29 21:14:35 +02:00
nvrl ce491ba519 chore(deps) updated 2026-09-28 01:15:55 +02:00
nvrl 9036c35697 chore(VER) bumped to 0.7.0 2026-09-28 01:07:12 +02:00
nvrl 1068ddf5d0 feat(drive): read a photo's location from its extended attributes
Other Proton clients write a `Location` object with `Latitude` and
`Longitude` into a photo's encrypted extended attributes. The decoder
dropped it. It is now decoded and exposed as `PhotoProperties::location`,
a `PhotoLocation` in WGS 84 decimal degrees.

Coordinates that are missing, not finite, or off the globe give no
location rather than a wrong one. The new field defaults on
deserialization, so nodes cached before this change still load.

`PhotoProperties` gains a public field, which breaks code that builds it
with a struct literal.
2026-09-28 01:06:59 +02:00
nvrl 917bad2ef4 chore(sync): bump upstream pin to f28a93ce 2026-09-27 12:12:48 +02:00
nvrl 7422036f65 fix(scripts): upstream-sync reads short pins and detects rewritten history
- read the SHA from the Pinned line (7-40 chars) instead of requiring a
  full 40-char SHA the script itself never printed; the old grep died
  silently under pipefail
- print the full head SHA to pin
- require the pin to be an ancestor of origin/main, since a rewritten-away
  pin still resolves locally until gc
- --diffs now prints patches, not just stats
- --all no longer reports dropped noise
- accept submodule/worktree checkouts where .git is a file
2026-09-27 12:10:33 +02:00
nvrl f7054c3c3b chore(VER) bumped to 0.6.6 2026-09-23 03:15:25 +02:00
nvrl 931a58794b feat(drive): album rename, delete and remove photos
Port AlbumsManager.updateAlbum (name only), deleteAlbum and removePhotos
from the TypeScript SDK: PUT photos/volumes/{vid}/albums/{lid},
DELETE ...?DeleteAlbumPhotos= and POST .../remove-multiple in batches of
ten.
2026-09-23 03:15:10 +02:00
nvrl 1f76f33d49 chore(VER) bumped to 0.6.5 2026-09-19 14:05:22 +02:00
nvrl 1ca28c5848 feat(drive,sdk): port upstream C# SDK up to 6cbf2f44
Port 6cbf2f44 (stop retrying permanent API errors on block transfers)
and 20822176 (small file upload interop); 6ce47922 is a no-op here.

- Add ProtonApiError::is_retriable / ProtonError::is_retriable mirroring
  C# RetryPolicy.IsRetriable: 4xx is permanent except 404, 408 and 429,
  with envelope codes able to override the HTTP status.
- Gate upload_one_block's retry loop on it so a permanent rejection of a
  revision fails immediately instead of being replayed three times.
- Extract BlockTargets from RevisionReader so the whole-file download
  path, the public link download and the reader all re-mint expired
  block URLs through one generation-deduplicated refresh.
- Fall back from the small upload to the block path on AlreadyExists
  only; the small upload is not idempotent, so everything else keeps
  propagating.
- Bound a server-supplied Retry-After by RetryPolicy::max_retry_after
  (default 120 s) so a long rate-limit answer cannot look like a hang.
2026-09-19 14:05:19 +02:00
nvrl 4f129b778a chore(VER) bumped to 0.6.4 2026-09-15 15:07:21 +02:00
nvrl bf36bbdf74 chore(deps): bump lru to 0.18 and base64 to 0.23
lru < 0.16.3 has an IterMut Stacked Borrows violation (GHSA advisory
flagged by Dependabot on downstream proton-drive-linux).
2026-09-15 15:06:56 +02:00
nvrl 6f052e1912 chore(VER) bumped to 0.6.3 2026-09-15 13:54:33 +02:00
nvrl d72065418b feat(drive,sdk): port upstream C# SDK up to ce000edd
- Moves go through `move-multiple` in batches of 150, with an optional
  target name per node (`move_nodes_streaming` / `NodeMoveItem`), a single
  `InvalidRequirements` retry against the server's current name hash, and
  passphrase signing for anonymously created nodes. A batch rejected as a
  whole keeps its API error code on every node.
- Share memberships carry invite time and inviter email, and the inviter's
  `drive.share-member.inviter` signature is verified as non-fatal metadata
  (`verify_detached_with_context`).
- `Node` gains `direct_role` and `share_id`.
- Trashed detection reads `TrashTime` as well as the legacy `Trashed`.

Breaking: new public struct fields on `Node` and `ShareMembership`.
2026-09-15 13:54:04 +02:00
nvrl 81844cb9da chore(VER) bumped to 0.6.2 2026-08-28 12:00:05 +02:00
nvrl aa17b52692 feat(photos): save photos to the timeline
Ports upstream C# e0e198e2 (SavePhotosToTimelineAsync, itself the JS
saveToTimeline). Photos already on our photos volume are moved into the
timeline root via PUT photos/volumes/{vid}/links/transfer-multiple,
batched so no request carries more than ten links and a photo is never
split from its related photos; photos on another volume — from an album
shared with us — are copied via POST volumes/{vid}/links/{lid}/copy, one
request each. That closes the cross-volume copy hole (TS copyPhoto).

The re-encryption is what add_photos_to_album already did, so it moves
into a shared photo_target_payload: name re-encrypted to the target key
and signed by the photos membership address, name/content hashes under
the target hash key, node passphrase rewrapped so the original
NodePassphraseSignature survives.

A photo the server reports an incomplete group for is re-queued once
with the related photos it named; only its second answer becomes an
outcome, so outcomes arrive in completion order, not input order.

Also adds ProtonPhotosClient::enumerate_events (c8d03244's only
non-interop hunk) and bumps the upstream pin to c8d03244. Upstream
29e58378 (upload telemetry) and 0537d906 (NodeProvider refactor) are
deliberately not ported — see UPSTREAM_SYNC.md.
2026-08-27 15:58:06 +02:00
nvrl 61117fd7dc feature(photos) implemented dups, import from takeout 2026-08-18 01:30:45 +02:00
nvrl 0cf760362a fix(sdk) token mutex no longer spans the refresh 2026-08-16 18:53:29 +02:00
nvrl 9ec0582efe added chain with salts 2026-08-10 15:19:18 +02:00
nvrl 12bcf61424 version bump to 0.5.0 2026-08-10 14:11:38 +02:00