Secure file sharing on the atPlatform. Built with at_client_flutter.
Two sharing modes:
- Direct send — encrypt-once, post a notification per recipient atSign. Recipient's app downloads and decrypts automatically.
- 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.
- 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.
| 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 |
@<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.
public:_furl_<randomId>.furl@<sender>
key=_furl_<randomId>(dashless UUID; leading_hides it fromscan)- Metadata:
isPublic = true,ttl = <chosen lifetime in ms>,namespaceAware = true - Value = JSON
ShareRecord(see below) - Published with
PutRequestOptions()..useRemoteAtServer = trueso it goes straight to the cloud secondary.
Revoking a share is a plain atClient.delete(atKey).
filebin_url.atmospherepro@<me> // private user override (sharedBy=sharedWith=me)
public:filebin_url.atmospherepro@<orgAtSign> // optional public org override
Resolution order in FilebinClient.resolveBase():
- Private user override
- Public org override (if
publicOverrideAtSignis set at construction) - Default
https://filebin.net
The unlock page at furl.host is a fixed deployment. The sender side must match exactly or decryption silently fails.
- 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).keySaltis 8 bytes. - PIN: 9 characters from the alphabet
ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz23456789!@#$%^&*()_+-=[]{}|;:,.<>?(no 0/O/1/l/I).
<filebinBase>/furl<binId>/<fileId>.encrypted
binIdandfileIdare independent v4 UUIDs with dashes stripped.
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
}https://furl.host/furl.html?atSign=@<sender>&key=_furl_<randomId>
The sender forwards URL + PIN out-of-band (split channels recommended).
{
"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.
@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
@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
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
flutter pub get
flutter run -d macos # or -d ios / -d android
flutter test # runs the wire-format + crypto round-trip tests
dart analyze
- macOS:
macos/Runner/DebugProfile.entitlementsandRelease.entitlementsenablenetwork.client,network.server,files.user-selected.read-only,files.user-selected.read-write. - iOS:
Info.plistaddsNSLocalNetworkUsageDescription,NSDocumentsFolderUsageDescription,UIFileSharingEnabled,LSSupportsOpeningDocumentsInPlace. - Android:
AndroidManifest.xmladdsINTERNET,READ_EXTERNAL_STORAGE(≤SDK 32), andREAD_MEDIA_{IMAGES,VIDEO,AUDIO}(SDK 33+).
- Fresh keychain → first launch shows the Atsign Gate.
- Tap Get My Starter Pack → external browser opens https://my.atsign.com/starterpack.
- Sign in via any of the 4 flows. After success, app navigates to Home.
- Direct send an arbitrary file from
@alice→@bob. On the@bobdevice the file appears in Received within seconds.sha512summatches. - Link share with 1h TTL. Open URL in a fresh browser tab, enter PIN, file downloads and decrypts.
- Revoke from Share success screen → URL now returns "not found".
- Set a private filebin override under
filebin_url.atmospherepro@<me>→ confirm next share lands on the override host. - Type
bobby(missing@) in the recipient field → rejected with inline error (via.toAtsign()).