Skip to content

sync: bring tinyland main current with origin#65

Merged
Jess Sullivan (Jesssullivan) merged 10 commits into
mainfrom
codex/sync-origin-main-20260607-darwin-bazel
Jun 8, 2026
Merged

sync: bring tinyland main current with origin#65
Jess Sullivan (Jesssullivan) merged 10 commits into
mainfrom
codex/sync-origin-main-20260607-darwin-bazel

Conversation

@Jesssullivan

@Jesssullivan Jess Sullivan (Jesssullivan) commented Jun 8, 2026

Copy link
Copy Markdown

Summary

  • Sync tinyland/main with the current Jesssullivan/tummycrypt origin/main line.
  • Preserves existing tinyland merge history with a normal merge commit.
  • Brings forward the recent TIN-1737 and TIN-1417 FileProvider/per-device prep commits needed before GloriousFlywheel can consume a current tummycrypt Darwin target.
  • Adds a documented cargo-deny ignore for RUSTSEC-2026-0173 (proc-macro-error2 unmaintained) after checking age 0.11.3; the transitive path remains age -> i18n-embed-fl -> proc-macro-error2, and the advisory reports no safe upgrade.

Validation

  • git diff --check tinyland/main..HEAD
  • Merge completed cleanly from origin/main into codex/sync-origin-main-20260607-darwin-bazel.
  • cargo-deny check
  • cargo tree -i proc-macro-error2
  • cargo info age@0.11.3

Notes

  • GitHub remotes only; no yoga remote used.
  • This is a repository sync PR plus CI advisory unblock, not the Bazel target implementation.

A hostile peer can publish a SymlinkManifest whose target points outside
the sync root or at a local secret store; every pulling host previously
materialized it verbatim (create_local_symlink ran std::os::unix::fs::symlink
on the attacker-supplied target with no validation). All three production
restore callers (auto-roam Pull, FileProvider, CLI pull) flow through
create_local_symlink, so the guard is centralized there.

Reject, before the link is created (skip + structured warn, not a hard error
so one hostile entry does not abort the pull):
  - empty target
  - absolute target (is_absolute, plus explicit leading-/ , leading-\\ and
    Windows Prefix screening)
  - `..` that escapes above the link's own directory (lexical resolve, no
    filesystem canonicalize); the link is always created inside the sync root,
    so this is at least as strict as "stays within the root"
  - resolved target hitting the fail-closed security deny-set
    (check_security_path_components)

Also self-defends upload_symlink_with_device (egress) by bailing on the same
conditions, since that public API can be called outside the collector which
already screens targets.

No behavior change when no symlink is restored/uploaded; default-off config
paths are untouched.
The FileProvider direct-read path built a master-only EncryptionContext
(EncryptionContext::new) and so could never read a per-device
(wrapped_file_keys-only) manifest, unlike tcfsd and the CLI which both
attach this device's age unwrap identity. This is the hard-gate
prerequisite to ever flipping crypto.per_device_wrapping; this PR does
NOT flip it (default stays false).

- New FP-local crate::device_ctx::build_encryption_context replicates
  tcfsd's build_encryption_context: loads the device registry + this
  device's age secret and attaches them via .with_device_wrapping. Gated
  on a per_device_wrapping config flag that defaults false, so the
  default config yields exactly EncryptionContext::new(mk) — byte
  identical for master-only manifests.
- grpc_backend fetch_direct_to_file now builds the device-aware context.
- direct.rs and uniffi_bridge.rs FAIL CLOSED on a per-device manifest
  (wrapped_file_keys present) instead of silently copying raw ciphertext;
  they only implement master-key unwrapping.
- FP crate gains an optional tcfs-secrets dependency (grpc feature only).

Dedupe follow-up: build_encryption_context now exists in tcfsd, the CLI,
and here. Lifting one copy into a shared crate (e.g. tcfs-sync) was
deferred because tcfs-sync does not currently depend on tcfs-secrets and
adding that edge to a core crate is broader than this security fix.
…FileProvider config

The FileProvider device-aware read path (PR Jesssullivan#492) reads `per_device_wrapping`
and `device_registry_path` from its JSON config, but no in-repo producer emitted
them. `tcfs config fileprovider` / `tcfs init --fileprovider-config-out` render
the FileProvider bootstrap JSON from the active config via
`build_fileprovider_init_config`; this adds the two keys so the read path
actually receives them.

- `FileProviderInitConfig` gains `per_device_wrapping` (mirrors
  `crypto.per_device_wrapping`) and `device_registry_path` (resolved from
  `sync.device_identity`, falling back to the shared default registry path).
- Default-off is byte-identical: `per_device_wrapping` is omitted from the
  rendered JSON via `skip_serializing_if` when false, and
  `device_registry_path` is only populated when wrapping is on. So the legacy
  master-only bootstrap output is unchanged.
- The extension derives the device secret (`device-<id>.age`) itself from the
  registry path + device id, matching PR Jesssullivan#492's `device_ctx`.
- Tests assert the keys are omitted when off (byte-identical) and present
  (with the resolved/default registry path) when on.

Does NOT flip `crypto.per_device_wrapping` (still default false). Out-of-repo
producers (lab nix, Swift HostApp keychain enrichment, App-Group sandbox secret
access) are specified in the PR body, not changed here.
…-restore-guard

DRAFT: TIN-1737 fail-closed guard for symlink restore ingress (agent-drafted, needs review)
…provider-device-ctx

DRAFT: TIN-1417 B1 FileProvider device-aware EncryptionContext (agent-drafted, needs review)
…provider-config

DRAFT: TIN-1417 B1 propagate per_device_wrapping + device_registry_path to FileProvider config (agent-drafted, needs review)
@Jesssullivan Jess Sullivan (Jesssullivan) merged commit e17d3e7 into main Jun 8, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant