0.01
No commit activity in last 3 years
No release in over 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
 Dependencies

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. The configuration looks as follows:

MobileSecrets:
  # hashKey: Key that will be used to hash the secret values.
  # For encrypting files the key needs to be 32 chars long as an AES standard.
  hashKey: "KokoBelloKokoKokoBelloKokoKokoBe"
  # 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"
  # Key-value dictionary for secrets. The key is then referenced in the code to get the secret.
  secrets:
    googleMaps: "123123123"
    firebase: "asdasdasd"
    amazon: "asd123asd123"
  # Optional, remove files if you do not want to encrypt them
  files:
    - tmp.txt
    - Info.plist

Hash key needs to be provided for obfuscating secrets or encrypting files with AES. MobileSecrets stores the key by default as an array of bytes within the bytes array of secrets. This can be changed by setting the shouldIncludePassword flag to false in the configuration. Handling the key becomes then dependent on the developer. While this approach brings a better security as the key IS NOT part of the compiled binary, it comes with the downside of fetching the key. No need to say that the key must be stored securely on the device in order to de-obfuscate the secrets or decrypt the files. A generated file from this configuration can then be imported into the iOS project and used as follows:

let googleMaps = Secrets.standard.string(forKey: "googleMaps")
try? Secrets.standard.decryptFiles() // This can be executed only once as the file will stay on the drive.

A generated file can look as follows:

//
//  Autogenerated file by Mobile Secrets
//

import CommonCrypto
import Foundation

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],
                                    [102, 105, 114, 101, 98, 97, 115, 101],
                                    [42, 28, 15, 14, 49, 1, 13, 31, 11],
                                    [97, 109, 97, 122, 111, 110],
                                    [42, 28, 15, 94, 112, 86, 13, 31, 11, 122, 93, 88]]

    private let fileNames: [[UInt8]] = [[116, 109, 112, 46, 116, 120, 116],
                                        [73, 110, 102, 111, 46, 112, 108, 105, 115, 116]]


    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)
    }

    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
    }


    func decryptFiles(bundle: Bundle = Bundle.main, password: String? = nil) throws {
        try fileNames.forEach({ (fileNameBytes) in
            guard let name = String(data: Data(fileNameBytes), encoding: .utf8) else {
                fatalError("Wrong name in file names")
            }

            try decryptFile(name, bundle: bundle, password: password)
        })
    }

    func decryptFile(_ fileName: String, bundle: Bundle = Bundle.main, password: String? = nil) throws {
        let password = password == nil ? String(data: Data(bytes[0]), encoding: .utf8) : password

        guard let pwd = password else {
            fatalError("No password for decryption was provided!")
        }

        guard let filePath = bundle.path(forResource: fileName, ofType: "enc"),
            let fileURL = URL(string: "file://" + filePath),
            let fileData = try? Data(contentsOf: fileURL) else {
                fatalError("File \(fileName) was not found in bundle!")
        }

        var outputURL = bundle.bundleURL
        outputURL.appendPathComponent(fileName)

        do {
            let aes = try AES(keyString: pwd)
            let decryptedString = try aes.decrypt(fileData)
            try decryptedString.write(to: outputURL, atomically: true, encoding: .utf8)
        } catch let e {
            throw e
        }
    }

    struct AES {
        enum Error: Swift.Error {
            case invalidKeySize
            case encryptionFailed
            case decryptionFailed
            case dataToStringFailed
        }

        private var key: Data
        private var ivSize: Int = kCCBlockSizeAES128
        private let options: CCOptions  = CCOptions()

        init(keyString: String) throws {
            guard keyString.count == kCCKeySizeAES256 else {
                throw Error.invalidKeySize
            }
            self.key = Data(keyString.utf8)
        }

        func decrypt(_ data: Data) throws -> String {
            let bufferSize: Int = data.count - ivSize
            var buffer = Data(count: bufferSize)
            var numberBytesDecrypted: Int = 0

            do {
                try key.withUnsafeBytes { keyBytes in
                    try data.withUnsafeBytes { dataToDecryptBytes in
                        try buffer.withUnsafeMutableBytes { bufferBytes in

                            guard let keyBytesBaseAddress = keyBytes.baseAddress,
                                let dataToDecryptBytesBaseAddress = dataToDecryptBytes.baseAddress,
                                let bufferBytesBaseAddress = bufferBytes.baseAddress else {
                                    throw Error.encryptionFailed
                            }

                            let cryptStatus: CCCryptorStatus = CCCrypt( // Stateless, one-shot encrypt operation
                                CCOperation(kCCDecrypt),                // op: CCOperation
                                CCAlgorithm(kCCAlgorithmAES),        // alg: CCAlgorithm
                                options,                                // options: CCOptions
                                keyBytesBaseAddress,                    // key: the "password"
                                key.count,                              // keyLength: the "password" size
                                dataToDecryptBytesBaseAddress,          // iv: Initialization Vector
                                dataToDecryptBytesBaseAddress + ivSize, // dataIn: Data to decrypt bytes
                                bufferSize,                             // dataInLength: Data to decrypt size
                                bufferBytesBaseAddress,                 // dataOut: decrypted Data buffer
                                bufferSize,                             // dataOutAvailable: decrypted Data buffer size
                                &numberBytesDecrypted                   // dataOutMoved: the number of bytes written
                            )

                            guard cryptStatus == CCCryptorStatus(kCCSuccess) else {
                                throw Error.decryptionFailed
                            }
                        }
                    }
                }
            } catch {
                throw Error.encryptionFailed
            }

            let decryptedData: Data = buffer[..<numberBytesDecrypted]
            guard let decryptedString = String(data: decryptedData, encoding: .utf8) else {
                throw Error.dataToStringFailed
            }

            return decryptedString
        }
    }
}

mobile-secrets usage:

  1. Create gpg first with --init-gpg "."
  2. Create a template for MobileSecrets with --create-template
  3. Configure MobileSecrets.yml with your hash key, secrets etc
  4. Import edited template to encrypted secret.gpg with --import ./MobileSecrets.yml
  5. Export secrets from secrets.gpg to source file with --export and PATH to project
  6. Add exported source file to the project
  7. Delete the configuration from your drive or repository as it is already stored and encrypted with GPG in the secrets.gpg file.