11 KiB
Server Library Encryption File Format v1
English | 简体中文
Recorded on 2026-10-18. Implementation snapshot: 6cbef4e.
This document describes the catalog.enc format at that commit. It supports maintenance, troubleshooting, and compatible reader implementations. This is a point-in-time format description, not a continuously maintained account of the latest implementation. The source code and ADR-0035 define current behavior.
1. File and Protection Scope
The default path is ~/Library/Application Support/ShadowsocksX-NG2/catalog.enc. The file is binary:
complete file = plaintext header || nonce || ciphertext || tag
additional authenticated data (AAD) = the original 45 header bytes
algorithm = AES-256-GCM
|| means byte concatenation. The header is authenticated but not encrypted. The file does not contain the master key.
The ciphertext contains the complete configuration catalog, subscription records, ordered favorites, and their referenced passwords, complete plugin options, and subscription URLs. This format does not cover the runtime configuration sslocal-active.json, derived listener authentication files, or files that users explicitly share or export.
2. Binary Layout
Offsets start at zero. All offsets and lengths are in bytes. Let P be the UTF-8 byte length of the complete payload before encryption. The AES-GCM ciphertext also has length P.
| Offset | Length | Field | Encoding and Meaning |
|---|---|---|---|
| 0 | 8 | magic | ASCII SSXNG2LB; hexadecimal 53 53 58 4E 47 32 4C 42 |
| 8 | 1 | envelopeVersion | The binary value 0x01, not the character 1 |
| 9 | 36 | keyID | A UUID encoded as UTF-8 text, including hyphens, without quotes, a terminating zero byte, or a newline |
| 45 | 12 | nonce | A random nonce generated by CryptoKit with its default configuration |
| 57 | P |
ciphertext | AES-GCM ciphertext of the complete payload |
57 + P |
16 | tag | GCM authentication tag |
Thus:
headerLength = 8 + 1 + 36 = 45
combined = nonce || ciphertext || tag
fileLength = P + 73
P = fileLength - 73
The file has no additional length field, padding, separator, compression layer, or outer Base64 encoding. An example keyID is 65A4F8AE-28F6-46A2-B61F-7CFA9A85D7F2. The writer generates this text with UUID.uuidString. Do not substitute the UUID's 16-byte binary representation.
Version 1 has no separate algorithm field. Version 0x01 specifies AES-256-GCM. The reader does not try a different algorithm after a failure.
3. Master Key and Authentication
SymmetricKey(size: .bits256) generates a random 32-byte master key. The key is neither a user password nor a value derived from keyID.
Production storage uses a Generic Password item in the classic keychain:
| Attribute | Value |
|---|---|
kSecClass |
kSecClassGenericPassword |
kSecAttrService |
com.qiuyuzhou.ShadowsocksX-NG2.server-library |
kSecAttrAccount |
The uuidString of the UUID parsed from the file header |
kSecValueData |
The raw 32-byte master key, not hexadecimal or Base64 text |
kSecAttrSynchronizable |
false |
kSecUseDataProtectionKeychain |
false |
When creating the item, SecAccessCreate receives a trusted application list containing only the current application. The header identifies the key; it does not grant keychain access. A copy of catalog.enc without the corresponding master key cannot decrypt the server library.
The writer uses CryptoKit's default nonce generation and generates a new nonce for each save. Normal saves retain the master key. A user-confirmed library reset generates a new key. The nonce and tag use the layout provided by AES.GCM.SealedBox.combined.
Encryption and decryption both use the original header bytes as AAD. A compatible reader must use the original 45 bytes from the file. Do not replace them with a header reconstructed after parsing the UUID.
4. Decrypted Payload
Decryption produces UTF-8 JSON with two required fields:
| Field | JSON Type | Meaning |
|---|---|---|
catalog |
string | The UTF-8 bytes of the catalog JSON, encoded as a Base64 string by Foundation Data Codable encoding |
secrets |
object | A mapping from credential reference strings to secret strings; an empty mapping is {} |
The decoding layers are:
catalog.enc
└─ AES-GCM decryption → payload JSON
├─ catalog → Base64 decoding → catalog JSON
└─ secrets → credential reference to secret value mapping
The outer JSON does not embed the catalog as an object. Nor is catalog a JSON-escaped catalog string: it requires Base64 decoding first. Base64 serializes the nested bytes; it does not provide confidentiality.
The following example represents an empty library. To shorten the example, the catalog JSON encoded in catalog omits the indentation used by the actual writer.
{
"catalog": "eyJlbnRyaWVzIjpbXSwiZmF2b3JpdGVJRHMiOltdLCJyb290Q2hpbGRyZW4iOltdLCJzdWJzY3JpcHRpb25zIjpbXSwidmVyc2lvbiI6Nn0=",
"secrets": {}
}
Decoding catalog produces:
{"entries":[],"favoriteIDs":[],"rootChildren":[],"subscriptions":[],"version":6}
The outer JSON has no separate payload version field and no fixed key order or whitespace convention. Do not depend on the exact text layout produced by a particular encoding operation.
4.1 Inner Catalog JSON
The current writer emits catalog schema v6. The reader accepts v1–v6. This schema version is independent of the header's envelopeVersion: 0x01 does not mean catalog schema v1.
Schema v6 has these top-level fields:
| Field | Meaning |
|---|---|
version |
Catalog schema version; the current writer emits 6 |
rootChildren |
An ordered array of root-level node identities |
entries |
All nodes, sorted by node id.rawValue when written |
subscriptions |
Subscription records, including URL credential references and refresh information |
favoriteIDs |
An ordered array of favorite node identities |
rootChildren and each configuration group's child array define hierarchy and display order. Do not substitute the order of the entries array. The inner writer enables prettyPrinted and sortedKeys. The corresponding Swift Codable models define the complete encoding of nodes, subscriptions, and enum fields. This document does not duplicate the full catalog schema.
4.2 Credential Reference Consistency
The keys in secrets must exactly match the set of credentials referenced by the catalog. The reader rejects both missing values and extra secret entries.
The reference set is the union of:
- Each server's
passwordRef. - Each server's non-nil
pluginOptionsRef. - Each subscription's
urlRef.
Values are the original secret strings. The writer includes only values referenced by the current catalog, not other credentials remaining in process memory. This rule does not include LAN listener authentication credentials.
5. Read Order and Errors
The current reader checks the file in this order:
- Check the file size.
- Validate magic.
- Validate envelopeVersion.
- Parse keyID.
- Find the corresponding master key and verify its length is 32 bytes.
- Authenticate and decrypt
combined, using the original header as AAD. - Decode the outer payload JSON.
- Base64-decode
catalog, then decode and validate the catalog schema. - Verify that the credential reference set exactly matches the keys in
secrets.
Data Base64 decoding occurs as part of the outer Codable decoding. Step 8 above describes the data dependency.
The current limit is P ≤ 64 × 1024 × 1024, or 67,108,864 bytes. This limit applies to the complete payload JSON, including the Base64 string and the encoded secrets mapping. It is not a limit on the inner catalog JSON alone.
The file size check accepts 73–67,108,937 bytes. The 73-byte minimum is only a structural check. An empty payload is not valid JSON, so a 73-byte file cannot represent a valid empty library.
| Condition | Current Error |
|---|---|
| Size out of range, invalid magic, invalid UUID, or invalid outer JSON or Base64 | ServerLibraryError.malformedFile |
envelopeVersion is not 0x01 |
ServerLibraryError.unsupportedVersion |
| Key missing or not 32 bytes long | ServerLibraryError.keyUnavailable |
| GCM authentication or SealedBox parsing fails | ServerLibraryError.authenticationFailed |
Catalog credential references do not match the keys in secrets |
ServerLibraryError.missingCredential |
| Inner catalog JSON, schema, or catalog structure validation fails | CatalogFileStore.PersistenceError.corrupt |
| Keychain denies access or requires interactive authorization | CredentialStoreError.keychainStatus, preserving the system status code |
File system read errors propagate as their original errors. If multiple conditions fail, the reader reports the first error encountered in its read order.
When catalog.enc exists, a read failure does not trigger fallback to catalog.json, plaintext decoding, or automatic empty-library creation. The application overwrites the library only after the user explicitly confirms discarding the existing configuration.
6. Writing and Migration
The write sequence is: collect referenced secrets → encode the catalog → encode the outer JSON → encrypt in memory → write an encrypted temporary file → atomically replace the target.
The directory permission is 0700. Temporary files are created with permission 0600. Temporary files reside in the target directory and use the name .catalog.enc.tmp-<UUID>. They contain the complete encrypted format too. The current writer does not explicitly call fsync; this document does not promise durability through power loss.
If catalog.enc does not exist, startup reads the old catalog.json and resolves its required legacy credentials. After all reads succeed, it generates a master key, commits the encrypted file, and reopens it for comparison. After successful verification, it attempts to delete the old file and migrated credential items. On first launch with neither library file present, it creates a new encrypted empty library.
7. Format Limitations
- The header, file size, and file system timestamps remain visible.
- Authentication detects tampering, but this format has no independent rollback protection state.
- After authorized access, the current application process retains the master key and decrypted data until exit.
- This is not a password-encrypted backup format that works independently of the local keychain.
8. Source Entry Points
- ServerLibraryStorage.swift: header, encryption, decryption, payload, size limits, and credential reference validation.
- CatalogFileStore.swift: catalog JSON encoding, schema versions, and structure validation.
- LibraryMasterKeyStore.swift: master key attributes and keychain access.
- CatalogModels.swift and SubscriptionRecord.swift: Codable models for the inner catalog.
- AtomicFileWriter.swift: temporary files and permission baseline.