0.01
No release in over 3 years
Low commit activity in last 3 years
Handle mobile secrets the secure way with ease
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
 Dependencies

Development

Runtime

= 0.7.0
 Project Readme

Mobile secrets

Handle mobile secrets the secure way with ease

Working with this GEM is described in detail here: https://medium.com/@cyrilcermak/mobile-secrets-8458ceaf4c16

MobileSecrets comes with a simple YAML configuration. The configuration can be generated by executing mobile-secrets --create-template.

Configuration

MobileSecrets:
  # hashKey: Key used to obfuscate/encrypt secret values.
  # Must be exactly 32 characters when using alg: "AES-GCM" or when encrypting files.
  hashKey: "REPLACE_THIS_32_CHAR_HASH_KEY___"
  # shouldIncludePassword: By default the password is saved in the code as a series of bytes, however it can also
  # be fetched from your API, saved in keychain and passed to the Secrets for improving the security.
  shouldIncludePassword: true
  # language: Swift is currently only supported language, Kotlin is coming soon.
  language: "Swift"
  # alg: Algorithm used to obfuscate secret values at build time.
  #   "XOR"    — XOR cipher (default). Fast, no platform version requirements.
  #   "AES-GCM" — AES-256-GCM authenticated encryption via CryptoKit.
  #               Requires iOS 13+ / macOS 10.15+ and a 32-character hashKey.
  alg: "XOR"
  # Key-value dictionary for secrets. The key is then referenced in the code to get the secret.
  secrets:
    googleMaps: "YOUR_GOOGLE_MAPS_KEY"
    firebase: "YOUR_FIREBASE_KEY"
    amazon: "YOUR_AMAZON_KEY"
  # Optional — remove the files section if you do not want to encrypt files.
  # File encryption always uses AES-256-CBC and requires a 32-character hashKey.
  files:
    - tmp.txt
    - Info.plist

Choosing an algorithm

XOR (default) AES-GCM
Security Obfuscation only Authenticated encryption (strong)
iOS requirement Any iOS 13+ / macOS 10.15+
hashKey length Any Exactly 32 characters
Swift framework Foundation CryptoKit
Per-secret IV No Derived deterministically (HMAC-SHA256)

Tip: Use AES-GCM for stronger protection at build time. Use XOR when you need to support iOS 12 or earlier, or when you supply the password at runtime from Keychain / a remote API.

Usage in iOS

The generated secrets.swift is dropped into your Xcode project and used as follows:

// Retrieve a secret (password embedded in binary)
let googleMapsKey = Secrets.standard.string(forKey: "googleMaps")

// Retrieve a secret with a runtime password (shouldIncludePassword: false)
let googleMapsKey = Secrets.standard.string(forKey: "googleMaps", password: myRuntimeKey)

// Decrypt bundled .enc files (run once — decrypted file persists on disk)
try? Secrets.standard.decryptFiles()

XOR-generated secrets.swift (example)

//
//  Autogenerated file by Mobile Secrets
//

import Foundation

// swiftlint:disable all
class Secrets {
    static let standard = Secrets()
    private let bytes: [[UInt8]] = [[75, 111, 107, 111, 66, 101, 108, 108, 111, 75, 111, 107, 111,
                                     75, 111, 107, 111, 66, 101, 108, 108, 111, 75, 111, 107, 111,
                                     75, 111, 107, 111, 66, 101],
                                    [103, 111, 111, 103, 108, 101, 77, 97, 112, 115],
                                    [122, 93, 88, 94, 112, 86, 93, 94, 92]]

    private init() {}

    func string(forKey key: String, password: String? = nil) -> String? {
        let pwdBytes = password == nil ? bytes[0] : password?.map({ c in c.asciiValue ?? 0 })
        guard let index = bytes.firstIndex(where: { String(data: Data($0), encoding: .utf8) == key }),
            let pwd = pwdBytes,
            let value = decrypt(bytes[index + 1], password: pwd) else { return nil }
        return String(data: Data(value), encoding: .utf8)
    }

    // XOR-based deobfuscation. The key cycles over the password bytes.
    private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
        guard !password.isEmpty else { return nil }
        var output = [UInt8]()
        for byte in input.enumerated() {
            output.append(byte.element ^ password[byte.offset % password.count])
        }
        return output
    }
}

AES-GCM-generated secrets.swift (example)

Each secret value is encrypted with AES-256-GCM. The IV is derived deterministically from HMAC-SHA256(hashKey, "secretName:value"), so the generated byte arrays are stable across exports for the same input — giving you reproducible builds and clean git diffs. Rotating a secret value automatically derives a new IV, preventing nonce reuse.

//
//  Autogenerated file by Mobile Secrets
//

import CryptoKit
import Foundation

// swiftlint:disable all
@available(iOS 13.0, macOS 10.15, tvOS 13.0, watchOS 6.0, *)
class Secrets {
    static let standard = Secrets()
    private let bytes: [[UInt8]] = [[75, 111, 107, 111, 66, 101, 108, 108, 111, 75, 111, 107, 111,
                                     75, 111, 107, 111, 66, 101, 108, 108, 111, 75, 111, 107, 111,
                                     75, 111, 107, 111, 66, 101],
                                    [103, 111, 111, 103, 108, 101, 77, 97, 112, 115],
                                    // IV(12) + AuthTag(16) + Ciphertext(N)
                                    [34, 201, 88, 12, 77, 203, 11, 99, 56, 210, 4, 33,
                                     145, 67, 23, 189, 200, 44, 90, 12, 78, 56, 33, 199, 100, 55, 21, 9,
                                     88, 201, 65, 77, 12, 90, 55, 23, 200]]

    private init() {}

    func string(forKey key: String, password: String? = nil) -> String? {
        let pwdBytes = password == nil ? bytes[0] : password?.map({ c in c.asciiValue ?? 0 })
        guard let index = bytes.firstIndex(where: { String(data: Data($0), encoding: .utf8) == key }),
            let pwd = pwdBytes,
            let value = decrypt(bytes[index + 1], password: pwd) else { return nil }
        return String(data: Data(value), encoding: .utf8)
    }

    // AES-256-GCM decryption. Input layout: IV(12) + AuthTag(16) + Ciphertext(N)
    private func decrypt(_ input: [UInt8], password: [UInt8]) -> [UInt8]? {
        guard password.count == 32 else { return nil }
        let ivSize = 12; let tagSize = 16
        guard input.count > ivSize + tagSize else { return nil }
        do {
            let key = SymmetricKey(data: Data(password))
            let nonce = try AES.GCM.Nonce(data: Data(input[0..<ivSize]))
            let tag = Data(input[ivSize..<(ivSize + tagSize)])
            let ciphertext = Data(input[(ivSize + tagSize)...])
            let sealedBox = try AES.GCM.SealedBox(nonce: nonce, ciphertext: ciphertext, tag: tag)
            return [UInt8](try AES.GCM.open(sealedBox, using: key))
        } catch { return nil }
    }
}

CLI commands

Command Description
--init-gpg PATH Initialise a GPG keyring in the given directory
--create-template Copy example.yml to ./MobileSecrets.yml
--import SECRETS_PATH [GPG_FILE] GPG-encrypt the YAML config (secrets.gpg by default)
--export PATH [ENCRYPTED_FILE_PATH] Decrypt GPG file and write secrets.swift to PATH
--encrypt-file FILE PASSWORD AES-256-CBC encrypt a single file → FILE.enc
--empty PATH Write an empty Secrets Swift stub to PATH/secrets.swift
--edit GPG_FILE Open the encrypted config in your editor via dotgpg edit
--usage Print numbered workflow instructions

Workflow

  1. Create a GPG keyring in the current directory:
    mobile-secrets --init-gpg "."
  2. Generate the config template:
    mobile-secrets --create-template
  3. Edit MobileSecrets.yml — set your hashKey, choose alg, add your secrets.
  4. Encrypt the config into secrets.gpg:
    mobile-secrets --import ./MobileSecrets.yml
  5. Export secrets.swift to your project:
    mobile-secrets --export ./MyApp/Sources/
  6. Add secrets.swift to your Xcode project target.
  7. Delete MobileSecrets.yml — your secrets are now safely stored in secrets.gpg only.

To update secrets later:

mobile-secrets --edit secrets.gpg   # opens editor via dotgpg
mobile-secrets --export ./MyApp/Sources/

Running the tests

# Full test suite (unit + E2E Swift compilation tests)
rake test

# Single test file
ruby -Ilib -Itest test/test_secrets_handler.rb

# Single test by name
ruby -Ilib -Itest test/test_e2e.rb -n test_aes_gcm_with_embedded_password_round_trips_all_secrets