Secure Key Generator for iOS Projects

SecureKeys version iOS version 13.0 Ruby version 3.3.6

secure-keys is a Ruby CLI that generates a SecureKeys.xcframework for iOS apps. It reads secret values from macOS Keychain on local machines or environment variables in CI, encrypts those values with AES-256-GCM, writes a Swift API, builds an XCFramework, and optionally adds that framework to an Xcode target.

Requirements

  • macOS 11.0 or later
  • Ruby 3.3 or later
  • Xcode command line tools
  • iOS 13.0 or later

Installation

Install with Homebrew:

brew tap derian-cordoba/secure-keys
brew install derian-cordoba/secure-keys/secure-keys

Install with RubyGems:

gem install secure-keys

Install with Bundler:

gem 'secure-keys'
bundle install

Quick Start

From an iOS project root:

security add-generic-password -a "secure-keys" -s "secure-keys" -w "apiKey,githubToken"
security add-generic-password -a "secure-keys" -s "apiKey" -w "your-api-key"
security add-generic-password -a "secure-keys" -s "githubToken" -w "your-github-token"

secure-keys

The command creates .secure-keys/SecureKeys.xcframework.

Use the generated framework from Swift:

import SecureKeys

let apiKey = SecureKey.apiKey.decryptedValue
let githubToken = key(for: .githubToken)

How Secret Generation Works

secure-keys does not create third-party API keys for providers such as GitHub, Firebase, Stripe, or AWS. Those secrets must be created in their owning service first.

The tool generates an iOS framework that contains encrypted copies of the secret values you provide:

  1. Resolve the secret source:
    • Local runs use macOS Keychain unless CI mode is enabled.
    • CI runs use environment variables.
  2. Read the configured list of secret names.
  3. Read each secret value by name.
  4. Generate a random AES-256-GCM key for this build.
  5. Encrypt each secret value.
  6. Write a Swift SecureKey enum with encrypted byte arrays.
  7. Build .secure-keys/SecureKeys.xcframework.
  8. Remove temporary Swift package files.

Local Configuration With Keychain

The default Keychain service and account identifier is secure-keys.

Store the list of secret names:

security add-generic-password -a "secure-keys" -s "secure-keys" -w "githubToken,apiKey"

Store each secret value under the same Keychain service:

security add-generic-password -a "secure-keys" -s "githubToken" -w "your-github-token"
security add-generic-password -a "secure-keys" -s "apiKey" -w "your-api-key"

Use a custom Keychain identifier:

export SECURE_KEYS_IDENTIFIER="my-app-secrets"

security add-generic-password -a "$SECURE_KEYS_IDENTIFIER" -s "$SECURE_KEYS_IDENTIFIER" -w "githubToken,apiKey"
security add-generic-password -a "$SECURE_KEYS_IDENTIFIER" -s "githubToken" -w "your-github-token"
security add-generic-password -a "$SECURE_KEYS_IDENTIFIER" -s "apiKey" -w "your-api-key"

Use a custom delimiter:

export SECURE_KEYS_DELIMITER="|"
security add-generic-password -a "secure-keys" -s "secure-keys" -w "githubToken|apiKey"

CI Configuration With Environment Variables

CI mode is enabled automatically when common CI variables are present, including CI=true and GITHUB_ACTIONS=true. You can also force it:

secure-keys --ci

Set the list of secret names in SECURE_KEYS_IDENTIFIER:

export SECURE_KEYS_IDENTIFIER="github-token,api_key,firebaseToken"

Set each secret value as an environment variable. Secret names are looked up exactly first, then normalized by converting - to _ and uppercasing.

export GITHUB_TOKEN="your-github-token"
export API_KEY="your-api-key"
export FIREBASETOKEN="your-firebase-token"

The SECURE_KEYS_ prefix is also supported for secret values:

export SECURE_KEYS_API_KEY="your-api-key"

You can store the list in a dedicated environment variable instead:

export CUSTOM_SECRET_LIST="github-token,api_key"
secure-keys --ci --identifier CUSTOM_SECRET_LIST

Use a custom delimiter in CI:

export SECURE_KEYS_DELIMITER="|"
export SECURE_KEYS_IDENTIFIER="github-token|api_key|firebaseToken"

CLI Reference

secure-keys --help
Usage: secure-keys [--options]

    -h, --help                       Use the provided commands to select the params
        --ci                         Enable CI mode (default: false)
    -d, --delimiter DELIMITER        The delimiter to use for the key access (default: ",")
        --[no-]generate              Generate the SecureKeys.xcframework
    -i, --identifier IDENTIFIER      The identifier to use for the key access (default: "secure-keys")
        --verbose                    Enable verbose mode (default: false)
    -v, --version                    Show the secure-keys version
        --xcframework                Add the xcframework to the target

Examples:

secure-keys
secure-keys --verbose
secure-keys --ci --identifier CUSTOM_SECRET_LIST
secure-keys --identifier "my-app-secrets" --delimiter "|"
secure-keys -i "my-app-secrets" -d "|"

Xcode Integration

Generate the framework only:

secure-keys

Generate and add the framework to an Xcode target:

secure-keys --xcframework --target "YourTargetName" --add

Replace an existing framework reference:

secure-keys --xcframework --target "YourTargetName" --replace

Add an already generated framework without rebuilding:

secure-keys --no-generate --xcframework --target "YourTargetName"

Select a project explicitly:

secure-keys --xcframework --target "YourTargetName" --xcodeproj "/path/to/YourProject.xcodeproj"

The same options can be configured with environment variables:

export SECURE_KEYS_XCFRAMEWORK_TARGET="YourTargetName"
export SECURE_KEYS_XCFRAMEWORK_ADD=true
export SECURE_KEYS_XCFRAMEWORK_REPLACE=false
export SECURE_KEYS_XCFRAMEWORK_XCODEPROJ="/path/to/YourProject.xcodeproj"

secure-keys --xcframework

Short environment variable names are also supported:

export XCFRAMEWORK_TARGET="YourTargetName"
export XCFRAMEWORK_ADD=true
export XCFRAMEWORK_REPLACE=false
export XCFRAMEWORK_XCODEPROJ="/path/to/YourProject.xcodeproj"

Manual Xcode Setup

If you do not use --xcframework, add the framework manually:

  1. Open the Xcode project target.
  2. Open General.
  3. Add .secure-keys/SecureKeys.xcframework to Frameworks, Libraries, and Embedded Content.
  4. Open Build Settings.
  5. Add $(SRCROOT)/.secure-keys to Framework Search Paths.

Swift API

The generated framework exposes SecureKey, key(for:), key(_:), and a String.secretKey helper.

import SecureKeys

let apiKey = SecureKey.apiKey.decryptedValue
let githubToken = key(for: .githubToken)
let sameGithubToken = key(.githubToken)
let keyFromString: SecureKey = "apiKey".secretKey
let valueFromString = "apiKey".secretKey.decryptedValue
let staticValue = String.key(for: .apiKey)

Generated key names are camelized for Swift enum cases:

api-key      -> SecureKey.apiKey
githubToken  -> SecureKey.githubToken

Output Files

The main output is:

.secure-keys/SecureKeys.xcframework

Temporary Swift package and build files are created under .secure-keys during generation and removed after the framework is built.

Security Notes

  • Do not commit .secure-keys/SecureKeys.xcframework unless your release process intentionally requires it.
  • Do not commit .env files or raw secret values.
  • Treat app-bundled secrets as obfuscation, not as a perfect security boundary. A determined attacker can inspect a shipped app binary.
  • Prefer server-side secret usage for highly sensitive credentials.
  • Use environment-specific keys for development, staging, and production.
  • Rotate keys regularly and revoke leaked credentials immediately.

Secret Scanning and Validation

secure-keys ships both a CLI and a Ruby API for validating individual secret values and scanning source files or git diffs for accidentally exposed credentials.

CLI

Scan the current directory:

secure-keys validate scan

Scan a specific path:

secure-keys validate scan ./src

Scan only staged git changes (useful as a pre-commit hook):

secure-keys validate scan --staged

Save the report as JSON:

secure-keys validate scan --output report.json

Override the file extensions and exclusions:

secure-keys validate scan --extensions .rb,.swift,.go --excludes vendor,tmp,build

Enable verbose output:

secure-keys validate scan --verbose

Full option reference:

Usage: secure-keys validate scan [path] [--options]

    -h, --help          Show help for the scan subcommand
        --staged        Scan staged git changes instead of a directory (default: false)
    -o, --output FILE   Save the scan report as JSON to FILE
        --extensions    Comma-separated file extensions to scan (e.g. .rb,.swift)
        --excludes      Comma-separated directory names to exclude from the scan
        --verbose       Enable verbose output (default: false)

Exit codes:

Code Meaning
0 Scan completed with no findings
1 One or more secrets were detected

Validating a Secret

SecureKeys::Validation::Validator checks a single value against a set of security rules and returns a ValidationResult.

require 'validation/validator'

validator = SecureKeys::Validation::Validator.new
result    = validator.validate(key: :api_key, value: ENV['API_KEY'])

puts result.summary          # ✅ api_key — no issues  /  ❌ api_key — 2 issue(s)
puts result.valid?           # true / false
puts result.severity_level   # :ok | :warning | :error | :critical
result.print                 # formatted report to stdout

Available validation options:

Option Type Default Description
check_entropy Boolean false Flag low-entropy (repetitive) values
allow_production Boolean false Skip the production-key warning
warn_on_pattern Boolean false Emit an informational notice when a pattern matches
result = validator.validate(
  key: :stripe_key,
  value: ENV['STRIPE_KEY'],
  options: { check_entropy: true, warn_on_pattern: true }
)

Detect the type of a secret value:

info = validator.detect_type(value: 'ghp_abc...')
# => { type: :github_token, description: "GitHub Personal Access Token", severity: :high, ... }

Get provider-specific security recommendations:

validator.recommendations(key: :githubToken)
# => ["Use GitHub Personal Access Tokens with minimal required scopes", ...]

Scanning Files for Exposed Secrets

SecureKeys::Validation::Scanner scans source files or git diffs for credentials that match any of the 25+ built-in patterns.

Scan a directory:

require 'validation/scanner'

scanner = SecureKeys::Validation::Scanner.new
result  = scanner.scan_directory(path: '.')

puts result.clean?        # true if no findings
puts result.files_count   # number of files scanned

result.findings.each { |f| puts f.to_s }
result.print if !result.clean?

Scan only staged git changes (useful in a pre-commit hook):

result = scanner.scan_git_diff                    # staged only (default)
result = scanner.scan_git_diff(staged_only: false) # staged + unstaged

Customize the scan at initialization or per call:

scanner = SecureKeys::Validation::Scanner.new(
  options: {
    extensions: ['.rb', '.swift', '.go'],
    excludes:   ['vendor', 'node_modules', '.git'],
    max_depth:  5
  }
)

Filter findings by severity:

result.by_severity(severity: :critical).each { |f| puts f.to_s }

Detected Patterns

The scanner recognizes the following secret types out of the box:

Pattern Severity
GitHub personal / OAuth / App / refresh token high
AWS access key ID critical
AWS secret access key critical
Google Cloud API key high
Google OAuth token high
Stripe secret key (live / test) critical
Stripe publishable / restricted key medium / high
Slack bot / app / webhook token high / medium
JWT token medium
PEM / RSA / EC / OpenSSH private key critical
Generic API key assignment medium
Generic secret / password assignment medium
Firebase API key medium
Twilio API key / Account SID high / low
SendGrid API key high
Mailchimp API key medium
Square access token high
PayPal Braintree access token critical
Heroku API key high
Base64-encoded secret low
Suspicious assignment (catch-all) low

Validation Configuration

All thresholds can be overridden with environment variables. Both the bare name and the SECURE_KEYS_ prefix are supported.

Environment variable Default Description
SECURE_KEYS_API_KEY_LENGTH 20 Minimum API key length
SECURE_KEYS_TOKEN_LENGTH 20 Minimum token length
SECURE_KEYS_SECRET_LENGTH 16 Minimum secret length
SECURE_KEYS_PASSWORD_LENGTH 12 Minimum password length
SECURE_KEYS_KEY_LENGTH 16 Minimum generic key length
SECURE_KEYS_SCAN_EXTENSIONS .swift,.rb,.py,.js,... Comma-separated file extensions to scan
SECURE_KEYS_SCAN_EXCLUDES .git,node_modules,Pods,... Comma-separated names to exclude
SECURE_KEYS_MAX_SCAN_DEPTH 10 Maximum directory traversal depth
SECURE_KEYS_MIN_ENTROPY_THRESHOLD 3.0 Shannon entropy threshold for check_entropy

Troubleshooting

Error fetching the key from Keychain

Verify the Keychain service, account, and identifier values. For the default configuration, the list must be stored with account secure-keys and service secure-keys.

Error fetching the key from ENV variables

Verify CI mode is enabled and that each configured key has a matching environment variable. For example, github-token maps to GITHUB_TOKEN.

xcodebuild fails

Verify Xcode command line tools are installed and selected:

xcode-select -p

SecureKeys.xcframework is not found by Xcode

Verify $(SRCROOT)/.secure-keys is present in Framework Search Paths and that the framework is attached to the correct target.

Development

Install dependencies:

bundle install

Run the test suite:

bundle exec rspec

Run the local CLI:

bundle exec ./bin/secure-keys --help

License

This project is licensed under the MIT License.