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