A Swift implementation of Shopify's EJSON library for managing encrypted secrets in source control.
- 🔐 NaCl Box Encryption - Uses Curve25519, Salsa20, and Poly1305 via libsodium
- 🔄 Format Compatible - Fully compatible with Go EJSON implementation
- 📦 Recursive JSON Processing - Automatically encrypts/decrypts nested structures
- 🎯 Type Preservation - Maintains JSON types (strings, numbers, booleans, arrays, objects)
- 📁 File Operations - Easy file encryption/decryption with public key management
- ✨ Swift-Native - Clean, type-safe Swift API
Add to your Package.swift:
dependencies: [
.package(url: "https://github.com/diogot/swift-ejson.git", from: "1.0.0")
]Then add EJSONKit to your target dependencies:
.target(
name: "YourTarget",
dependencies: ["EJSONKit"]
)import EJSONKit
// Generate a new keypair
let keyPair = try EJSON.generateKeyPair()
print("Public Key: \(keyPair.publicKey)")
print("Private Key: \(keyPair.privateKey)")let plaintext = "my secret password"
// Encrypt
let encrypted = try EJSON.encrypt(plaintext, publicKey: keyPair.publicKey)
// Returns: "EJ[1:ephemeral_pk:nonce:ciphertext]"
// Decrypt
let decrypted = try EJSON.decrypt(encrypted, privateKey: keyPair.privateKey)
// Returns: "my secret password"let ejson = EJSON()
// Original JSON
let json: [String: Any] = [
"database_password": "super_secret",
"api_key": "my_api_key",
"nested": [
"secret": "nested_secret"
]
]
// Encrypt all string values
let encrypted = try ejson.encryptJSON(json, publicKey: keyPair.publicKey)
// Decrypt back
let decrypted = try ejson.decryptJSON(encrypted, privateKey: keyPair.privateKey)// Encrypt a JSON file
try ejson.encryptFile(at: "/path/to/secrets.json", publicKey: keyPair.publicKey)
// Decrypt a JSON file
let secrets = try ejson.decryptFile(at: "/path/to/secrets.json", privateKey: keyPair.privateKey)
// Extract public key from an encrypted file
let publicKey = try ejson.extractPublicKey(from: "/path/to/secrets.json"){
"_public_key": "63ccf05a9492e68e12eeb1c705888aebdcc0080af7e594fc402beb24cce9d14f",
"database_password": "EJ[1:yF4JKMR4RUJY0hcxKYKDOg==:Yw6rqhvtLx7Kdc1hGtxqPBnx9bxk8kAzTCGNZPwVU5c=:ZCaH/xShYQ==]",
"nested": {
"secret": "EJ[1:x7F9KMTR5RUJZ1ida9KDPh==:Zw7sqiwuMy8Ldc2iHuyqQCoy0cyl9lB0UDHOaQxWV6d=:ADcI/yTiZR==]"
}
}EJ[1:ephemeral_pk:nonce:ciphertext]
1- Version numberephemeral_pk- Base64-encoded ephemeral public key (32 bytes)nonce- Base64-encoded nonce (24 bytes)ciphertext- Base64-encoded encrypted data
Keys are 32-byte Curve25519 keys represented as 64-character hexadecimal strings.
public struct EJSON {
public init()
// Key Management
public func generateKeyPair() throws -> KeyPair
// Value Encryption/Decryption
public func encrypt(_ plaintext: String, publicKey: String) throws -> String
public func decrypt(_ ciphertext: String, privateKey: String) throws -> String
// JSON Processing
public func encryptJSON(_ json: [String: Any], publicKey: String) throws -> [String: Any]
public func decryptJSON(_ json: [String: Any], privateKey: String) throws -> [String: Any]
// File Operations
public func encryptFile(at path: String, publicKey: String) throws
public func decryptFile(at path: String, privateKey: String) throws -> [String: Any]
public func extractPublicKey(from path: String) throws -> String
// Static convenience methods
public static func generateKeyPair() throws -> KeyPair
public static func encrypt(_ plaintext: String, publicKey: String) throws -> String
public static func decrypt(_ ciphertext: String, privateKey: String) throws -> String
}public struct KeyPair {
public let publicKey: String // 64-char hex string
public let privateKey: String // 64-char hex string
}public enum EJSONError: Error {
case sodiumInitializationFailed
case invalidKeyFormat
case invalidHexString
case invalidBase64String
case encryptionFailed
case decryptionFailed
case invalidEncryptedFormat
case invalidJSONData
case missingPublicKey
case fileNotFound
case fileReadError
case fileWriteError
}- Generate ephemeral keypair - A new keypair is created for each encryption
- Create nonce - A random 24-byte nonce is generated
- Encrypt - NaCl Box encryption using:
- Recipient's public key
- Ephemeral private key
- Random nonce
- Format - Package into
EJ[1:ephemeral_pk:nonce:ciphertext]
- Parse - Extract ephemeral public key, nonce, and ciphertext from
EJ[1:...]format - Decrypt - NaCl Box decryption using:
- Ephemeral public key
- Recipient's private key
- Nonce from encrypted value
- Return - Original plaintext
The library recursively walks the JSON tree and:
- Encrypts all string values (except
_public_key) - Preserves all other types (numbers, booleans, null)
- Maintains structure (nested objects and arrays)
- Adds
_public_keyfield to root object
This library is fully compatible with the Go EJSON implementation:
- ✅ Files encrypted with Go EJSON can be decrypted with EJSONKit
- ✅ Files encrypted with EJSONKit can be decrypted with Go EJSON
- ✅ Identical encrypted value format
- ✅ Same key format (64-character hex strings)
- Private keys should never be committed to source control
- Public keys are safe to commit (they're in the encrypted files anyway)
- Use secure key storage (Keychain, environment variables, etc.)
- The
_public_keyfield is never encrypted (it's needed for decryption) - Each encryption uses a unique ephemeral keypair and nonce
import EJSONKit
// 1. Generate keys (do this once)
let keyPair = try EJSON.generateKeyPair()
print("Store this private key securely:", keyPair.privateKey)
// 2. Create secrets file
let secrets: [String: Any] = [
"database": [
"host": "db.example.com",
"username": "admin",
"password": "super_secret_password"
],
"api_keys": [
"stripe": "sk_live_...",
"twilio": "AC..."
]
]
let secretsData = try JSONSerialization.data(withJSONObject: secrets)
try secretsData.write(to: URL(fileURLWithPath: "secrets.json"))
// 3. Encrypt the file
let ejson = EJSON()
try ejson.encryptFile(at: "secrets.json", publicKey: keyPair.publicKey)
// 4. Commit the encrypted file (safe!)
// git add secrets.json
// git commit -m "Add encrypted secrets"
// 5. Later, decrypt when needed
let decrypted = try ejson.decryptFile(at: "secrets.json", privateKey: keyPair.privateKey)
let dbPassword = (decrypted["database"] as? [String: Any])?["password"] as? String// Store keys in environment variables
let publicKey = ProcessInfo.processInfo.environment["EJSON_PUBLIC_KEY"]!
let privateKey = ProcessInfo.processInfo.environment["EJSON_PRIVATE_KEY"]!
// Use them
let ejson = EJSON()
try ejson.encryptFile(at: "secrets.json", publicKey: publicKey)
let secrets = try ejson.decryptFile(at: "secrets.json", privateKey: privateKey)EJSONKit includes a command-line tool compatible with the Go EJSON CLI.
Download the latest release for your platform from GitHub Releases:
macOS (Universal Binary - Intel & Apple Silicon):
# Download and install the latest version
VERSION="1.0.0" # Replace with latest version
curl -L "https://github.com/diogot/swift-ejson/releases/download/v${VERSION}/ejson-${VERSION}-macos-universal.tar.gz" | tar xz
sudo mv ejson /usr/local/bin/
ejson helpVerify the checksum:
# Download checksum
curl -L "https://github.com/diogot/swift-ejson/releases/download/v${VERSION}/ejson-${VERSION}-macos-universal.tar.gz.sha256" -o ejson.sha256
# Verify
shasum -a 256 -c ejson.sha256If you prefer to build from source:
# Clone the repository
git clone https://github.com/diogot/swift-ejson.git
cd swift-ejson
# Build the CLI
swift build -c release
# Install to PATH
cp .build/release/ejson /usr/local/bin/Requirements:
- Swift 6.2+
- Linux only: libsodium-dev (
apt-get install libsodium-dev) - macOS: No additional dependencies (uses bundled libsodium)
ejson <command> [options]
Commands:
keygen Generate a new keypair
encrypt <file>... Encrypt one or more EJSON files
decrypt <file> Decrypt an EJSON file
Global Options:
-keydir <path> Path to keydir (default: /opt/ejson/keys or $EJSON_KEYDIR)
Keygen Options:
-w Write private key to keydir and print only public key
Generate a keypair:
# Print both keys to stdout
ejson keygen
# Write private key to keydir, print only public key
ejson keygen -wEncrypt a file:
# Create a secrets file
cat > secrets.json << EOF
{
"_public_key": "your_public_key_here",
"database_password": "secret123",
"api_key": "my_api_key"
}
EOF
# Encrypt it (modifies file in-place)
ejson encrypt secrets.jsonDecrypt a file:
# Decrypt and print to stdout (doesn't modify file)
ejson decrypt secrets.jsonCustom keydir:
# Using environment variable
export EJSON_KEYDIR=~/.ejson/keys
ejson decrypt secrets.json
# Using command line option
ejson -keydir ~/.ejson/keys decrypt secrets.jsonPrivate keys are stored in the keydir (default: /opt/ejson/keys or $EJSON_KEYDIR) with the filename matching the public key:
/opt/ejson/keys/
└── 63ccf05a9492e68e12eeb1c705888aebdcc0080af7e594fc402beb24cce9d14f
Keys are saved with 0600 permissions (readable only by owner).
The library includes comprehensive tests covering:
- Key generation and validation
- Single value encryption/decryption
- Unicode and special character handling
- Recursive JSON processing
- File operations
- Error handling
- Edge cases
- Performance benchmarks
Run tests with:
swift test- swift-sodium (v0.9.1+) - Provides NaCl cryptography primitives with bundled libsodium
- Swift 6.2+
- macOS 10.15+ / iOS 13+ / tvOS 13+ / watchOS 6+
- Linux: libsodium-dev package required
Contributions are welcome! Please ensure:
- All tests pass
- New features include tests
- Code follows Swift conventions
- Changes maintain compatibility with Go EJSON
MIT License - See LICENSE file for details
- Shopify EJSON - Original Go implementation
- EJSON Format Specification
- NaCl Cryptography
- libsodium
Inspired by Shopify's excellent EJSON library. This Swift implementation aims to bring the same security and ease-of-use to Swift projects.