A reviewable description of the implemented design.
RonikCloud's official client encrypts new file contents and user-visible workspace labels before storage. The design separates Personal standard encryption, Personal advanced secure mode, shared/group Business encryption, password shares, and file requests into distinct key flows. Authentication, authorization, quota enforcement, lifecycle state, and sharing policy remain service functions and therefore require service-visible metadata.
The implementation references included throughout this paper are the
evidence for those statements. The high-level module boundary is in
docs/ARCHITECTURE.md; the upload guard and cipher
selection are in
app_/lib/src/storage/wasabi_storage_service.dart and
app_/lib/src/security/encryption_service.dart.
RonikCloud's internal adversarial review records implemented controls
and remaining risks. It describes itself as defensive testing and
prioritization, not an audit. Some internal protocol identifiers still
use the historical ronikdrive name; those are compatibility
identifiers in the RonikCloud implementation, not separate products.
Implementation references: docs/ADVERSARIAL_SECURITY_REVIEW.md, app_/lib/src/security/encryption_service.dart, app_/lib/src/security/business_encryption_service.dart.
What this paper covers, trusts, and excludes.
This paper covers:
- encryption and key derivation in the Flutter client;
- cloud metadata and object-storage formats;
- Personal and Business authorization;
- password-protected public links and encrypted file requests;
- recovery kits, local protected state, logging, and aggregate growth analytics; and
- tests and continuous-integration checks that exercise those contracts.
The primary components are the Flutter app in app_/,
Firebase Functions in functions_/, Firestore rules in
firestore.rules, and Wasabi-compatible S3 object storage.
Optional cache/cold-storage services and the experimental Cloudflare
Worker are not included in this paper's confidentiality assurance.
The Worker remains a separately gated prototype.
The design trusts the user's endpoint while it is unlocked, the delivered application code, its cryptographic dependency, Firebase Authentication and service infrastructure, and the device credential store. The locked-data design does not protect against malware or a malicious browser extension on an unlocked endpoint, a compromised release/update channel, an authorized recipient who saves plaintext, or a user who discloses a password, link secret, recovery kit, or decrypted export.
Local standard keys are persisted through
FlutterSecureStorage. The login password is retained only
in app memory. Android requests encrypted shared preferences; iOS and
macOS request a device-bound keychain accessibility class.
Implementation references: README.md, docs/ADVERSARIAL_SECURITY_REVIEW.md, server_/cloudflare_workers/worker.js, app_/lib/src/authentication/login_password_vault.dart.
Threats the current controls are designed to reduce.
RonikCloud is designed to reduce the effect of:
- an object-storage or database observer attempting to read new file content, filenames, folder labels, comments, review notes, or file-request labels;
- an authenticated user attempting to read another Personal prefix or a Business scope they are not currently assigned;
- a person who has only a public share URL, or who has a file-request URL without its fragment key and attempts to read the protected request title or create an owner-decryptable submission;
- accidental disclosure through application diagnostics and support records;
- ciphertext modification or metadata-envelope transplantation; and
- repeated public or authenticated requests intended to amplify egress or service cost.
The corresponding controls are client-side authenticated encryption, domain-separated keyed identifiers, per-recipient Business envelopes, Firebase identity and Firestore rules, live Business membership checks, short-lived signed object requests, request budgets, and bounded rate-limit records.
The model does not conceal traffic timing, IP or network information, object sizes, request volume, account and membership relationships, or all lifecycle state from RonikCloud and its infrastructure providers. It is a confidentiality-and-integrity design for protected payloads, not an anonymity system, traffic-analysis defense, availability guarantee, or digital-rights-management system.
Implementation references: app_/lib/src/security/workspace_metadata_encryption_service.dart, app_/lib/src/security/business_encryption_service.dart, firestore.rules, functions_/storage_object_policy.js, functions_/share_access_policy.js, functions_/file_access_privacy.js.
Parameters are explicit and tied to versioned formats.
The client pins the Dart cryptography package at version
2.8.0. The following parameters are explicit in current
RonikCloud code.
| Purpose | Construction and parameters | Implementation evidence |
|---|---|---|
| Password and passphrase derivation | PBKDF2-HMAC-SHA-256, 300,000 iterations, 256-bit output. | encryption_service.dart |
| Standard account salt | 16 random bytes, normally generated by an authenticated Node callable with crypto.randomBytes; if that callable is unavailable, the official client can generate the same-length salt with Dart Random.secure() in an authenticated Firestore transaction. The salt is stored as Base64 in the user's profile. | functions_/index.js, encryption_setup_privacy.js, workspace_home_page.dart |
| File content and generic binary envelopes | AES-CTR with a 256-bit key and HMAC-SHA-256 authentication; encoded as algorithm nonce, ciphertext, then MAC. | encryption_service.dart, wasabi_storage_service.dart |
| File and standard folder labels | AES-256-GCM; enc-- plus unpadded Base64url of a 12-byte nonce, ciphertext, and 16-byte tag. | encryption_service.dart, workspace_metadata_encryption_service.dart |
| Queryable protected metadata | HMAC-SHA-256 with separate domain strings for backup paths, local paths, content-integrity tags, folder IDs, and secure shard names. | workspace_metadata_encryption_service.dart, encryption_service.dart |
| Business recipient envelopes | X25519 with an ephemeral sender key; HKDF-HMAC-SHA-256 with 32-byte output, fixed protocol salt, and context containing object key, recipient UID, and key ID. The wrapping key protects the 32-byte file key with AES-CTR/HMAC-SHA-256. | business_encryption_service.dart |
| Business company and group labels | Random 32-byte metadata key, AES-256-GCM, and one X25519/HKDF envelope per recipient. | business_metadata_encryption.dart, business_encryption_service.dart |
| Collaboration text | File-scoped HKDF-HMAC-SHA-256, 32-byte output, then AES-256-GCM with additional data binding file, record, kind, and actor. | file_collaboration_encryption.dart |
| Recovery kit | PBKDF2-HMAC-SHA-256 at 300,000 iterations with a 16-byte random salt; AES-256-GCM with a 12-byte nonce, 16-byte tag, and format/version additional data. | encryption_recovery_kit.dart |
| App-layer TOTP | Base32 secret, HMAC-SHA-1 HOTP calculation, six digits, 30-second period, verification window of one period on either side; successful verification creates a 12-hour app-layer session. | functions_/config.js, functions_/index.js |
Algorithm identifiers are versioned in stored records so readers can reject unsupported formats. The implementation does not advertise algorithm agility as a security property; changing a format requires an explicit migration and compatibility plan.
Implementation references: app_/pubspec.lock, firestore.rules, functions_/business_encryption_identity.js, functions_/file_request_privacy.js.
Different workflows use deliberately separate key paths.
Personal standard encryption
After email/password sign-in, the client normally obtains the
account's random salt through an authenticated empty-payload
callable. If that callable is unavailable, the official client can
generate the same 16-byte salt with Dart Random.secure()
and create the profile value in an authenticated Firestore
transaction. Neither path sends the password. The client locally
derives the 256-bit standard key from the sign-in password. The
callable receives no client identity fields beyond the authenticated
Firebase context. The derived key, not the password, may be persisted
in the device credential store so a restored Firebase session can
open standard content.
For new Personal standard uploads, the official client encrypts the
filename with AES-256-GCM and the bytes with
AES-CTR/HMAC-SHA-256 before uploading as
application/octet-stream. Upload is blocked in the
storage service unless an encryption key is supplied or the caller
explicitly marks a payload as already encrypted.
New object keys use an owner scope followed by an opaque
/objects/<shard>/<shard>/<token>
shape. That routing token is not an authorization credential. The
metadata callable receives the object key, encrypted label, size,
encryption mode, storage layout, folder ID, keyed backup and
integrity tags when applicable, and lifecycle and scope fields; it
rejects readable folder names, colors, and path segments.
Folder labels use the same authenticated label encryption. Stable folder and backup identities use keyed HMAC tags so the service can query or reconcile them without receiving the source label or raw content digest. Parent relationships and opaque folder IDs remain visible because navigation and access control require them.
Implementation references: login_page.dart, encryption_setup_service.dart, encryption_setup_privacy.js, workspace_home_page.dart, login_password_vault.dart, wasabi_storage_service.dart, background_backup_runner.dart, upload_metadata_service.dart, upload_contracts.js.
Personal advanced secure mode
Advanced secure mode uses a separate user-entered passphrase and a separate 16-byte salt, with the same PBKDF2 parameters. The passphrase and derived key are held in the active client session; the profile stores the salt, not a recoverable advanced-mode password. The mode is not available for shared/group Business uploads.
The client encrypts the entire file once with AES-CTR/HMAC-SHA-256, then splits that ciphertext into 4 MiB storage shards. Each shard's provider key includes an HMAC-SHA-256 token derived from the secure key, base object key, and shard index. Sharding is an opaque storage layout; it is not erasure coding, independent per-shard encryption, secret sharing, or a substitute for the cryptographic layer.
Implementation references: workspace_home_page.dart, user_documentation_page.dart, secure_shard_assembler.dart.
Business encryption
Company-private storage follows the account's Personal standard-key path. Company-shared and group storage instead use a fresh random 32-byte key per file. Content and filename are encrypted with that file key, and upload proceeds only when the client has a complete recipient set for currently authorized, encryption-ready members. Advanced secure mode is deliberately disallowed in shared Business spaces because per-member file keys are the sharing mechanism there.
Each member has an X25519 identity. The public key and key ID are service-visible; the 32-byte private seed is wrapped under that member's standard account key using AES-CTR/HMAC-SHA-256. A file key is wrapped separately for each recipient using a fresh ephemeral X25519 key and context-bound HKDF output. Company and group display metadata has its own random AES-GCM key and recipient envelopes.
Business authorization and cryptographic access are related but not identical. Firestore and Functions can remove a member's live access immediately, while removing knowledge already delivered to a device requires client-side content and key rotation. Rotation creates a fresh encrypted object and complete envelope set; reconciliation counters let another authorized device discover unfinished work without storing a readable task description. Revocation cannot erase plaintext or keys already exported or retained by a former authorized recipient.
Implementation references: business_encryption_registry.dart, business_encryption_service.dart, business_metadata_encryption.dart, business_encryption_identity.js, business_key_rotation.js, business_reconciliation_state.js.
Comments and reviews
New comment author names, comment bodies, mention addresses, reviewer names, and review notes are encrypted on the client with a file-scoped derived key. Additional authenticated data prevents a valid envelope from being moved to a different file, record, actor, or record type. The service still sees file and actor IDs, review status, record timestamps, and ciphertext so it can authorize reads, enforce authorship, and route minimal mention notifications.
Implementation references: file_collaboration_encryption.dart, file_collaboration_privacy.js.
Encrypted file requests
The owner creates a random 32-byte request key. One copy is carried in the URL fragment for the uploader, and another is encrypted under the owner's standard key and stored in an owner-readable secret record. The request title, submitted filename, MIME type, and submitted file bytes are encrypted with the request key. The browser reads the fragment locally; its metadata request body is empty, so the fragment key is not transmitted to the API.
The service generates a separate 24-byte random request token and stores owner identity, encrypted title and owner key envelope, coarse expiry, file-size and upload limits, counts, status, ciphertext object identity, and completion state. The public response excludes UID, object keys, internal timestamps, and plaintext labels. The complete URL, including its fragment key, is needed to read the protected title and create files the owner can decrypt. The server does not verify possession of that fragment key: a token-only holder can still reach the bounded authorization and upload endpoints, submit opaque data that the owner cannot use, and consume bounded upload slots. Fragment secrecy protects confidentiality and usable submission creation; it is not server-verified authorization or an identity check.
Implementation references: workspace_home_page.dart, file_request_upload_page.dart, file_request_metadata_envelope.dart, functions_/index.js, file_request_privacy.js, firestore.rules.
Recovery kits and local plaintext
A recovery kit contains the standard key, account UID, environment ID, account encryption salt, and creation time inside AES-256-GCM authenticated ciphertext. Only format and version, KDF salt and parameters, nonce, ciphertext, and tag are outside it. Creation and restore run locally, require a recovery passphrase of at least 16 characters with at least six distinct code points, and validate account, environment, and salt before accepting the key. Account Security also requires password reauthentication and a current TOTP code when app-layer 2FA is enabled, and rejects use of the account password as the recovery passphrase.
The kit does not recover the advanced secure-mode passphrase. RonikCloud does not upload the kit in this flow, but the operating system share or export destination is controlled by the user. Existing copies cannot be revoked independently; a person with the kit, its passphrase, and access to the matching account can restore the standard key.
Opening or sharing through another native application can require a
temporary plaintext handoff file. RonikCloud creates it under an
opaque lease directory, applies mode 0700/0600
on Linux and macOS, defaults to a 30-minute lease, retries stale
cleanup, and exposes a manual clear action. Windows permissions rely
on the platform temporary-directory and account controls; a receiving
app may keep its own copy or recent-file record.
Device-local workspace state is protected separately with random AES-256-GCM keys in the platform credential store and account and namespace additional data. Native offline file bodies use chunked AES-256-GCM with per-object HMAC-SHA-256-derived keys bound to account/environment, object key, and optionally the workspace file key. These controls depend on the security of the operating system, credential store, and unlocked session.
Implementation references: encryption_recovery_kit.dart, security_settings_page.dart, temporary_plaintext_lease_native.dart, temporary_plaintext_files_control.dart, local_private_cache.dart, local_offline_vault_native.dart.
Protected payloads do not eliminate operational metadata.
| Area | Service or provider-visible data | Protected from normal service processing in current-format flows |
|---|---|---|
| Account | Firebase UID and authentication claims, account email at authentication and business-directory boundaries, subscription and security state, and per-account encryption salts. | The sign-in password is not included in the encryption-setup request; derived standard and secure keys are not uploaded by that flow. |
| Files and folders | Object key and prefix, ciphertext size, owner/company/group/access IDs, parent and opaque folder IDs, encryption/layout/version markers, quota and lifecycle state, transfer timing, and network logs. | New file content, filename, folder label, local path, and raw local digest. |
| Business | Company/member/group relationships, roles, permissions, public X25519 keys and key IDs, wrapped private identity, per-recipient envelopes, and rotation generation. | File and metadata keys, X25519 private seed in plaintext, and new content and labels. |
| Password shares | URL token, creator/source references, KDF salt and parameters, encrypted metadata, proof verifier, expiry, request budget/count, revocation and one-bit alert state. Storage sees derivative object key, size, timing, and requests. | Password, share key, current-format filename and content type, and plaintext shared bytes. |
| File requests | Request token, owner, encrypted title and owner key envelope, expiry, size/upload limits and counts, upload status/object identity, and traffic metadata. | URL-fragment request key, current-format title, submitted filename and MIME type, and plaintext bytes. |
| Collaboration | File, record, and actor IDs, review status, timestamps, encrypted payload, and minimal routing IDs. | Current-format names, comment and review text, and mention addresses. |
| Recovery kit | No recovery-kit record is created by the RonikCloud service flow. | Account and environment binding and the standard key remain inside the user-exported encrypted kit. |
Infrastructure providers can still retain ordinary access, request, error, billing, and abuse-prevention logs subject to their configuration and policies. Encryption does not hide object length or traffic patterns.
Minimization controls reduce exposure without claiming anonymity.
Backend logging helpers replace UIDs and object keys with short deterministic support and object references and reduce provider errors to fixed categories plus an optional HTTP status. Client debug output is disabled outside debug builds and passes through a redactor for credentials, tokens, emails, URLs, object references, encrypted labels, file names, and paths. These are defense-in-depth filters, not proof that every external platform log is free of sensitive metadata.
Manual support reports are intentionally readable to support, but their schema omits direct UID and email fields, substitutes an opaque support code, applies client and server redaction, restricts platform and build values, and sets bounded retention. Users should still avoid entering secrets or sensitive content in report text.
Growth analytics accepts a closed vocabulary of events, sources, and enumerated dimensions and writes daily aggregates. Product activation uses a server-only per-account deduplication marker, while the aggregate record contains counts rather than a user event stream. This is data minimization, not anonymity from the service while the deduplication transaction is running.
Implementation references: privacy_logging.js, privacy_debug_logging.dart, privacy_redaction.dart, support_report_privacy.js, support_report_privacy.dart, growth_analytics.js, firestore.rules.
Security properties depend on passwords, clients, configuration, and use.
- Password strength remains critical. Anyone who obtains a standard or share ciphertext and its visible salt can test password guesses offline. 300,000-round PBKDF2 raises cost but does not rescue a weak password. A Firebase password change does not itself re-encrypt existing standard data; a retained device key or matching recovery kit is needed when the prior derived key is unavailable.
- Advanced secure mode is deliberately unrecoverable by the service. Loss of its passphrase can make those files permanently unreadable. The standard recovery kit does not contain it.
- Authorization revocation cannot retract knowledge. Removing a Business member or revoking a share blocks future service-mediated access, but cannot delete plaintext, ciphertext, passwords, or keys already copied to another device. Business cryptographic removal depends on successful rotation and reconciliation.
- Legacy compatibility exists. Older readable workspace and collaboration metadata and proofless share links have migration or compatibility paths. New schemas reject reintroduction in many places, but users should recreate legacy links and complete migrations before assuming current-format properties apply to every historical record.
- The server cannot prove bytes are semantically encrypted. The official client enforces encryption before upload and the backend validates formats, scope, provider existence, and declared encryption metadata. A modified authorized client can still place arbitrary bytes in its permitted storage prefix. Metadata flags are not remote cryptographic attestation.
- Metadata and availability remain service responsibilities. The service can observe the metadata listed above, deny access, delete or fail to return ciphertext, and deploy changed client code. This design does not provide anonymous access, censorship resistance, immutable storage, guaranteed deletion, or defense against a compromised trusted endpoint or release channel.
- Cryptographic claims are implementation-specific. They apply to the versioned formats and official-client paths described here, not to files exported in plaintext, third-party copies, manual support text, experimental services, or a differently configured deployment.
Tests exercise contracts; they are not deployment attestations.
The repository contains focused Flutter tests for encryption
primitives, recovery kits, Business identities and metadata,
share and file-request envelopes, collaboration binding, secure-shard
assembly, local cache and vault behavior, upload metadata, and
server-managed storage. Representative suites include
encryption_service_test.dart,
encryption_recovery_kit_test.dart,
business_encryption_service_test.dart,
share_metadata_envelope_test.dart,
file_request_metadata_envelope_test.dart,
file_collaboration_encryption_test.dart, and
local_offline_vault_test.dart.
Functions tests exercise authorization and policy modules, privacy
allowlists, Business key rotation, storage object policy, upload
contracts, and Firestore rules. Representative suites include
share_access_policy_unit_test.js,
file_request_privacy_unit_test.js,
business_key_rotation_unit_test.js,
upload_contracts_unit_test.js, and
firestore_rules_smoke_test.js.
Continuous integration is configured to analyze and test Flutter, lint and unit-test Functions, run Firestore emulator tests, audit Node and Python dependencies, run Bandit, build network-isolated non-root containers, and scan Git history for secrets. CI configuration is evidence of intended verification, not evidence that a particular deployment passed a particular run unless the corresponding CI result is independently checked.
Repository-path references make this paper reviewable against source, but this document does not assert that the repository is publicly licensed or that source availability itself constitutes independent assurance. Security reports can be sent to support@ronikcloud.com.
Implementation reference: .github/workflows/security_checks.yml. Security contact: security.txt.
Security whitepaper FAQ
Is this whitepaper an independent security audit?
No. It is an implementation-derived technical overview, not a third-party audit, certification, penetration-test report, formal proof, deployment attestation, or guarantee that every running environment uses this exact revision and configuration.
What does RonikCloud encrypt before storage?
The current official client encrypts new protected file content and user-visible workspace labels before storage. Current-format collaboration text, password-share metadata, and encrypted file-request labels and payloads also have the client-side protections described above.
Does RonikCloud hide all metadata?
No. The service and infrastructure providers can observe operational metadata such as object size, timing, opaque identifiers, access relationships, routing, lifecycle and subscription state, and ordinary network information.
Can RonikCloud recover an advanced secure-mode passphrase?
No. Advanced secure mode uses a separate passphrase that the service cannot recreate. The standard recovery kit does not contain that passphrase, so losing it can make advanced-mode files permanently unreadable.