1288 lines
70 KiB
Markdown
1288 lines
70 KiB
Markdown
# Decision Record
|
|
|
|
## 2026-07-31
|
|
|
|
### D001: New protected group
|
|
|
|
Create a new protected-group mode backed by a private Telegram carrier
|
|
supergroup. Do not initially convert existing groups or create protected
|
|
subrooms.
|
|
|
|
### D002: Telegram-only remote infrastructure
|
|
|
|
Use Telegram for all remote delivery and encrypted blob storage. Operate no
|
|
custom backend.
|
|
|
|
### D003: Active-server threat target
|
|
|
|
Protect established keys, membership, and state transitions against active
|
|
server manipulation. State the unavoidable first-contact and availability
|
|
limitations of a Telegram-only design.
|
|
|
|
### D004: TOFU identity bootstrap
|
|
|
|
Use trust on first use, signed key gossip, and a human-readable safety code.
|
|
Warn prominently on unexpected key changes. Out-of-band comparison is optional.
|
|
|
|
### D005: No stock-client plaintext
|
|
|
|
Accounts using stock clients receive no cryptographic keys. There is no
|
|
plaintext compatibility mode.
|
|
|
|
### D006: Password-unlocked account vault
|
|
|
|
Use one user-memorized E2E password per account to unlock a random account vault
|
|
master key. Version one does not remember the password or unlocked vault key in
|
|
an operating-system credential store. Keep them only for the unlocked process
|
|
session, cleanse owned password buffers on every failure or lock, and lock the
|
|
vault automatically when the application passcode locks. New vaults require at
|
|
least 12 Unicode characters; old vaults remain unlockable without changing
|
|
their password bytes.
|
|
|
|
### D007: No server password recovery
|
|
|
|
Password loss creates a new E2E identity. Group owners or E2E administrators may
|
|
admit that identity and regrant history allowed by policy.
|
|
|
|
### D008: Configurable history admission
|
|
|
|
Provide a signed group default plus a per-invitation override. Support no
|
|
history, history from join, history from a boundary, and full history.
|
|
|
|
### D009: E2E administration
|
|
|
|
The group owner and explicitly appointed E2E administrators may manage E2E
|
|
admission and history policy. Telegram administrator status alone is not enough.
|
|
|
|
### D010: Replaceable MLS implementation (superseded by D024)
|
|
|
|
Define an implementation-independent engine boundary and require RFC 9420
|
|
vectors, interoperability, fuzzing, and security review before production use.
|
|
The original deferral of the concrete implementation is superseded by D024;
|
|
the boundary remains.
|
|
|
|
### D011: Initial scale target
|
|
|
|
Validate groups containing 500 participant accounts and multiple concurrent
|
|
client instances without encoding 500 as a protocol maximum.
|
|
|
|
### D012: Freshness gate before first send
|
|
|
|
Allow a new installation to unlock its vault, read available history, download
|
|
ciphertext, and compose messages before another participant confirms freshness.
|
|
Keep composed messages in a protected local queue. Send no new content and allow
|
|
no security-critical E2E administration until any current installation of any
|
|
participant returns a valid response to a fresh challenge. Encrypt and upload
|
|
the queue only against confirmed current group state.
|
|
|
|
### D013: Fixed-order version-one envelope
|
|
|
|
Encode the application transport envelope as a bounded fixed-order binary
|
|
structure with unsigned big-endian integers. Reject unknown versions, malformed
|
|
lengths, identifier mismatches, and trailing bytes. Keep MLS wire messages
|
|
opaque inside the payload and authenticate carrier binding through the
|
|
object-specific MLS authenticated data or signature.
|
|
|
|
### D014: Authenticated history boundary
|
|
|
|
Represent a `Since` history grant with the stable object identifier of an
|
|
authenticated archive event. Dates remain a user-interface selection aid and
|
|
are never the signed authorization boundary.
|
|
|
|
### D015: Capability-scoped E2E administrators
|
|
|
|
Only the E2E owner appoints administrators and assigns their explicit
|
|
capabilities. Default administrators may admit and remove ordinary members,
|
|
grant bounded history, and change bounded defaults. Granting full history is a
|
|
separate opt-in capability. Telegram roles never assign these capabilities.
|
|
|
|
### D016: E2E owner remains authoritative
|
|
|
|
If Telegram carrier ownership diverges from protected ownership, retain the E2E
|
|
owner as cryptographic authority and show a security warning. The carrier owner
|
|
may still delete or censor transport objects and cause denial of service but
|
|
does not gain keys, roles, or history access.
|
|
|
|
### D017: Unsupported carrier members remain pending
|
|
|
|
Show Telegram members without a verified protected credential as pending or
|
|
unsupported. They are not protected members, do not receive keys, and never
|
|
trigger plaintext fallback.
|
|
|
|
### D018: Canonical protected-group transitions
|
|
|
|
Encode each version-one group transition as a fixed 185-byte payload. Require a
|
|
single canonical value for unused fields, exact generation increments, unique
|
|
transition identifiers, an active actor client, and state-level authorization
|
|
after cryptographic verification.
|
|
|
|
### D019: Mandatory MLS 1.0 suite first
|
|
|
|
Start version one with cipher suite `0x0001`,
|
|
`MLS_128_DHKEMX25519_AES128GCM_SHA256_Ed25519`, which every MLS 1.0 client must
|
|
implement. Keep suite and credential versions explicit so a reviewed hybrid
|
|
post-quantum migration can replace it later without transport negotiation.
|
|
|
|
### D020: Stable account authorization, distinct MLS keys
|
|
|
|
Use a stable Ed25519 account key to authorize client credentials and a stable
|
|
X25519 archive HPKE key for granted history. Derive the account identifier with
|
|
domain-separated SHA-256 over the canonical public credential. Never reuse the
|
|
account signing key as a per-group MLS leaf key.
|
|
|
|
Pairwise safety codes hash the sorted account identifiers. Group safety codes
|
|
hash the conversation identifier, E2E owner, and sorted active account roster.
|
|
Display 240 bits as grouped Crockford Base32 and retain the full digest for QR
|
|
comparison.
|
|
|
|
### D021: Purpose-bound encrypted local records
|
|
|
|
Protect local outbox and state snapshots with AES-256-GCM under a random local
|
|
record key obtained after vault unlock. Generate a new 96-bit nonce per write,
|
|
authenticate the versioned header and record purpose, cleanse owned key material,
|
|
and atomically replace only ciphertext files. Never derive this record key
|
|
directly from the password and never fall back to plaintext storage.
|
|
|
|
### D022: Opaque document carrier with two-stage acknowledgement
|
|
|
|
Carry every version-one protocol envelope as an ordinary Telegram document
|
|
whose bytes are the exact encoded envelope. Use the fixed control/content
|
|
filenames defined by D035, MIME type `application/octet-stream`, an empty
|
|
caption, no preview, and no original application-file metadata. Treat both
|
|
upload and `messages.sendMedia` as one transport operation. Remove an outbox
|
|
item only after the final send RPC succeeds; an uploaded file token alone is
|
|
not delivery.
|
|
|
|
Downloaded documents remain untrusted byte strings until the protocol codec,
|
|
carrier binding, signature or MLS authentication, generation rules, and replay
|
|
state accept them. Telegram message identifiers and document identities never
|
|
become protocol object identifiers.
|
|
|
|
### D023: Independently authenticated resumable file chunks
|
|
|
|
Encrypt file content with a new random 256-bit key per file. Use AES-256-GCM
|
|
independently for each chunk. Construct its 96-bit nonce from a random 64-bit
|
|
per-file prefix followed by the unsigned 32-bit chunk index. Authenticate the
|
|
conversation identifier, random file identifier, total plaintext size, chunk
|
|
size and count, nonce prefix, index, and exact chunk plaintext length.
|
|
|
|
The encrypted private manifest carries the key, nonce prefix, full plaintext
|
|
digest, layout, original filename, and original MIME value. None of those
|
|
private metadata values are copied into Telegram document metadata. A
|
|
persistent nonce ledger must retain the exact ciphertext or plaintext digest
|
|
for every allocated `(file key, chunk index)` before upload; changed source
|
|
bytes may never be encrypted under an already allocated nonce.
|
|
|
|
### D024: OpenMLS production engine
|
|
|
|
Use the MIT-licensed OpenMLS `0.8.1` code line at upstream commit
|
|
`0e99bc8814d136f0bc7bc9ce86dd288eb32273ed` behind a narrow versioned C ABI.
|
|
That commit carries the upstream July 2026 HPKE and cryptographic dependency
|
|
updates. Enable only RFC 9420 cipher suite `0x0001` and the RustCrypto provider
|
|
for version one. Do not expose draft protocol extensions. Rust ownership, panics,
|
|
allocations, and library types never cross the ABI; callers exchange bounded
|
|
byte strings and numeric status codes.
|
|
|
|
OpenMLS is selected because its `StorageProvider` is designed to persist the
|
|
complete long-lived group state after successful operations and the upstream
|
|
project builds the required desktop targets plus future Android and iOS
|
|
targets. The evaluated unmodified Cisco MLS++ interface does not expose a
|
|
supported complete persistence format for all private TreeKEM, key-schedule,
|
|
and sender/receiver ratchet state. Maintaining a private serialization fork of
|
|
cryptographic internals is a larger and less reviewable risk.
|
|
|
|
The pin is a dependency baseline, not a claim that the application is ready for
|
|
production. Updating it requires review of security advisories, storage-format
|
|
changes, RFC vectors, differential interoperability against an independent MLS
|
|
implementation, fuzzing, and migration tests. The bridge build pins Rust 1.91
|
|
and a checked Cargo lockfile so the toolchain and transitive graph do not drift.
|
|
|
|
The earlier release-tag dependency graph was rejected after RustSec reported
|
|
six vulnerabilities in the selected cryptographic transitive graph. The
|
|
replacement lockfile has no reported vulnerability as of 2026-07-31. Its sole
|
|
maintenance warning is an unmaintained build-time proc-macro reached through
|
|
the verified SHA3 implementation; it is tracked but is not runtime crypto code.
|
|
|
|
### D025: Transactional MLS state and outgoing bytes
|
|
|
|
Run every mutating MLS operation against an isolated copy of the current
|
|
per-conversation provider state. The engine returns a candidate state snapshot
|
|
and any exact outgoing wire message without publishing either. Commit the new
|
|
snapshot, request digest, operation identifier, and complete outgoing envelope
|
|
as one purpose-bound encrypted atomic local record. Only then may the in-memory
|
|
state advance or Telegram upload begin.
|
|
|
|
A failed local commit leaves the previous provider state authoritative and
|
|
exposes no candidate ciphertext. A retry with an already committed operation
|
|
identifier returns the recorded exact envelope. Reusing that identifier with a
|
|
different request digest or ciphertext fails closed. Receipts remain until the
|
|
outbox acknowledges Telegram delivery; stale receipts may be compacted only
|
|
after the outbox no longer references them.
|
|
|
|
### D026: Transactional inbound MLS plaintext
|
|
|
|
Process every incoming MLS message against an isolated provider copy. Commit
|
|
the resulting provider snapshot and the authenticated decrypted application
|
|
record in one encrypted atomic local write before releasing plaintext. A
|
|
separate replay journal may remain pending across that write: restart recovery
|
|
checks the committed application record and either delivers it without replay,
|
|
retries an operation known not to have committed, or fails closed when neither
|
|
state can be proven. Never process uncertain ciphertext twice against a mutable
|
|
receiver ratchet.
|
|
|
|
### D027: Strict archive boundaries
|
|
|
|
Every retained archive epoch records its own monotonically increasing archive
|
|
generation, the protected-group generation at which it became active, and the
|
|
authenticated event identifier that activated it. Rotate the archive epoch on
|
|
each admission, removal, and history-policy boundary. `FromJoin` grants begin
|
|
only at an epoch whose activation generation exactly equals the member's join
|
|
generation. `Since` accepts only an archive-activation event, never an arbitrary
|
|
Telegram timestamp or an event in the middle of an epoch. The interface may
|
|
resolve a requested date to the nearest later safe boundary, but may not grant
|
|
an earlier epoch and silently reveal extra history.
|
|
|
|
Persist archive keys in a purpose-bound encrypted atomic snapshot. Importing a
|
|
history grant is idempotent only when every repeated generation has identical
|
|
activation metadata and key bytes; any conflict blocks the conversation for
|
|
recovery rather than replacing a key.
|
|
|
|
### D028: Signed group chain binds MLS and archive state
|
|
|
|
Every transition signs its predecessor checkpoint, canonical protected
|
|
mutation, actor account/client, exact MLS object ID and hash, next archive-key
|
|
commitment, encrypted archive-distribution object ID and hash, and any target
|
|
client authorization. A transition is accepted only after the account
|
|
signature, MLS sender/AAD, archive key, and resulting OpenMLS roster all agree.
|
|
Telegram cannot substitute a participant, commit, or archive key independently.
|
|
|
|
### D029: One recoverable transaction across group stores
|
|
|
|
Persist each group change through an encrypted write-ahead transaction spanning
|
|
the OpenMLS provider, archive epoch store, signed group ledger, and—only for a
|
|
local change—outbox. Recovery checks base revisions and exact already-written
|
|
values and replays only missing steps. Inbound and first-Welcome transactions
|
|
never contain outgoing envelopes. A new member initializes its local archive at
|
|
the current epoch and obtains any older authorized epochs through history
|
|
grants.
|
|
|
|
### D030: Telegram delivery order is untrusted
|
|
|
|
Stage signed transitions, commits, and archive distributions in a bounded
|
|
encrypted inbox keyed by stable application object identifiers. Assemble only
|
|
the unique next-generation bundle whose signed IDs and hashes match. Preserve
|
|
the bundle over restart, accept exact duplicates, treat object-ID reuse as a
|
|
conflict, and report multiple next-generation transitions as a fork. Never use
|
|
Telegram message order as protocol order.
|
|
|
|
### D031: Every protected transition advances MLS and archive epochs
|
|
|
|
Admissions use an MLS add commit, removals use one batch remove commit, and
|
|
role, ownership, and history-policy changes use an MLS self-update commit.
|
|
Every protected generation also creates a new archive epoch whose key is sent
|
|
inside an MLS application message after the commit. Keeping these three
|
|
generations aligned removes ambiguous unsigned policy boundaries and ensures a
|
|
removed client cannot decrypt the next archive key.
|
|
|
|
### D032: Account-signed freshness and identity gossip
|
|
|
|
A freshness response signs the random nonce, exact challenged checkpoint,
|
|
witness checkpoint, and witness account/client under the stable account key.
|
|
Only a client active at the challenged generation is a valid witness. Equal
|
|
checkpoints open sending, newer checkpoints force resynchronization, and an
|
|
equal-generation different hash signals a fork.
|
|
|
|
Identity gossip signs a checkpoint plus the sorted Telegram-user, account-ID,
|
|
and credential-hash observations for the full protected roster. Valid gossip
|
|
is compared with the reconstructed local state at that generation. A mismatch
|
|
creates a security event and never replaces a TOFU pin automatically. Telegram
|
|
can still censor all witnesses and preserve permanent isolation.
|
|
|
|
### D033: Own removal destroys active MLS state
|
|
|
|
After a valid removal commit excludes the local client, atomically replace its
|
|
active OpenMLS provider state with a protected tombstone bound to the signed
|
|
transition, resulting checkpoint, commit hash, account, and client. Advance the
|
|
signed group ledger but do not decrypt or append the new archive epoch. A later
|
|
admission requires a fresh account-authorized KeyPackage and Welcome. Preserve
|
|
the absent archive generations as an explicit access gap, and authenticate
|
|
stored content against the exact protected generation that activated its epoch.
|
|
|
|
### D034: KeyPackages are bounded, one-time admission tickets
|
|
|
|
Each client publishes account-signed, conversation-scoped KeyPackages for one
|
|
exact protected-group generation. Their MLS lifetime is 84 days and renewal
|
|
starts seven days before expiry or immediately after a generation change.
|
|
Still-valid replaced private packages remain in a bounded encrypted local pool
|
|
until a Welcome consumes one or they expire. Telegram stores only the signed
|
|
public publication and may suppress it, but cannot substitute a package,
|
|
move it to another protected conversation, or make a stale-generation package
|
|
authorize admission.
|
|
|
|
### D035: Separate control and content carrier streams
|
|
|
|
Use `protected-control.tde2e` for group authority, credentials, KeyPackages,
|
|
freshness, and safety gossip, and `protected-content.tde2e` for encrypted
|
|
messages, MLS content descriptors, and file manifests. File chunks use the
|
|
per-file carrier selected in D041. Keep every stream in the same private
|
|
Telegram carrier group with the same generic MIME type and empty caption. This
|
|
lets content use a protected incremental observation boundary without allowing
|
|
Telegram message order to become group authority.
|
|
|
|
### D036: Signed deterministic checkpoint gossip
|
|
|
|
After observing a current group checkpoint, each active account/client publishes
|
|
at most one deterministic account-signed gossip object for that checkpoint.
|
|
Count distinct reporter accounts as automatic witnesses. A valid future
|
|
checkpoint, equal-generation fork, or credential/Telegram binding conflict
|
|
blocks security-sensitive operation; malformed or unauthenticated gossip is
|
|
ignored. Human group, account, and pairwise safety codes remain available for
|
|
out-of-band comparison.
|
|
|
|
### D037: Desktop administration uses canonical transitions
|
|
|
|
Expose member removal, owner-controlled role changes, signed default-history
|
|
changes, and per-member history changes in the Desktop protected-group surface.
|
|
Do not mutate UI or Telegram membership as authority. Every action first passes
|
|
the freshness gate and protected capability checks, then uses the existing
|
|
OpenMLS removal or policy-change engine, archive rotation, write-ahead
|
|
transaction, exact-byte outbox, and vault-checkpoint update. A per-member
|
|
history change includes its HPKE history grant in that prepared transition.
|
|
|
|
### D038: Bound version-one large-group reconstruction
|
|
|
|
Permit at most 65,536 matching control objects and 512 MiB in one public
|
|
bootstrap/discovery/group-change reconstruction. Keep the signed local ledger
|
|
at the same 65,536-generation limit and retain the 4,096-member protocol ceiling
|
|
while testing for 500 participants. These are denial-of-service and lifecycle
|
|
bounds, not target group sizes. A protocol version that needs longer-lived
|
|
groups must add reviewed signed state snapshots and compaction instead of
|
|
silently increasing untrusted memory.
|
|
|
|
### D039: Fail-closed Desktop carrier interface
|
|
|
|
Recognize carrier presentation only by the reserved legacy/control/content or
|
|
vault filename together with the exact generic MIME type. Hide recognized
|
|
carrier objects from protected-client history elements and shared media, expose
|
|
only a generic encrypted-activity preview, and disallow forwarding or clipboard
|
|
export through ordinary message actions.
|
|
|
|
When a Telegram peer is mapped to an authenticated protected conversation, or
|
|
its loaded history contains reserved group-carrier metadata, replace the entire
|
|
ordinary compose surface with one action that opens the protected interface.
|
|
This blocks text, attachments, voice messages, bot commands, forwarding, and
|
|
editing through the plaintext Telegram sender. A matching unverified carrier
|
|
can therefore cause a local availability failure, but it cannot make the client
|
|
downgrade to plaintext. Cryptographic processing never trusts this UI marker;
|
|
the envelope, peer binding, signature, MLS state, and replay checks remain
|
|
authoritative.
|
|
|
|
### D040: Discover before asking for a vault password
|
|
|
|
Do not assume that a protected identity exists when the Desktop service is
|
|
constructed. On first opening the protected surface, issue one bounded Saved
|
|
Messages document search for the reserved vault filename without a password,
|
|
then verify the exact MIME type and size locally. Do not download carrier bytes
|
|
or walk the complete history during discovery. Show identity creation only
|
|
after an empty result and show password unlock only after a carrier is present.
|
|
Discovery never parses or accepts the encrypted bytes and cannot bypass normal
|
|
vault selection after password entry. Retryable discovery failure exposes only
|
|
a retry action, not an unsafe create-or-unlock guess.
|
|
|
|
### D041: Fetch file ciphertext only on explicit Save
|
|
|
|
Publish each new encrypted chunk under an exact carrier filename derived from
|
|
the manifest's random file identifier. Do not route this carrier into ordinary
|
|
content observation. An explicit Save performs a bounded, manifest-authorized
|
|
search after the manifest message, verifies and caches only matching chunks,
|
|
and atomically reconstructs the plaintext after all chunks arrive. Keep a
|
|
bounded `protected-content.tde2e` fallback for chunks uploaded by older clients.
|
|
This prevents a large file or hostile member from turning every chat refresh
|
|
into gigabytes of automatic downloads without changing Telegram's role as the
|
|
only opaque remote store.
|
|
|
|
### D042: Ambiguous Telegram searches fail closed
|
|
|
|
Treat `messages.messagesNotModified` as an unexpected retryable transport
|
|
failure for protected-carrier searches because every such request uses a zero
|
|
hash. Never interpret it as an empty result or a completed history page. During
|
|
passwordless vault discovery, accept absence only from a complete
|
|
`messages.messages` response or from a slice whose declared total equals the
|
|
returned first page. A malformed count or a partial first page without a valid
|
|
vault carrier exposes Retry only, never identity creation. The same malformed or
|
|
not-modified responses interrupt protected history synchronization instead of
|
|
silently truncating it.
|
|
|
|
Every MTP search response must also contain no more messages than the requested
|
|
page limit. Enforce the same limit again in every pagination controller before
|
|
parsing, retaining, or authenticating an object so future transport adapters
|
|
cannot accidentally weaken the boundary.
|
|
|
|
### D043: KeyPackage tickets commit to the Telegram author
|
|
|
|
Derive every client KeyPackage publication object ID from its conversation,
|
|
account, client, generation, account credential, KeyPackage bytes, and the
|
|
publishing Telegram user ID. The account authorization signs that object ID.
|
|
Before automatic admission, recompute the ID using the Telegram sender observed
|
|
for the carrier message and reject any mismatch. This prevents an active server
|
|
from transplanting a valid Alice publication into a message attributed to Bob.
|
|
|
|
This binding does not remove the first-contact limitation: Telegram can still
|
|
present a consistently false account view before any identity is pinned. It
|
|
does prevent independent relabeling of already signed package bytes. Legacy
|
|
pending joins with an unbound ticket generate and publish a new bound ticket on
|
|
local recovery; old unbound publications are never admitted.
|
|
|
|
### D044: New installations require a complete visible vault chain
|
|
|
|
When no local vault anchor exists, accept the Telegram history only if the
|
|
decrypted candidates start at generation one and every subsequent generation
|
|
increments by one and names the exact previous blob digest. Reject a missing
|
|
genesis, an internal gap, a duplicate generation, or a transition between
|
|
parallel branches. An existing local anchor continues to permit older history
|
|
before the anchor while requiring an exact contiguous suffix from it.
|
|
|
|
This does not solve absolute rollback on a new installation. Telegram may still
|
|
show a complete stale prefix ending before the real latest generation. The rule
|
|
does ensure that suppression cannot be hidden inside the visible history and
|
|
that the selector never assembles a newest vault from incompatible branches.
|
|
|
|
### D045: Deferred MLS content cannot advance synchronization
|
|
|
|
Stop content replay when an authenticated MLS application reports a deferred
|
|
result. Return a retry-required completion and keep the previous persisted
|
|
Telegram boundary unchanged. On a later run, replay the complete suffix and
|
|
rely on object IDs and the inbound journal to skip applications that were
|
|
already committed before the deferred object.
|
|
|
|
Never classify a deferred application as ignored. The overlap retains only a
|
|
bounded recent suffix, so advancing the boundary after a deferred object could
|
|
make older ciphertext permanently unreachable even though its failure was
|
|
explicitly retryable.
|
|
|
|
### D046: Exact manifest duplicates retain the earliest carrier position
|
|
|
|
Keep the minimum positive Telegram message identifier observed for identical
|
|
authenticated content in the protected content index. A newer duplicate must
|
|
not replace the original manifest position: file chunks are published after
|
|
the original manifest, so using the duplicate as the exclusive search boundary
|
|
could permanently exclude valid chunks between the two copies.
|
|
|
|
The identifier remains an untrusted transport hint and is not included in file
|
|
identity, signatures, or encryption context. Updating it rewrites only the
|
|
authenticated index, leaving the immutable content record and its hash
|
|
unchanged. Legacy file-manifest indexes migrate to the conservative boundary
|
|
one so already stored files remain recoverable even when an older client kept
|
|
only a later duplicate position.
|
|
|
|
### D047: Manifest authorization failure stops content synchronization
|
|
|
|
Advance content synchronization past a file manifest only after both its
|
|
immutable content record and protected chunk authorization are durable. Treat
|
|
authorization quota exhaustion like any other local persistence failure rather
|
|
than silently reporting the manifest as processed. A later retry may reuse the
|
|
already stored content record and finish authorization without losing the
|
|
manifest behind the saved Telegram boundary.
|
|
|
|
Chunk-cache quota exhaustion during an explicit Save remains a bounded download
|
|
failure: it cannot advance ordinary content synchronization and the user may
|
|
retry after recovering local capacity.
|
|
|
|
### D048: Publish-state durability precedes manifest outbox removal
|
|
|
|
After Telegram accepts the final MLS manifest envelope, persist the file
|
|
transfer's `manifestPublished` state before removing that exact envelope from
|
|
the protected outbox. If this pre-acknowledgement persistence step fails, keep
|
|
the exact bytes queued and retry them with the same Telegram random identifier.
|
|
Unrelated queued messages do not prevent this transition.
|
|
|
|
Never regenerate a manifest pair merely because its final local state write
|
|
failed. The archived content preparation uses fresh randomized encryption, so
|
|
regeneration under the same event and content object identifiers would create
|
|
competing authenticated hashes and security-block recipients. Chunks start only
|
|
after the durable publish state proves the final manifest envelope was accepted.
|
|
The same pre-acknowledgement rule applies when a restarted active group drains
|
|
its persisted outbox through startup recovery, and successful group observation
|
|
must resume a pending chunk transfer even when that outbox is already empty.
|
|
|
|
### D049: Release durable file-chunk cache entries after use
|
|
|
|
Keep an outgoing chunk's exact ciphertext until Telegram accepts it and the
|
|
next chunk cursor is durably committed. Then remove that cache entry. After a
|
|
downloaded file is fully authenticated, atomically saved, and hash-verified,
|
|
remove its cached chunks while retaining the small manifest authorization.
|
|
|
|
Removal is idempotent and best-effort after the durability boundary. A failed
|
|
cleanup cannot invalidate a completed upload or saved output, while successful
|
|
cleanup returns the protected bytes to the bounded cache quota. Retaining the
|
|
manifest authorization allows a later Save to authenticate freshly downloaded
|
|
chunks without reprocessing chat history.
|
|
|
|
### D050: A new file cannot replace an unfinished transfer
|
|
|
|
Reject a new protected-file send while the conversation has a durable pending
|
|
file transfer. Perform this check before hashing the selected source so a busy
|
|
conversation does not synchronously scan an unrelated file.
|
|
|
|
Never replace the persisted transfer implicitly. Its manifest or some of its
|
|
chunks may already be present in Telegram, and replacing its local cursor would
|
|
make that authenticated file permanently incomplete while leaving no state from
|
|
which upload recovery could resume.
|
|
|
|
### D051: Full-file hashing never blocks the interface thread
|
|
|
|
Hash a newly selected source on a worker before creating its durable transfer.
|
|
Keep the prepared path and digest attached to the conversation while control
|
|
synchronization finishes, then construct the manifest from the latest accepted
|
|
group state. Reject another file selection until that preparation is consumed.
|
|
|
|
Do not synchronously hash the same source again before queuing its manifest; a
|
|
size and readability check is sufficient because chunk encryption reads the
|
|
source afterward and recipients verify the manifest's full plaintext hash. Once
|
|
all chunks are accepted, verify the source hash again on a worker before clearing
|
|
the durable transfer. Operation epochs and transfer identity prevent late worker
|
|
results from mutating a relocked or different conversation state.
|
|
|
|
### D052: Saved files are assembled cooperatively
|
|
|
|
After all encrypted chunks are available, decrypt, hash, and append one chunk
|
|
per interface-loop turn instead of assembling the complete file in one call.
|
|
Keep the manifest, digest context, and atomic output owned by the pending Save
|
|
operation, and bind every continuation to its event object and operation epoch.
|
|
Discover cached chunks with bounded filesystem metadata probes; authenticate
|
|
their full local records only when the cooperative writer consumes each chunk.
|
|
|
|
Commit the destination only after the reconstructed plaintext size and SHA-256
|
|
match the authenticated manifest. Release cached chunks afterward in bounded
|
|
batches, then report success. Cancelling, locking, or destroying the group drops
|
|
the pending atomic output without exposing a partial destination file.
|
|
|
|
### D053: File-transfer cancellation is durable and explicit
|
|
|
|
Allow the user to cancel an unfinished outgoing file after confirmation. First
|
|
persist `cancelRequested` in the protected transfer snapshot. A cancelled
|
|
transfer cannot publish its manifest, advance its chunk cursor, or be replaced
|
|
by another transfer.
|
|
|
|
Remove the manifest event/content pair from the persistent outbox in one atomic
|
|
rewrite, release the current retry chunk, and only then clear the transfer. If
|
|
the app stops between these steps, startup observes the cancellation marker and
|
|
finishes cleanup before any outbox recovery upload. Failed cleanup remains
|
|
retryable and never silently resumes the cancelled file.
|
|
|
|
### D054: Administration waits for the outgoing pipeline
|
|
|
|
Reject a local administrative transition while file preparation, a durable file
|
|
transfer, the direct transport, the protected outbox, or its upload controller
|
|
has work. An administrative transition moves the group into the vault-update
|
|
state machine, so an older upload callback would otherwise target a group that
|
|
is no longer in the active-group map and fail to acknowledge accepted bytes.
|
|
|
|
The user may retry the role, removal, or history-policy action after the active
|
|
operation reaches its durability or callback boundary.
|
|
|
|
### D055: Transient file work serializes control transitions
|
|
|
|
Do not start control observation while a source hash, final source verification,
|
|
or on-demand file Save is active. A verified control transition temporarily
|
|
moves the conversation through the vault-update state machine, while those
|
|
continuations deliberately resolve their conversation in the active-group map.
|
|
Letting both proceed could drop a worker or cooperative-write continuation and
|
|
leave the file operation permanently busy.
|
|
|
|
Reject a new Save while control observation is active or already queued, and
|
|
reject local administration while a Save is active. A control notification that
|
|
arrives during transient file work remains dirty and automatically starts after
|
|
the file operation finishes. Durable outgoing file transfers may still cross a
|
|
remote transition because their manifest, cursor, and source path survive the
|
|
state-machine move and resume from persistent state.
|
|
|
|
### D056: Control observation precedes content observation
|
|
|
|
Run at most one control or content observation for a conversation at a time.
|
|
Control has priority because it may replace the MLS roster and archive epoch,
|
|
then temporarily move the conversation through the vault-update state machine.
|
|
A content callback that completed during that move would otherwise miss the
|
|
active-group map and leave its finished controller permanently installed.
|
|
|
|
Coalesce content notifications while control is active. If a control
|
|
notification arrives during content observation, keep it dirty and start it as
|
|
soon as the content controller finishes, including after a retryable content
|
|
failure. The normal outgoing pump restarts deferred content after control is
|
|
settled. Local administration waits for an active content observation for the
|
|
same callback-lifetime reason.
|
|
|
|
### D057: Cancellation survives an in-flight carrier upload
|
|
|
|
Persist a file-transfer cancellation request even while the manifest outbox or
|
|
one direct chunk upload is active. Do not report a false rejection merely
|
|
because Telegram has not completed its current callback. Keep the transfer in a
|
|
synchronizing state until that callback reaches its durability boundary, then
|
|
remove the manifest pair and current retry chunk before clearing the transfer.
|
|
|
|
If Telegram accepted a chunk after cancellation was requested, do not advance
|
|
the durable chunk cursor. The accepted ciphertext becomes an unusable opaque
|
|
orphan and local cleanup removes its retry copy. Manifest acknowledgement still
|
|
finishes normally before cancellation cleanup, so an accepted Telegram object
|
|
is never confused with an unacknowledged local outbox entry. Retryable and
|
|
permanent callback results also resume the durable cleanup instead of leaving
|
|
the cancellation marker stuck until restart.
|
|
|
|
### D058: Permanent transport failures remain manually retryable
|
|
|
|
Treat `PermanentTransportError` as a prohibition on automatic retry, not as a
|
|
requirement to restart the client. A peer can regain write access, an account
|
|
can rejoin a group, and transport-side validation conditions can change while
|
|
the process remains open. Expose an explicit Retry action for vault discovery,
|
|
password-backed vault loading, and pending group publication or join.
|
|
|
|
Retry the same durable group operation and identifiers when one exists. If a
|
|
failed vault creation already discarded its pending identity, retry performs a
|
|
fresh password-backed cloud selection; a still-missing vault then returns to
|
|
the normal confirmed creation form. Security-blocked and local-persistence
|
|
failures never use this path, and no transport state retries automatically.
|
|
|
|
### D059: Lock closes protected plaintext surfaces
|
|
|
|
Close every protected conversation, file, group-list, and security dialog as
|
|
soon as the vault leaves `Ready`. These surfaces may already contain decrypted
|
|
messages, filenames, member safety codes, or an unsent draft, so leaving their
|
|
widgets alive after a manual or passcode lock would expose stale plaintext even
|
|
though the service has discarded its keys. Clear both password fields when the
|
|
main protected-groups dialog returns to `Locked` as well.
|
|
|
|
Keep content, file-transfer, and security revision streams monotonic across a
|
|
lock and explicitly publish a revision after clearing the protected state.
|
|
Resetting a revision to zero can repeat its initial value and suppress the
|
|
reactive notification. Publish security revisions for verified roster and
|
|
history-policy changes too, so an open security view cannot retain an older
|
|
membership state before the lock boundary.
|
|
|
|
### D060: Security failures destroy the unlocked runtime
|
|
|
|
Treat `SecurityBlocked` as a fail-closed boundary, not only as a status shown by
|
|
the interface. Once a rollback, fork, identity conflict, invalid pagination, or
|
|
authenticated-content conflict selects that state, defer one lock operation
|
|
until the current callback returns. The lock cancels controllers, destroys the
|
|
unlocked vault and group objects, cleanses remembered passwords, and invalidates
|
|
worker continuations without deleting the persistent encrypted recovery state.
|
|
|
|
While that deferred cleanup is pending, require `Ready` and an unlocked vault
|
|
at every public plaintext, send, file, administration, synchronization, and
|
|
active pipeline entry point. In-flight observation or upload callbacks must not
|
|
write, acknowledge, or publish after a separate callback has already detected a
|
|
security failure. A manual lock that wins the race makes the deferred cleanup a
|
|
no-op, and assigning `SecurityBlocked` with no unlocked runtime does not create
|
|
a lock loop.
|
|
|
|
### D061: Admission and discovery failures remain retryable
|
|
|
|
An incoming client waiting for admission has no protected conversation surface
|
|
from which to request synchronization. If its control observation ends with a
|
|
retryable or permanent transport error, publish the corresponding group state
|
|
so the main protected-groups dialog exposes Retry instead of remaining forever
|
|
in `AwaitingAdmission`.
|
|
|
|
Retry restarts every stalled admission observation already restored in the
|
|
active-group map. Collect conversation identifiers before starting controllers,
|
|
because a synchronous completion may move a group into the vault-update state
|
|
machine and invalidate map iterators. If an admission completes synchronously,
|
|
preserve the state selected by that completion instead of writing the waiting
|
|
state over it.
|
|
|
|
Keep a peer in the discovery queue after both retryable and permanent transport
|
|
failures. Neither state retries automatically; the existing explicit Retry
|
|
action returns the group state to Ready and resumes the queued discovery. A
|
|
missing or non-protected bootstrap is still a completed negative discovery and
|
|
is not retried.
|
|
|
|
Failure to start an admission observation or discovery request is retryable as
|
|
well. Do not leave admission in a waiting state without a live controller, and
|
|
do not discard a discovery peer before a request actually starts. In both
|
|
cases, preserve the durable in-memory operation and restore the Retry action.
|
|
|
|
Existing protected conversations remain accessible from the main dialog while
|
|
another group is publishing, awaiting admission, retryable, or locally failed.
|
|
The global creation state controls creation and retry actions, but it must not
|
|
hide unrelated active groups or force a lock-and-unlock cycle merely to read
|
|
them.
|
|
|
|
### D062: Open protected views never keep stale authority data
|
|
|
|
Close security and member-management dialogs when the verified security
|
|
revision changes. Their action buttons and safety codes describe one atomic
|
|
membership snapshot; updating only their text could leave a revoked action or
|
|
an obsolete verification code visible. Reopening the dialog constructs every
|
|
control from the new verified snapshot, while the service still rechecks
|
|
authorization before accepting an action.
|
|
|
|
Rebuild an open protected-file list whenever the content revision changes.
|
|
The list remains bounded by the existing visible-page limit and therefore can
|
|
show newly synchronized manifests without retaining an unbounded plaintext
|
|
snapshot or requiring the user to close the conversation.
|
|
|
|
Refresh an open conversation's status and input availability on the same
|
|
security revision. Removal or re-admission changes the active phase without
|
|
necessarily adding content, so waiting for a content notification would leave
|
|
the field looking writable even though the service correctly rejects the send.
|
|
|
|
### D063: A control observer must exist before showing synchronization
|
|
|
|
If a control observation cannot start, retain its dirty marker but expose a
|
|
retryable content error instead of `Synchronizing`. For a client awaiting
|
|
admission, expose the same retryable failure through the group state so restart
|
|
recovery cannot leave the main dialog waiting without a live controller.
|
|
|
|
Starting a controller may synchronously invoke its completion callback. Any
|
|
caller that wants to inspect the group afterward must find it again by
|
|
conversation identifier, because processing a verified control page may move
|
|
the group into the durable vault-update state machine. Never dereference the
|
|
pre-start map entry or group reference across that callback boundary.
|
|
|
|
### D064: File-download security failures lock the vault
|
|
|
|
Treat an authenticated file-chunk conflict, invalid pagination, or exceeded
|
|
download bound as the same fail-closed boundary as a protected message or
|
|
control conflict. A file-save callback returning `SecurityBlocked` is not
|
|
sufficient while the unlocked service can still send or administer the group.
|
|
|
|
Set both the vault and conversation states to `SecurityBlocked` before
|
|
finishing the transient file operation. This prevents its cleanup from
|
|
starting another observation, still delivers the failure to the UI callback,
|
|
and lets the existing deferred security lock destroy all unlocked runtime
|
|
state immediately afterward.
|
|
|
|
### D065: Vault-changing group operations are globally serialized
|
|
|
|
Starting a protected group or an administrative transition is allowed only
|
|
while the global group operation is `Idle` or `Ready`. Administration also
|
|
requires that no creation, indexed join, or discovery controller owns the
|
|
global slot. Reading and ordinary protected sends in unrelated groups remain
|
|
available because they do not rewrite the account vault index.
|
|
|
|
Without this gate, a discovery callback could finish while administration of a
|
|
different group had moved that group into the pending vault-update state. The
|
|
join would then fail only because the slot was occupied and could overwrite the
|
|
real operation state with `LocalFailure`, leaving both operations without a
|
|
correct recovery action.
|
|
|
|
### D066: Discovery owns the global group-operation slot
|
|
|
|
Start queued group discovery only while the global group operation is `Idle`
|
|
or `Ready`, and publish `Preparing` before invoking its controller. The start
|
|
can complete synchronously, so assigning the state afterward would overwrite
|
|
the completion result. Existing chats remain accessible under D061, but the UI
|
|
does not offer another vault-changing action while discovery is live.
|
|
|
|
Keep discovery requests queued while admission or freshness is unresolved.
|
|
After a successful verified control observation, try the queue again; this is
|
|
the boundary that can turn `AwaitingFreshness` back into `Ready`. Pending
|
|
creation or join still takes precedence and naturally keeps discovery queued.
|
|
|
|
### D067: Global vault work defers every control boundary
|
|
|
|
Do not start a group control observation while creation, indexed join, or
|
|
discovery owns the global operation slot. If an already-running observation
|
|
finishes after another group takes that slot, first honor an unambiguous
|
|
security failure, otherwise discard its transient page, retain
|
|
`observationDirty`, and do not advance the persistent Telegram boundary. The
|
|
page will be fetched and authenticated again from the old boundary.
|
|
|
|
When the slot is released, collect dirty conversation identifiers and restart
|
|
their observers one by one. Stop if a synchronous completion claims the slot
|
|
again. Discovery also waits until no control observer is running or dirty, so a
|
|
new control notification received during discovery is deferred rather than
|
|
raced. Resume this work after publication and after retryable, permanent,
|
|
start, or local join/discovery failures.
|
|
|
|
### D068: Accepted outgoing file chunks are cleaned before state is forgotten
|
|
|
|
After advancing the durable outgoing-file cursor, remove every cached chunk
|
|
strictly before that cursor before preparing more work. A stop between the
|
|
cursor commit and the original best-effort removal can therefore leave at most
|
|
a recoverable cache entry, and restart removes it without discarding the
|
|
current chunk whose exact ciphertext is required for a safe retry.
|
|
|
|
Cancellation removes the complete file chunk range before clearing the durable
|
|
transfer. Successful finalization repeats the same check immediately before
|
|
clearing, after the final source hash has completed. A cleanup failure retains
|
|
the transfer and becomes a local failure instead of forgetting the only index
|
|
from which the encrypted cache can be reclaimed.
|
|
|
|
### D069: Chunk-store lock contention never blocks the interface thread
|
|
|
|
Acquire per-record file locks without waiting. Chunk preparation, manifest
|
|
authorization, and cache cleanup run from interface-driven callbacks, so a
|
|
five-second lock wait can freeze the entire client and a prefix cleanup can
|
|
multiply that pause by the number of cached records. Contention now fails the
|
|
current operation immediately while preserving its durable state for recovery.
|
|
|
|
### D070: Removal durably discards every pending outgoing object
|
|
|
|
When a verified control transition removes the local client, persist
|
|
cancellation of any outgoing file, reclaim its cached chunks, and atomically
|
|
clear the remaining protected outbox before publishing the updated vault index.
|
|
Freshness challenges and other objects queued before removal must never be sent
|
|
after the client has lost membership.
|
|
|
|
Run the same cleanup from restored vault-update work. A transient cleanup or
|
|
outbox persistence failure remains a local recoverable operation with its
|
|
durable state intact; it no longer masquerades as authenticated vault damage
|
|
and no longer forces a global security lock during unlock.
|
|
|
|
### D071: Cancellation waits for final source verification
|
|
|
|
Keep a cancelled transfer durable while its final source-hash worker is still
|
|
running. The worker already owns the single `fileFinalHashInProgress` slot and
|
|
will resume the outgoing pump after observing the cancellation marker. Clearing
|
|
the transfer early would let a second file reach finalization while that slot
|
|
still belonged to the first file, leaving the second transfer synchronized
|
|
forever with no continuation able to restart it.
|
|
|
|
### D072: Equal content states still notify every conversation
|
|
|
|
Keep the per-conversation content-state map authoritative, but force the shared
|
|
reactive notifier to emit for every map update. Protected conversation views use
|
|
that producer as an invalidation signal and then read their own conversation's
|
|
state. A normal equality-filtered assignment loses the second notification when
|
|
two different groups consecutively enter the same enum state, leaving one view
|
|
with stale status and controls even though the service state is correct.
|
|
|
|
### D073: File cancellation covers hashing as well as upload
|
|
|
|
Expose file preparation as a pending transfer immediately, prevent text from
|
|
being queued behind it, and let the existing cancel action stop both the initial
|
|
and final source-hash workers. Each worker owns a distinct atomic cancellation
|
|
token; its callback must still match that token before changing group state, so
|
|
a late completion can never finish a newer file operation. Destroying a group
|
|
also signals both tokens, preventing lock or shutdown from continuing to read a
|
|
large plaintext file in the background.
|
|
|
|
### D074: Protected peers never downgrade after local recognition
|
|
|
|
Persist a monotonic account-local set of Telegram peer bindings as soon as a
|
|
protected group is created, restored, indexed by the authenticated unlocked
|
|
vault, or accepted from a verified protected bootstrap. The ordinary Telegram
|
|
composer must consult this set even while the E2E vault is locked. Removing,
|
|
unloading, or hiding the carrier object therefore cannot turn a previously
|
|
known protected group back into a plaintext chat.
|
|
|
|
The marker contains no secret beyond a Telegram peer binding already known to
|
|
the local account and lives in Telegram Desktop's encrypted account-local
|
|
preferences. Flush the preference synchronously at the recognition boundary;
|
|
the normal delayed settings write leaves a crash window in which an active
|
|
server could remove the carrier before restart and regain a plaintext composer.
|
|
The marker is deliberately never pruned from server history. Invalid or
|
|
oversized marker state fails toward confidentiality by treating every group
|
|
peer as protected instead of silently re-enabling plaintext composition.
|
|
Unauthenticated reserved metadata continues to block composition while present,
|
|
but cannot write a permanent marker and give an ordinary group member a lasting
|
|
local denial-of-service primitive.
|
|
|
|
### D075: One Telegram carrier peer maps to one protected conversation
|
|
|
|
Require the authenticated account-vault conversation index to contain unique
|
|
Telegram peer bindings as well as unique sorted conversation identifiers. Two
|
|
independent MLS and archive states must never observe the same Telegram carrier
|
|
group as their authoritative transport. Reject an ambiguous index during both
|
|
encoding and authenticated decoding, so the client blocks rather than choosing
|
|
one conversation based on iteration or UI state.
|
|
|
|
### D076: Authenticated markers survive Telegram peer migration
|
|
|
|
Check the durable protected-peer marker before trusting the current Telegram
|
|
peer classification. When Telegram Desktop links a migrated supergroup to an
|
|
older basic group whose marker is authenticated, immediately persist the new
|
|
peer as another monotonic protected presentation alias. The migrated chat then
|
|
keeps its plaintext composer disabled even after the old carrier is unloaded or
|
|
the server later hides that migration edge. Opening the protected surface also
|
|
falls back through the migrated history binding to the original conversation.
|
|
|
|
### D077: Identity creation repeats passwordless discovery
|
|
|
|
Treat a previous `Missing` result as a snapshot, not as durable proof that the
|
|
Telegram account still has no protected identity. Opening the protected surface
|
|
again repeats the bounded Saved Messages discovery. More importantly, pressing
|
|
Create starts another passwordless discovery before generating any identity or
|
|
uploading a vault. If another device has created an identity since the form was
|
|
shown, switch to the unlock flow and discard the proposed password instead of
|
|
publishing a competing account identity.
|
|
|
|
This preflight narrows honest multi-device races but does not claim an atomic
|
|
compare-and-swap from Telegram storage. A server that hides first-contact
|
|
metadata remains covered by the documented first-contact limitation.
|
|
|
|
### D078: Deferred file completions stay inside one lock epoch
|
|
|
|
Bind every Desktop file-download completion to both the service operation epoch
|
|
and the manifest event that started the Save request. The transport controller
|
|
can finish on a non-interface thread and defer its completion to the next main
|
|
turn. A lock destroys the old group runtime, but the service object survives and
|
|
can later restore the same conversation identifier. Without both bindings, a
|
|
late old completion could mistake a new Save request in that conversation for
|
|
its own operation, reset its controller, or begin writing the wrong manifest.
|
|
|
|
An epoch mismatch or event mismatch now drops the stale callback without
|
|
touching the restored group or its user callback.
|
|
|
|
### D079: Loaded carriers survive a locked-vault interval
|
|
|
|
After the vault becomes authenticated and ready through either unlock or first
|
|
identity creation, run local-group recovery, enumerate only the Telegram group
|
|
histories already loaded in the session, and queue ordinary protected-group
|
|
discovery for histories that retain carrier metadata. A carrier received while
|
|
the vault was unavailable would otherwise be forgotten because the new-item
|
|
notification is not replayed merely by making the vault ready.
|
|
|
|
The loaded metadata remains only a discovery trigger. It cannot decrypt,
|
|
enroll, index, or advance protected state; the existing authenticated bootstrap
|
|
pipeline still verifies every accepted byte. Do not turn this recovery into a
|
|
full Telegram history scan.
|
|
|
|
### D080: Admission retry preserves synchronous outcomes
|
|
|
|
An admission observation may complete synchronously while Retry is starting
|
|
it. Stop the retry loop immediately if that completion claims the global group
|
|
operation slot; the remaining dirty admissions will resume when the slot is
|
|
released. Likewise, retain a synchronous permanent, local, or security result
|
|
instead of replacing it with a generic retryable failure merely because no live
|
|
observer remains after `beginGroupObservation` returns.
|
|
|
|
This is required for multiple waiting groups as well as test transports. A
|
|
synchronous completion of the first group can move it into the vault-update
|
|
state while later admission groups are still present in the active map.
|
|
|
|
### D081: Unanchored discovery cannot block the account vault
|
|
|
|
Object conflicts, ambiguous bootstraps, capacity excess, and invalid pagination
|
|
while discovering an unindexed Telegram carrier group reject that discovery
|
|
attempt and release the global operation slot. They do not security-block the
|
|
authenticated account vault or destroy established protected-group runtimes.
|
|
Before a verified bootstrap is accepted there is no cryptographic trust anchor
|
|
linking that group to the account, so an ordinary group member or active server
|
|
can construct these inputs at will.
|
|
|
|
This rule narrows only first-contact availability scope. No conflicting bytes
|
|
are accepted, no peer is added to the vault, and another carrier notification
|
|
may trigger a fresh bounded discovery later. The same conflict in an indexed or
|
|
already authenticated group remains a fail-closed security block.
|
|
|
|
### D082: Password KDF work never blocks the interface thread
|
|
|
|
Run cloud-vault selection and first-vault creation on a background worker. The
|
|
64 MiB Argon2id derivation is intentionally expensive and must not freeze the
|
|
Protected Groups box, window painting, or its Close action while a password is
|
|
being checked. A controller-owned completion guard discards selection results
|
|
after cancellation or destruction; first creation additionally binds its result
|
|
to the service operation epoch so locking or restarting cannot adopt stale key
|
|
material.
|
|
|
|
The worker owns independent crypto providers and all input copies. It returns
|
|
only the completed move-only vault result to the main thread, where persistent
|
|
anchors, uploads, UI state, and service-owned secrets continue to change.
|
|
|
|
### D083: Pagination work shares the version-one lifecycle bound
|
|
|
|
Limit every generic long-running carrier scan to 65,536 pages and 64 MiB of
|
|
retained encoded cursors. The earlier million-page allowance was finite in a
|
|
type-theoretic sense but still let an active server impose effectively
|
|
unbounded network work and excessive duplicate-tracking memory. The new bound
|
|
still exceeds the minimum pages needed for the existing 65,536-object control
|
|
lifecycle and million-record content-store ceiling at the 100-object transport
|
|
page size.
|
|
|
|
Exhaustion remains an explicit invalid-pagination failure. No partial scan is
|
|
committed as complete and no unauthenticated object becomes authoritative.
|
|
|
|
### D084: Every asynchronous transport attempt has a generation token
|
|
|
|
Bind each page request, vault discovery, vault selection, outbox attempt, and
|
|
two-stage carrier upload to a monotonically advancing in-memory token. Object
|
|
identifiers and a generic `requestActive` flag are insufficient because a
|
|
cancelled controller may restart with the same object or while a newer page is
|
|
already active. A duplicated or delayed callback from the older operation must
|
|
be ignored without consuming the new operation's active slot.
|
|
|
|
Lifetime guards still prevent callbacks after destruction. Generation tokens
|
|
cover the distinct case where the owner remains alive and deliberately retries
|
|
the exact same authenticated ciphertext or reuses the controller.
|
|
|
|
### D085: Carrier downloads do not own unrelated document loads
|
|
|
|
Reuse a Telegram document that is already loaded or loading instead of changing
|
|
its destination or cancelling it. Start at most one module-owned download for a
|
|
shared `DocumentData`, even when Telegram returns duplicate messages for the
|
|
same document. Only a download started by the protected transport may be
|
|
cancelled and have its temporary location cleared when the page completes or
|
|
the backend is destroyed.
|
|
|
|
Clearing a module-owned temporary location removes both the live document
|
|
location and its account-local location record, including filename-pair and
|
|
alias bookkeeping. This prevents deleted temporary paths from accumulating in
|
|
Telegram Desktop's persistent media-location map.
|
|
|
|
### D086: Protected groups use the native history and composer
|
|
|
|
Supersede the presentation part of D039 while retaining its carrier-hiding and
|
|
fail-closed requirements. Materialize only authenticated records from the local
|
|
protected content store as silent client-side items in the bound Telegram group
|
|
history. Mark every item visibly as encrypted and prohibit Telegram reply,
|
|
forward, pin, and reaction actions. Keep materialization bounded
|
|
to the newest 200 records and extend it in 200-record pages when the native
|
|
history approaches its top.
|
|
|
|
For an unlocked active protected group, retain the ordinary Telegram text
|
|
field, send button, and local-file attachment picker. Intercept text before rich
|
|
message or ordinary send processing and route it to `sendProtectedText`; route
|
|
local file paths to `sendProtectedFile`. Disable Telegram cloud drafts and link
|
|
preview resolution for the protected composer. Hide or reject voice, bots,
|
|
stickers, GIFs, inline results, contacts, send-as, scheduling, forwarding,
|
|
in-memory media without a local path, and any stale stock send dialog.
|
|
If no authenticated conversation runtime is available, retain D039's single
|
|
unlock or recovery action and never fall back to plaintext Telegram sending.
|
|
|
|
### D087: File previews are authenticated protected content
|
|
|
|
Generate a bounded JPEG thumbnail locally while the original file is hashed.
|
|
Store that thumbnail, its original dimensions, and media duration inside the
|
|
encrypted file manifest. Telegram receives only the opaque manifest carrier
|
|
and ciphertext chunks; it never receives a plaintext thumbnail, filename,
|
|
MIME value, dimensions, or duration from the protected presentation.
|
|
|
|
Materialize a protected file as a native local document item with the decrypted
|
|
thumbnail and a lock caption. Clicking an uncached item runs the authenticated
|
|
protected-file restoration flow and opens only the hash-verified result. Keep
|
|
manifest revision 2 readable without a preview and emit revision 3 for new
|
|
files. Previously sent revision-2 files remain generic because no thumbnail
|
|
exists in their authenticated ciphertext.
|
|
|
|
### D088: Clipboard images use the protected file pipeline
|
|
|
|
Supersede D086's rejection of in-memory media for non-animated images. Encode a
|
|
pasted image as an owner-only staged PNG, then pass it through the ordinary
|
|
protected-file hash, encrypted manifest, chunk encryption, freshness, and
|
|
authenticated upload flow. Never upload the staged PNG through Telegram's
|
|
ordinary photo API and never expose its path as a durable local history cache.
|
|
|
|
Remove the staged plaintext after successful final hash verification, durable
|
|
cancellation, or any preparation failure. A staged source that belongs to a
|
|
persisted transfer remains available across a restart until that exact transfer
|
|
finishes or is cancelled.
|
|
|
|
### D089: Message changes are immutable authenticated events
|
|
|
|
Represent edits and deletions as encrypted version-two message-body events that
|
|
reference the immutable original event identifier. Telegram retains the opaque
|
|
original and mutation carriers; clients fold the authenticated events into the
|
|
native timeline. A deletion is a permanent tombstone, while the newest valid
|
|
edit supplies the visible text and native edited badge.
|
|
|
|
Accept a mutation only when its authenticated account identifier equals the
|
|
original message or file author's account identifier. Any authorized device of
|
|
that account may therefore change its own content, while Telegram and other
|
|
participants cannot forge the operation. Editing is limited to text; deletion
|
|
also applies to protected files. The ordinary Telegram edit field and delete
|
|
confirmation route to these events and never call Telegram's edit or delete
|
|
message APIs for decrypted protected items.
|
|
|
|
### D090: Selected files use durable staging and automatic retries
|
|
|
|
Copy every selected local file into an owner-only conversation staging path
|
|
while computing the manifest hash, and persist that staging path as the source
|
|
of the outgoing transfer. Temporary picker paths and later removal of the
|
|
original file must not truncate a multi-chunk upload. Remove the staged source
|
|
only after full source-hash verification or durable cancellation.
|
|
|
|
Retry transient manifest and chunk uploads automatically with bounded
|
|
exponential delay. A Save operation also retries transient downloads and
|
|
temporarily missing chunks while an authenticated sender may still be
|
|
publishing them. Keep retries bounded, retain already authenticated chunks, and
|
|
commit the destination only after its exact size and full hash match.
|
|
|
|
### D091: Retry the control observation that gates file recovery
|
|
|
|
An interrupted account authorization or transient Telegram failure may leave a
|
|
restored group control observation marked dirty before a persisted file transfer
|
|
can resume. Retry that observation automatically with bounded exponential
|
|
delay. A successful request invalidates older retry callbacks; permanent
|
|
transport failures remain explicit and require user retry.
|
|
|
|
The retry belongs to the protected-group control plane. It does not admit,
|
|
delay, pace, or own MTProto media/upload sessions. Once observation succeeds,
|
|
the existing durable transfer continues from its saved chunk cursor and exact
|
|
manifest instead of replacing the transfer or regenerating ciphertext.
|
|
|
|
### D092: Windows chunk locks cannot leave restart-blocking files
|
|
|
|
Use a non-waiting per-record Windows kernel mutex for protected chunk cache
|
|
authorization, storage, and cleanup. The kernel releases mutex ownership when
|
|
the client exits or crashes, so a forced restart cannot strand `QLockFile`
|
|
artifacts that recursively grow `.rmlock` suffixes and permanently block a
|
|
durable transfer. Retain the existing non-waiting `QLockFile` on other systems.
|
|
|
|
While holding the mutex, remove only exact legacy lock paths left by earlier
|
|
Windows builds. Windows refuses that removal while another process still owns
|
|
the old file handle, preserving contention safety. Retry each existing exact
|
|
legacy file for at most 500 milliseconds because antivirus or indexing readers
|
|
can make a single Windows deletion attempt fail even after the owner exited.
|
|
Spend that bounded retry budget only after the legacy record identifies an
|
|
exited process; live or unreadable owners fail immediately, retaining the
|
|
non-blocking contention contract. Do not route deletion back through
|
|
`QLockFile`, whose one-shot explicit stale removal recreated the permanent
|
|
failure for the original base lock. The migration changes only the local
|
|
encrypted chunk cache; Telegram upload sessions and their MTProto connection
|
|
lifecycle remain untouched.
|
|
|
|
### D093: A durable file transfer does not block protected messages
|
|
|
|
Allow protected text, edit, and delete events to enter the persistent outbox
|
|
while an already-durable file transfer is publishing chunks. Materialize those
|
|
events locally at once and keep the existing file-transfer pump ahead of the
|
|
ordinary outbox, so the file resumes from its committed cursor and queued chat
|
|
events follow without replacing or regenerating any encrypted file object.
|
|
|
|
Initial file staging and hashing remain exclusive because their durable transfer
|
|
does not exist yet, and a second file remains rejected until the first transfer
|
|
finishes or is cancelled. A busy-file rejection is an expected user-visible
|
|
state, not a local cryptographic or persistence failure. This policy changes
|
|
only protected-content queueing; it does not alter MTProto upload-session
|
|
ownership, pacing, endpoints, or connection selection.
|
|
|
|
### D094: Carrier metadata hides content only in protected peers
|
|
|
|
Hide a group carrier from the native Telegram timeline only after the peer is
|
|
known to protected presentation state. A document in an ordinary group that
|
|
happens to use the reserved carrier filename and MIME type remains an ordinary
|
|
visible document instead of disappearing from history, notifications, copy,
|
|
and shared media.
|
|
|
|
Keep protected-group discovery metadata-only so a new carrier can still prove
|
|
that an unindexed group should enter protected presentation. Persisted monotonic
|
|
peer markers and their fail-closed invalid-marker behavior continue to prevent
|
|
a previously protected group from downgrading to plaintext presentation.
|
|
|
|
### D095: Oversized media dimensions do not reject a protected file
|
|
|
|
Normalize private preview dimensions to the manifest's authenticated maximum
|
|
while preserving their aspect ratio. The encrypted JPEG thumbnail remains
|
|
bounded independently, and the original file bytes, size, hash, MIME type, and
|
|
encrypted chunk stream remain unchanged.
|
|
|
|
This prevents an otherwise valid high-resolution image or video from failing
|
|
the entire send merely because its optional presentation dimensions exceed the
|
|
manifest bound. Receivers continue to validate the normalized dimensions and
|
|
the embedded preview before exposing either to the native timeline.
|
|
|
|
### D096: Local chunk cleanup retries without user intervention
|
|
|
|
Treat a failed removal of an authenticated local chunk-cache record as
|
|
retryable while its durable file transfer still exists. Retry with the existing
|
|
bounded exponential delay before the next chunk, after final hash validation,
|
|
and while completing a cancellation. A cancellation remains the first branch
|
|
of the pump, so a delayed callback can clean it up but can never resume upload.
|
|
|
|
This covers antivirus, indexing, and legacy-lock migration races that outlive a
|
|
single bounded deletion attempt. It changes only local encrypted-cache cleanup;
|
|
it does not admit, pace, select, close, or otherwise own Telegram media or
|
|
upload sessions.
|
|
|
|
### D097: Queued content is the recovery source for missing local history
|
|
|
|
Persist each outgoing archived-content envelope and its live MLS descriptor as
|
|
one adjacent atomic outbox pair before updating the protected content index.
|
|
Before any upload, authenticate that exact pair and idempotently reconstruct a
|
|
missing message or manifest record. A crash between the two store commits then
|
|
resumes with the original event IDs, content key, ciphertext, and plaintext;
|
|
the client never regenerates a competing envelope under an existing object ID.
|
|
|
|
Once the descriptor has been sealed, its plaintext content key is no longer in
|
|
the outbox. Local history must already contain the corresponding authenticated
|
|
record at that boundary; otherwise recovery fails closed. File-transfer state
|
|
remains an additional manifest recovery journal and continues to resume the
|
|
same staged source and chunk cursor.
|
|
|
|
### D098: Staging cleanup follows durable transfer recovery
|
|
|
|
Never clean a conversation's staged source directories from the group runtime
|
|
constructor, because the protected file-transfer journal has not been loaded at
|
|
that point. After the journal is authenticated, preserve its exact active
|
|
source path and remove only unrelated files from both `staged-files` and
|
|
`staged-images`.
|
|
|
|
New groups with an authenticated empty journal may clean every orphan after the
|
|
empty state is established. Restored groups keep the full staged source across
|
|
restart, so manifest and chunk retry continues from the durable cursor instead
|
|
of failing after startup with a missing file.
|
|
|
|
### D099: File preparation may wait behind an existing outbox
|
|
|
|
Accept one new protected-file source while earlier protected text, edit, or
|
|
delete objects are still queued or finishing upload. Copy and hash that source
|
|
on the worker as usual, but allocate its manifest and durable transfer only
|
|
after the existing outbox and active upload are empty. The normal outbox pump
|
|
already enforces that boundary before `commitPreparedFileTransfer`.
|
|
|
|
This removes a composer race in which attaching a file immediately after
|
|
sending a message produced a generic queue failure even though no file transfer
|
|
was active. A second prepared or durable file remains rejected, control
|
|
reconstruction remains serialized, and the change does not alter MTProto
|
|
session ownership, connection selection, pacing, or retry behavior.
|
|
|
|
### D100: Deleting an uploading file cancels the matching transfer
|
|
|
|
When the native history delete action targets the event object of the one
|
|
durable outgoing file transfer, persist its authenticated deletion event before
|
|
requesting durable transfer cancellation. Suppress the deletion queue's normal
|
|
immediate pump until the cancellation marker is committed. The cancellation
|
|
cleanup removes only the manifest event/content pair, so the deletion remains
|
|
queued and is published as soon as the staged source and chunks are released.
|
|
|
|
If cancellation persistence fails after the deletion is already durable, keep
|
|
the deletion and report a local transfer failure; completing the original file
|
|
cannot make the deleted card reappear. Unrelated messages and files do not
|
|
cancel the transfer. This changes no MTProto session, admission, pacing, or
|
|
connection ownership.
|
|
|
|
### D101: Confirmed protected peers reserve exact carrier filenames
|
|
|
|
Keep protected-group discovery strict on both the exact carrier filename and
|
|
`application/octet-stream`. Once either side of a chat-to-megagroup migration
|
|
is already known to protected presentation, reserve the exact current and
|
|
legacy carrier filenames across both linked histories and hide those documents
|
|
regardless of server-normalized MIME metadata. Legacy underscore filenames may
|
|
contain the historical 24-byte identifier; current identifiers remain 32 bytes.
|
|
|
|
Ordinary groups still show documents that merely resemble carriers because
|
|
they have no monotonic protected-peer marker. Exact filename parsing remains
|
|
fail-closed, so partial names, uppercase identifiers, other extensions, and
|
|
vault-only filenames remain visible and cannot enter protected transport.
|
|
|
|
### D102: New clients catch up before publishing their KeyPackage
|
|
|
|
Build a new installation's client authorization against the latest verified
|
|
protected-group generation visible in the Telegram control history, not against
|
|
the immutable generation-one bootstrap. Before creating or uploading the
|
|
KeyPackage, stage the bounded public join objects, replay the signed transition
|
|
chain, and require it to reach the checkpoint authenticated by the account
|
|
vault. If the visible chain is ahead, persist that newer verified checkpoint in
|
|
the vault preflight.
|
|
|
|
The KeyPackage then commits to the caught-up generation. This is required after
|
|
the first client addition or any administrative transition: a package bound to
|
|
generation one is correctly rejected by current participants and would
|
|
otherwise leave every later installation waiting for admission forever.
|
|
|
|
### D103: Client admission retries the durable control inbox
|
|
|
|
Admit new clients from the authenticated `group-change-inbox.state`, not only
|
|
from the Telegram page that happened to finish the current observation. The
|
|
control synchronizer stages each observed KeyPackage before admission and
|
|
removes expired packages first. If freshness or another serialized operation
|
|
temporarily prevents administration, advancing the Telegram observation
|
|
boundary must therefore not strand the request forever.
|
|
|
|
Every later control synchronization reconstructs the bounded, locally
|
|
authenticated inbox and retries usable KeyPackages against the current group
|
|
generation and Telegram sender binding. Invalid or expired packages remain
|
|
rejected, inbox reconstruction failure is a local failure, and successful MLS
|
|
admission keeps the existing transactional publish path.
|