Files
ShadowsocksX-NG/docs/server-library-encryption-format.md
T

11 KiB
Raw Blame History

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:

  1. Check the file size.
  2. Validate magic.
  3. Validate envelopeVersion.
  4. Parse keyID.
  5. Find the corresponding master key and verify its length is 32 bytes.
  6. Authenticate and decrypt combined, using the original header as AAD.
  7. Decode the outer payload JSON.
  8. Base64-decode catalog, then decode and validate the catalog schema.
  9. 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