Files
bitlogin/docs/s3c-format.md

635 lines
16 KiB
Markdown

# 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:
```text
s3c1:<base64url-encoded-canonical-json>
```
The prefix is exactly:
```text
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
```json
{
"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
```json
{
"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`
```json
"v": 1
```
Required integer. Must equal `1` for this version.
### 6.2 `endpoint`
```json
"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`
```json
"region": "us-east-1"
```
Required string passed to the S3 signing implementation.
Examples:
```text
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`
```json
"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`
```json
"access_key_id": "AKIAEXAMPLE"
```
Required non-empty access-key identifier.
### 6.6 `secret_access_key`
```json
"secret_access_key": "example-secret-key"
```
Required non-empty secret access key.
This is sensitive bearer credential material.
### 6.7 `session_token`
```json
"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`
```json
"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`
```json
"addressing_style": "path"
```
Optional. Allowed values:
```text
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:
```text
virtual: https://my-bucket.s3.example.com/object.jpg
path: https://s3.example.com/my-bucket/object.jpg
```
Default: `auto`.
### 6.10 `object_prefix`
```json
"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`
```json
"provider": "cloudflare-r2"
```
Optional user-interface hint.
Suggested values:
```text
aws-s3
cloudflare-r2
minio
backblaze-b2
wasabi
synology
generic
```
Unknown values are allowed as opaque lowercase identifiers matching:
```text
[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`
```json
"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`
```json
"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:
```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:
```text
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:
```json
{
"declared_permissions": [
"list",
"read",
"write",
"delete"
]
}
```
Suggested operation names:
```text
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:
```json
{
"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:
```json
{
"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:
```javascript
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:
```text
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:
```text
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.