Skip to content

LilianGRD/File_sharing_app

Repository files navigation

atmospherePro

Secure file sharing on the atPlatform. Built with at_client_flutter.

Two sharing modes:

  1. Direct send — encrypt-once, post a notification per recipient atSign. Recipient's app downloads and decrypts automatically.
  2. Share via link — encrypt, upload, publish a PIN-protected public record. Recipient opens a URL at https://furl.host, types the PIN, file is decrypted in-browser. Recipient does not need an atSign.

For SDK-level guidance, see ATPLATFORM_GUIDELINES.md. This file documents what is specific to this app.


Identity model

  • Sender: any atSign that has signed in via one of the four flows below.
  • Recipient (direct): any atSign — file lands in their Received tab.
  • Recipient (link): any browser user — no atSign required.

The app does not require any specific atSigns by default. @alice and @bob are used in examples only.

Namespaces

Namespace Owner Purpose
atmospherepro sender's atSign Direct-send notifications, plus the private filebin override AtKey
furl sender's atSign Public share records for the link-share mode. The unlock page at furl.host reads keys in this namespace

AtKey shapes

Direct send

@<recipient>:<fileId>.atmospherepro@<sender>
  • key = dashless UUID (the file id, reused as the filebin object id)
  • sharedBy = sender, sharedWith = recipient
  • Metadata: ttr = -1, ccd = true
  • Value = JSON DirectPayload (see below). The AtKey value is E2E-encrypted by the platform.

Delivered via notificationService.notify(NotificationParams.forUpdate(atKey, value: payload)). The recipient app subscribes to regex: '\.atmospherepro@' and downloads the ciphertext from the embedded URL.

Link share (public)

public:_furl_<randomId>.furl@<sender>
  • key = _furl_<randomId> (dashless UUID; leading _ hides it from scan)
  • Metadata: isPublic = true, ttl = <chosen lifetime in ms>, namespaceAware = true
  • Value = JSON ShareRecord (see below)
  • Published with PutRequestOptions()..useRemoteAtServer = true so it goes straight to the cloud secondary.

Revoking a share is a plain atClient.delete(atKey).

Filebin override

filebin_url.atmospherepro@<me>      // private user override (sharedBy=sharedWith=me)
public:filebin_url.atmospherepro@<orgAtSign>   // optional public org override

Resolution order in FilebinClient.resolveBase():

  1. Private user override
  2. Public org override (if publicOverrideAtSign is set at construction)
  3. Default https://filebin.net

Wire format — link share

The unlock page at furl.host is a fixed deployment. The sender side must match exactly or decryption silently fails.

Algorithms

  • File encryption: ChaCha20-RFC7539 (no Poly1305). 32-byte key, 12-byte nonce.
  • Integrity: SHA-512 hex over the plaintext.
  • Content-key wrap: AES-CTR. 16-byte IV. Key = pinKey = SHA-256(utf8(PIN) || keySalt). keySalt is 8 bytes.
  • PIN: 9 characters from the alphabet ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*()_+-=[]{}|;:,.<>? (no 0/O/1/l/I).

Ciphertext upload URL

<filebinBase>/furl<binId>/<fileId>.encrypted
  • binId and fileId are independent v4 UUIDs with dashes stripped.

Public share record JSON (consumed by furl.host)

Field names are literal. All binary fields are standard base64 (NOT urlsafe).

{
  "file_url":     "<filebin URL>",
  "chacha20_key": "<b64 wrapped content key>",
  "key_iv":       "<b64 16-byte AES-CTR IV>",
  "key_salt":     "<b64 8-byte salt>",
  "file_nonce":   "<b64 12-byte ChaCha20 nonce>",
  "file_name":    "report.pdf",
  "cipher":       "chacha20",
  "sha512_hash":  "<lowercase hex>",
  "message":      "(optional)",
  "file_size":    482915
}

Recipient URL

https://furl.host/furl.html?atSign=@<sender>&key=_furl_<randomId>

The sender forwards URL + PIN out-of-band (split channels recommended).

Wire format — direct send

{
  "file_url":     "<filebin URL>",
  "chacha20_key": "<b64 raw 32-byte content key>",
  "file_nonce":   "<b64 12-byte ChaCha20 nonce>",
  "file_name":    "report.pdf",
  "sha512_hash":  "<lowercase hex>",
  "file_size":    482915,
  "sent_at":      1715000000000,
  "message":      "(optional)"
}

The whole JSON is the AtKey value, encrypted end-to-end by the platform. No PIN, no key wrap.

Data flow — direct send

@alice picks file
   │  ChaCha20 encrypt(plain) → ct, sha512(plain) → hex
   ▼
filebin.net  ← PUT ciphertext
   │
   ▼
@alice notify(@bob:<fileId>.atmospherepro@alice, DirectPayload JSON)
   │  (atPlatform E2E encrypts the AtKey value)
   ▼
@bob subscribe('\.atmospherepro@')
   │
   ▼
@bob GET filebin URL → ChaCha20 decrypt(ct) → verify sha512 → save to local Received folder

Data flow — link share

@alice picks file
   │  ChaCha20(file) → ct
   │  PIN ← random 9 chars; pinKey = SHA-256(PIN || salt)
   │  AES-CTR(pinKey, iv) wrap(content key) → wrapped
   ▼
filebin.net ← PUT ct
   │
@alice put(public:_furl_<rid>.furl@alice = ShareRecord JSON,
           PutRequestOptions(useRemoteAtServer = true), ttl = chosen)
   │
   ▼ (out of band)
Charlie  ← URL on channel A, PIN on channel B
   │
   ▼
furl.host loads
   │ /api/fetch/@alice/_furl_<rid> → ShareRecord JSON
   │ /api/download?url=<filebin URL> → ciphertext (CORS proxy)
   │ WASM: SHA-256(PIN || salt) → pinKey; AES-CTR unwrap → content key
   │        ChaCha20 decrypt → plaintext → browser save dialog
   ▼
Charlie gets the file

Project layout

lib/
  main.dart                          - MaterialApp, launches AtsignGateScreen
  core/constants.dart                - namespaces, alphabets, URLs, sizes
  core/result.dart
  models/share_record.dart           - hand-written toJson with EXACT field names
  models/direct_payload.dart
  models/received_item.dart
  services/
    at_service.dart                  - AtClient singleton, preference builder
    keychain_gate.dart               - First-Run Atsign Gate check
    onboarding_service.dart          - 4 auth flows
    notification_listener.dart
    direct_send_service.dart
    direct_receive_service.dart
    link_share_service.dart          - wire-format compliant
    filebin_client.dart              - override resolution, PUT/GET
    crypto/
      chacha20.dart                  - cryptography pkg, MacAlgorithm.empty
      aes_ctr_wrap.dart              - pointycastle AES/CTR
      pin_generator.dart             - Random.secure() over PIN alphabet
      hashing.dart                   - sha256 / sha512
  screens/
    atsign_gate_screen.dart          - MANDATORY first-run blocker
    welcome_screen.dart              - 4 auth flow buttons
    home_screen.dart                 - tabs: Direct | Link | Received
    send_direct_screen.dart
    send_link_screen.dart
    share_success_screen.dart        - URL + PIN + revoke
    received_screen.dart
    settings_screen.dart             - filebin override
  widgets/
    file_picker_button.dart
    atsign_chip_input.dart           - validates each via .toAtsign()
    pin_display.dart

Running

flutter pub get
flutter run -d macos          # or -d ios / -d android
flutter test                  # runs the wire-format + crypto round-trip tests
dart analyze

Platform permissions

  • macOS: macos/Runner/DebugProfile.entitlements and Release.entitlements enable network.client, network.server, files.user-selected.read-only, files.user-selected.read-write.
  • iOS: Info.plist adds NSLocalNetworkUsageDescription, NSDocumentsFolderUsageDescription, UIFileSharingEnabled, LSSupportsOpeningDocumentsInPlace.
  • Android: AndroidManifest.xml adds INTERNET, READ_EXTERNAL_STORAGE (≤SDK 32), and READ_MEDIA_{IMAGES,VIDEO,AUDIO} (SDK 33+).

End-to-end verification checklist

  1. Fresh keychain → first launch shows the Atsign Gate.
  2. Tap Get My Starter Pack → external browser opens https://my.atsign.com/starterpack.
  3. Sign in via any of the 4 flows. After success, app navigates to Home.
  4. Direct send an arbitrary file from @alice@bob. On the @bob device the file appears in Received within seconds. sha512sum matches.
  5. Link share with 1h TTL. Open URL in a fresh browser tab, enter PIN, file downloads and decrypts.
  6. Revoke from Share success screen → URL now returns "not found".
  7. Set a private filebin override under filebin_url.atmospherepro@<me> → confirm next share lands on the override host.
  8. Type bobby (missing @) in the recipient field → rejected with inline error (via .toAtsign()).

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors