Files

16 KiB

S3C: Portable S3 Connection String

Status: Experimental BitLogin format Document version: 0.1 Date: July 22, 2026 Implementation status: Not implemented

1. Summary

S3C is a proposed portable string format for importing and exporting credentials for S3-compatible object storage.

It is designed to work with:

  • Amazon S3
  • Cloudflare R2
  • MinIO
  • Backblaze B2 S3-compatible API
  • Wasabi
  • Synology and other self-hosted S3-compatible services
  • Generic S3-compatible providers

An S3C string contains enough information for a compatible application to connect to one bucket or bucket prefix using an S3-compatible SDK.

The format is provider-neutral. Provider-specific behavior is represented only as hints and must not override the explicit endpoint, region, bucket, and credential fields.

2. Security warning

An S3C string is encoded, not encrypted.

Anyone who obtains the complete string can decode the credentials and perform every operation permitted by the provider-side policy.

Applications must treat an S3C string like a password or private key. It must never be placed in:

  • Public Nostr events.
  • URLs or query strings.
  • Browser history.
  • Logs or analytics.
  • Crash reports.
  • Screenshots by default.
  • Unencrypted local storage.
  • Source-control repositories.

BitLogin stores imported S3C credentials only inside encrypted Connection Vault records. See connection-vault.md.

3. String syntax

Version 1 uses this syntax:

s3c1:<base64url-encoded-canonical-json>

The prefix is exactly:

s3c1:

The payload is:

  1. A JSON object conforming to this document.
  2. Canonically serialized using RFC 8785 JSON Canonicalization Scheme.
  3. Encoded as UTF-8.
  4. Encoded using unpadded base64url.

Padding characters (=) are not permitted in the canonical exported form. Importers may accept padded base64url for compatibility, but must emit the unpadded canonical form.

4. Minimal payload

{
  "access_key_id": "AKIAEXAMPLE",
  "bucket": "my-bucket",
  "endpoint": "https://s3.example.com",
  "region": "us-east-1",
  "secret_access_key": "example-secret-key",
  "v": 1
}

Required fields:

  • v
  • endpoint
  • region
  • bucket
  • access_key_id
  • secret_access_key

5. Complete payload

{
  "access_key_id": "AKIAEXAMPLE",
  "addressing_style": "path",
  "bucket": "adam-bitdrive",
  "endpoint": "https://1234567890abcdef.r2.cloudflarestorage.com",
  "expires_at": null,
  "label": "BitDrive R2",
  "object_prefix": "bitdrive/",
  "provider": "cloudflare-r2",
  "public_base_url": "https://files.example.com",
  "region": "auto",
  "secret_access_key": "example-secret-key",
  "session_token": null,
  "v": 1
}

The object may contain only fields defined by this version unless an importer explicitly supports extension fields. Unknown fields must not be silently reinterpreted.

6. Field definitions

6.1 v

"v": 1

Required integer. Must equal 1 for this version.

6.2 endpoint

"endpoint": "https://s3.example.com"

Required absolute URL for the S3-compatible API endpoint.

Requirements:

  • Must use https:// in normal operation.
  • Must not contain a username or password.
  • Must not contain a fragment.
  • Should not contain a query string.
  • Must not include the bucket name unless the provider specifically requires a bucket-bound endpoint.
  • A trailing slash should be removed during normalization, except for the root slash inherent to the URL.

http:// may be accepted only behind an explicit local-development option for localhost, loopback addresses, or private-network deployments.

6.3 region

"region": "us-east-1"

Required string passed to the S3 signing implementation.

Examples:

us-east-1
eu-west-1
auto

The string is provider-defined. Importers must not assume that every provider uses AWS region names.

6.4 bucket

"bucket": "adam-bitdrive"

Required non-empty bucket name.

The importer should preserve case exactly, although many providers restrict bucket names to lowercase.

6.5 access_key_id

"access_key_id": "AKIAEXAMPLE"

Required non-empty access-key identifier.

6.6 secret_access_key

"secret_access_key": "example-secret-key"

Required non-empty secret access key.

This is sensitive bearer credential material.

6.7 session_token

"session_token": "temporary-session-token"

Optional string used with temporary S3 credentials.

When present, it must be exported together with the corresponding access-key ID and secret access key. Omitting it from a temporary credential usually makes the connection unusable.

6.8 expires_at

"expires_at": 1787356800

Optional Unix timestamp in seconds indicating credential expiration.

The storage provider remains authoritative. This field is advisory and may be stale.

6.9 addressing_style

"addressing_style": "path"

Optional. Allowed values:

auto
virtual
path

Meaning:

  • auto — the client or SDK chooses.
  • virtual — bucket appears in the host name.
  • path — bucket appears in the URL path.

Examples:

virtual: https://my-bucket.s3.example.com/object.jpg
path:    https://s3.example.com/my-bucket/object.jpg

Default: auto.

6.10 object_prefix

"object_prefix": "apps/satisfied/"

Optional key prefix applications should treat as the root of their storage namespace.

Normalization rules:

  • Must not begin with /.
  • Empty string means the bucket root.
  • Non-empty values should end in /.
  • . and .. path segments are prohibited.
  • Backslashes are prohibited.
  • The prefix must be interpreted as an object-key prefix, not as a filesystem path.

This field is not an access-control mechanism. Provider-side IAM or token policy must independently restrict the credential to the intended prefix.

6.11 provider

"provider": "cloudflare-r2"

Optional user-interface hint.

Suggested values:

aws-s3
cloudflare-r2
minio
backblaze-b2
wasabi
synology
generic

Unknown values are allowed as opaque lowercase identifiers matching:

[a-z0-9][a-z0-9-]{0,63}

Clients must use explicit connection fields as the source of truth rather than changing credentials or endpoints based solely on this hint.

6.12 public_base_url

"public_base_url": "https://files.example.com"

Optional absolute HTTPS URL used to construct public or CDN delivery URLs after an object has been uploaded.

This field does not imply that the bucket is public. Applications must not assume that an object is readable without testing or receiving an explicit provider response.

6.13 label

"label": "BitDrive R2"

Optional human-readable label. It has no effect on authentication or signing.

For privacy, applications may omit labels from exported S3C strings and keep them only as encrypted local or BitLogin metadata.

7. Canonical example

Canonical JSON:

{"access_key_id":"AKIAEXAMPLE","addressing_style":"path","bucket":"adam-bitdrive","endpoint":"https://1234567890abcdef.r2.cloudflarestorage.com","object_prefix":"bitdrive/","provider":"cloudflare-r2","region":"auto","secret_access_key":"example-secret-key","v":1}

Canonical S3C string:

s3c1:eyJhY2Nlc3Nfa2V5X2lkIjoiQUtJQUVYQU1QTEUiLCJhZGRyZXNzaW5nX3N0eWxlIjoicGF0aCIsImJ1Y2tldCI6ImFkYW0tYml0ZHJpdmUiLCJlbmRwb2ludCI6Imh0dHBzOi8vMTIzNDU2Nzg5MGFiY2RlZi5yMi5jbG91ZGZsYXJlc3RvcmFnZS5jb20iLCJvYmplY3RfcHJlZml4IjoiYml0ZHJpdmUvIiwicHJvdmlkZXIiOiJjbG91ZGZsYXJlLXIyIiwicmVnaW9uIjoiYXV0byIsInNlY3JldF9hY2Nlc3Nfa2V5IjoiZXhhbXBsZS1zZWNyZXQta2V5IiwidiI6MX0

All credentials in this example are intentionally non-functional placeholders.

8. Parsing algorithm

An importer should:

  1. Verify the exact s3c1: prefix.
  2. Reject unreasonable total string sizes before decoding.
  3. Base64url-decode the payload.
  4. Decode strict UTF-8.
  5. Parse one JSON object with duplicate-key rejection.
  6. Verify v equals 1.
  7. Reject missing required fields.
  8. Validate every known field according to this document.
  9. Reject unsupported critical or unknown fields under the implementation's compatibility policy.
  10. Normalize endpoint, prefix, and addressing-style values.
  11. Optionally verify the connection using a low-risk operation.
  12. Store the structured fields in encrypted storage rather than retaining the plaintext string unnecessarily.

Parsers must never include the full S3C string or secret fields in thrown error messages.

9. Connection verification

After user approval, a client may verify credentials using the least-privileged operation likely to succeed.

Possible checks:

  • HeadBucket
  • A prefix-limited list request with a very small result limit
  • A provider-specific account or capability check

Verification must not:

  • Upload a visible object without explicit consent.
  • Delete or overwrite an object.
  • Assume a failed list request proves that reads or writes are unavailable.
  • Claim exact permissions unless provider policy has been authoritatively inspected.

A write test, when explicitly requested, should use a random temporary object under the configured prefix and immediately delete it. The interface must warn that lifecycle rules, logging, replication, or versioning may retain traces.

10. Permission metadata

A BitLogin Connection Vault record may store descriptive permission metadata:

{
  "declared_permissions": [
    "list",
    "read",
    "write",
    "delete"
  ]
}

Suggested operation names:

list
read
write
delete
multipart
presign-read
presign-write
admin

These values are not part of the S3C v1 wire string unless a future version explicitly adds them. They are display metadata and do not override provider-side policy.

11. BitLogin internal credential profile

After import, BitLogin stores a structured object:

{
  "schema": "bitlogin.connection.s3.v1",
  "endpoint": "https://1234567890abcdef.r2.cloudflarestorage.com",
  "region": "auto",
  "bucket": "adam-bitdrive",
  "access_key_id": "AKIAEXAMPLE",
  "secret_access_key": "example-secret-key",
  "session_token": null,
  "expires_at": null,
  "addressing_style": "path",
  "object_prefix": "bitdrive/",
  "provider": "cloudflare-r2",
  "public_base_url": "https://files.example.com",
  "declared_permissions": [
    "list",
    "read",
    "write",
    "delete"
  ]
}

Complete Connection Vault record:

{
  "schema": "bitlogin.connection.v1",
  "connection_id": "Vx7LgdZCsVbT_2uvB0YoGA",
  "connection_type": "s3",
  "state": "active",
  "label": "BitDrive R2",
  "created_at": 1784750000,
  "updated_at": 1784750000,
  "credential": {
    "schema": "bitlogin.connection.s3.v1",
    "endpoint": "https://1234567890abcdef.r2.cloudflarestorage.com",
    "region": "auto",
    "bucket": "adam-bitdrive",
    "access_key_id": "AKIAEXAMPLE",
    "secret_access_key": "example-secret-key",
    "session_token": null,
    "expires_at": null,
    "addressing_style": "path",
    "object_prefix": "bitdrive/",
    "provider": "cloudflare-r2",
    "public_base_url": "https://files.example.com",
    "declared_permissions": ["list", "read", "write", "delete"]
  },
  "application_binding": {
    "origin": "https://drive.example",
    "app_pubkey": null
  },
  "notes": null
}

12. Application authorization

Applications should request a particular connection and operations:

const grant = await bitlogin.storage.requestAccess({
  connectionId: "Vx7LgdZCsVbT_2uvB0YoGA",
  prefix: "apps/satisfied/",
  operations: ["list", "read", "write"],
  durationSeconds: 3600,
  mode: "broker"
});

The user prompt should show:

  • Requesting application origin or identity.
  • Provider, bucket, and prefix.
  • Requested operations.
  • Requested duration.
  • Whether long-lived credentials will be revealed.
  • Whether the request can delete or overwrite data.

13. Preferred authorization hierarchy

From safest to least isolated:

  1. Presigned single-object URL for one upload or download.
  2. Temporary scoped credentials limited by bucket, prefix, methods, and expiration.
  3. Brokered operations performed by an isolated BitLogin client.
  4. Revealed long-lived S3C credential after explicit warning.

Not every S3-compatible provider supports every option. Applications must make capability differences explicit rather than silently falling back to a more powerful credential.

14. Provider-side restriction

Every production connection should use a dedicated credential restricted to:

  • One bucket where possible.
  • One application prefix where possible.
  • Only required operations.
  • A limited lifetime where supported.
  • No provider account administration.

Recommended pattern:

BitDrive   -> adam-bitdrive/bitdrive/*
BitVid     -> media/videos/*
BitRoad    -> commerce/products/*
Satisfied  -> satisfied/uploads/*

The object_prefix field is informative. Real restriction must be enforced by IAM, token policy, bucket policy, access points, or another provider-side mechanism.

15. Prohibited or discouraged credentials

A compatible interface should reject or strongly warn about:

  • AWS root-account credentials.
  • Provider-wide administrative keys.
  • Credentials covering unrelated buckets without a clear reason.
  • Credentials that can alter IAM or account billing.
  • Plain HTTP endpoints outside explicit local development.
  • One powerful shared credential reused by unrelated applications.
  • Long-lived credentials where temporary credentials are available.

16. Export behavior

Before exporting an S3C string, BitLogin must show:

This string contains storage credentials. Anyone who obtains it may read, upload, overwrite, or delete data allowed by the provider-side policy.

Export should require a fresh user action. A plaintext string should not remain visible after navigation, logout, or a short inactivity interval.

Normal BitLogin account backups should contain only the encrypted Connection Vault event, not the plaintext S3C string.

17. Revocation and deletion

Removing the BitLogin record does not invalidate the provider credential.

Safe flow:

  1. Revoke or deactivate the key at the S3 provider.
  2. Confirm that applications no longer need it.
  3. Tombstone the Connection Vault record.
  4. Publish a NIP-09 deletion request for the previous event.
  5. Clear local decrypted copies and persistent grants.

Provider-specific adapters may help open the correct dashboard or automate key rotation, but S3C itself defines no universal revocation endpoint.

18. Versioning

Future incompatible formats use a new textual prefix:

s3c2:

A v1 parser must never silently reinterpret a different prefix or payload version.

Compatible optional fields may be added only after defining how older parsers handle them. Fields that change signing, endpoint selection, credential scope, or security semantics require a new version unless an explicit extension mechanism is standardized.

19. Standalone specification path

S3C begins as an experimental BitLogin format. It may move into a standalone repository after:

  • At least one working BitLogin implementation exists.
  • At least one independent application imports or exports it.
  • Test vectors and malformed-input fixtures are published.
  • Security and interoperability feedback has been incorporated.

A future standalone repository should define only the portable format, validation, fixtures, and reference codecs. BitLogin's relay encryption and Connection Vault behavior remain BitLogin-specific.

20. Required test vectors

Before implementation, publish fixtures for:

  • Minimal valid AWS-style configuration.
  • Cloudflare R2 with region auto.
  • MinIO using path-style addressing.
  • Temporary credentials with session token and expiration.
  • Empty and non-empty object prefixes.
  • Unicode labels.
  • Invalid base64url.
  • Invalid UTF-8.
  • Duplicate JSON keys.
  • Missing required fields.
  • Insecure endpoints.
  • Endpoint credentials or fragments.
  • Invalid addressing styles.
  • Prefix traversal attempts.
  • Canonical decode and re-encode equality.

All published fixtures must use non-functional placeholder credentials.