何が問題なの?
普段あまり、AWSやFIREBASEなどのサービスの利用を何とか避けてニッチなアプリで凌いでいましたが、ついにそんな悠長な事も言って入れなくなってきました。
改めて、この種のサービスを利用する際に常に気になっていたのが、こんな記法です。SDKがこれらの情報を必要とするのは理解できますが、これらの情報をリテラルで渡すのはかなり気になります。スクリプトなどを介在して環境変数から、これらの情報を渡す場合もあるようですが、コンパイル時にはリテラルになっているのでやはり気になります。
WorldGreatService(apiKey: "123456", secretKey: "abcdef")
例えば、ターゲットとなるアプリがあったとします。WorldGreatService
を利用しているのがわかっているものとします。そして、WorldGreatService
がSDKを公開していたとします。
自分でも同SDKを組み込んでみるとします。自分用のapi-keyとsecret-keyを取得して上記同様にリテラルで組み込んでビルドしてみます。次にビルドしたアプリのバイナリから、自分のapi-keyとsecret-keyを探して見つけます。見つけたら、その前後のバイト列のメモします。
次に、ターゲットのアプリからその前後のバイト列を検索などで見つけます。この二つのバイナリー列を比較すると……
実際に試したわけではないので、ビルドされたコードからそんな単純に見つかるとは思いませんが、ハッカーがこんな感じでキーをクラックする手がかりになるのは容易に想像できると思います。
ちなみに、今回紹介する方法は、専門的な情報機関からのクラック対策を目標としたものではなく、あくまでもカジュアルハッカーからのクラック対策を目標としています。また、クラックされない事を保証したものではなく、気軽にハックできない程度を目標としています。
Getting Started
サンプルプロジェクトは以下のURLから入手可能です。
プロジェクト内には.credentials
フォルダがあります。この中にcredentials.plist
というファイルがあるので、この中にAPI KEYやSECRET KEYなどの秘匿情報を記載します。が、間違っても、Xcodeのプロジェクトに組み込まないでください。ipa
を開くと丸見えになります。
次にTerminalから./configure
を実行します。
./configure
./configure
はcodegen.swift
を実行します。別に、このcodegen.swift
を直接走らせてもかまいません。同フォルダにAES256.swift
がある事を確認してください。
実行すると、同フォルダ内にCredentials.swift
が生成されます。このソースファイルをXcodeのプロジェクトに組み込みます。しつこいようですが間違ってもcredentials.plist
を組み込まないでください。
ちなみに、のちにcredentials.plist
を変更した場合は./configure
を再度実行し、Credentials.swift
を再生成します。
Using Credentials
Credentials.swift
を組み込んだら、これで秘匿情報をアプリで取得する準備が完了です。
let my_api_key = CREDENTIALS.shared["MY_API_KEY"] as! String
let my_secert_key = CREDENTIALS.shared["MY_SECERT_KEY"] as! String
WorldGreatService(apiKey: my_api_key, secretKey:my_secert_key)
Credentials.swift
ちなみに、生成されたCredentials.swift
は以下のようなコードになります。
import Foundation
import CommonCrypto
struct AES256Key {
static let length = Int(kCCKeySizeAES256)
let key: [UInt8]
init(string: String) {
var buffer = [UInt8](repeating: 0, count: Self.length)
if let data = string.data(using: .utf8) {
let bytes = [UInt8](data)
(0 ..< min(bytes.count, Self.length)).forEach { buffer[$0] = bytes[$0] }
}
assert(buffer.count == Self.length)
self.key = buffer
}
init?(data: Data) {
guard data.count == Self.length else { return nil }
var buffer = [UInt8](repeating: 0, count: Self.length)
(0 ..< Self.length).forEach { buffer[$0] = data[$0] }
self.key = buffer
}
}
extension Data {
static private let keyLength = Int(kCCKeySizeAES256+1)
func encryptAES256(key: AES256Key) -> Data? {
let blockSize = Int(kCCBlockSizeAES128)
var buffer = [UInt8](repeating: 0, count: self.count + blockSize)
var numberOfBytesDecrypted: size_t = 0
let status = CCCrypt(CCOperation(kCCEncrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding),
key.key, kCCKeySizeAES256,
nil, // initialization vector (optional)
(self as NSData).bytes, self.count, // input
&buffer, buffer.count, // output
&numberOfBytesDecrypted);
if status == kCCSuccess {
return Data(bytes: &buffer, count: Int(numberOfBytesDecrypted))
}
return nil
}
func decyptAES256(key: AES256Key) -> Data? {
let blockSize = Int(kCCBlockSizeAES128)
var buffer = [UInt8](repeating: 0, count: self.count + blockSize)
var numberOfBytesDecrypted: size_t = 0
let status = CCCrypt(CCOperation(kCCDecrypt), CCAlgorithm(kCCAlgorithmAES128), CCOptions(kCCOptionPKCS7Padding),
key.key, kCCKeySizeAES256,
nil, // initialization vector (optional)
(self as NSData).bytes, self.count, // input
&buffer, buffer.count, // output
&numberOfBytesDecrypted);
if status == kCCSuccess {
return Data(bytes: &buffer, count: Int(numberOfBytesDecrypted))
}
return nil
}
}
let aesKeyBin = Data([0xaa,0xda,0xfd,0x38,0x3f,0x3f,0x3d,0xe6,0x18,0xab,0x06,0x02,0x8a,0x99,0x65,0x4c,0x3d,0x41,0x65,0xea,0x00,0xf9,0x36,0x76,0xe6,0x57,0x71,0x2e,0x15,0x9f,0xb4,0x7f])
let aesKey = AES256Key(data: aesKeyBin)!
let encryptedBin = Data([0xd0,0xb2,0x82,0x17,0xaf,0xf7,0x62,0x50,0xde,0xcd,0xa1,0xc2,0x0f,0xcb,0x45,0x53,0xb5,0xc5,0x84,0x9f,0x32,0x8b,0x9e,0xd7,0xf9,0x1b,0x96,0xe0,0xad,0x19,0xd8,0xca,0x2e,0x5d,0xd2,0x07,0x24,0x0e,0x9f,0xa7,0x64,0x17,0xd4,0x7c,0xc7,0x24,0x0c,0x0f,0x17,0x9e,0x55,0x82,0x47,0x44,0x24,0xba,0x55,0x88,0x86,0x17,0xb8,0x29,0x1c,0xd0,0x0a,0x3b,0x55,0x67,0xd1,0xf5,0x58,0x41,0x4a,0x6f,0x3e,0x90,0x6d,0xe3,0xd5,0xe2,0xb8,0x47,0x87,0xaf,0xc0,0x81,0xd9,0xb5,0x03,0x31,0xb1,0x41,0x52,0x44,0xba,0xb9,0x9a,0x3b,0x06,0xed,0x0a,0x28,0xdb,0x07,0x1b,0xe2,0xf5,0xbd,0xb7,0x13,0xca,0x7a,0xcd,0x3e,0xa8,0x1f,0xce,0xc6,0xea,0xea,0x95,0x89,0xb7,0xea,0x58,0x1e,0x93,0xfb,0x50,0x66,0xdb,0x72,0x2c,0x97,0x98,0x6d,0xe6,0x5a,0x28,0xd8,0x7f,0x1f,0x53,0x47,0x84,0x8b,0xed,0x8c,0x0a,0xdc,0x80,0xf1,0xfe,0x48,0xbb,0xeb,0xaf,0xd6,0x32,0xc0,0x94,0x49,0x39,0xf9,0x91,0xba,0x7b,0xb7,0x11,0x1f,0x76,0x8f,0xcf,0x99,0xe5,0x2d,0x6d,0x22,0xa7,0x4d,0x10,0x66,0x4c,0x09,0x63,0x55,0x2d,0xc2,0x7c,0x99,0x6c,0x6f,0x39,0x6a,0xf1,0x2c,0x4b,0x92,0xcc,0xf9,0x6e,0x3a,0x4f,0xed,0x90,0xc6,0x49,0x6e,0x8a,0x27,0xd9,0x54,0x94,0x84,0x91,0xc9,0xf3,0xdc,0xfa,0xf7,0xa3,0x64,0x75,0x3a,0x97,0xa9,0xc7,0xa7,0x74,0x8a,0x9e,0x40,0x85,0xdc,0xa9,0x2e,0x7d,0x21,0xd0,0x5a,0x90,0x50,0x6d,0x53,0x9f,0x2a,0x31,0xc3,0x3c,0xd4,0xf9,0xee,0x0f,0xe2,0xe0,0x53,0x81,0xd2,0x53,0xda,0xdf,0x01,0x7b,0x90,0xd8,0x7f,0x33,0x24,0x95,0xa7,0x4b,0x96,0xcc,0x4b,0x25,0x5e,0x61,0x86,0x63,0xee,0x52,0x7c,0x30,0x82,0x9a,0x0c,0x22,0x61])
let decryptedBin = encryptedBin.decyptAES256(key: aesKey)!
class CBBA33F3DC70A4502851183DCF579CB1E {
static let shared = CBBA33F3DC70A4502851183DCF579CB1E()
let dictionary: [String: Any]?
private init() {
dictionary = try? PropertyListSerialization.propertyList(from: decryptedBin, options: [], format: nil) as? [String: Any]
}
subscript(key: String) -> Any? {
return dictionary?[key]
}
}
typealias CREDENTIALS = CBBA33F3DC70A4502851183DCF579CB1E
暗号キーと、その暗号キーで暗号化されたコンテンツはソースコードの形で埋め込まれています。そして、CREDENTIALS
で利用するクラスは、UUID
に類似した名前で実装されており、そのtypealias
でCREDENTIALS
を参照しています。こうする事で、class_dump
のようなツールでSymbolを覗かれても、CREDENTIALS
の類のシンボルが見つからないので、クラックされにくくなると考えています。
UUID似の名前は実行毎に新しくなりますが、typealias
のCREDENTIALS
で利用すれば、クライアントコードのブレークはないはずです。最も、シンボルの中にこんな不思議な名前のクラスがあれば、やはり不自然ですが、何より「MY_API_KEY」とか「MY_SECRET_KEY」とかいった名前でひくのは、もっと問題かと思いますが、あまり変な名前にすると、可読性が落ちるので、利用する場合はその辺りのバランスを考える必要があります。
CHAT GPT
ちなみに、Chat GPTにリテラル埋め込みの危険性を訊いてみたらこんな答えが返ってきました。
(質問)
There are many SDKs or other services require API keys or secret keys, and typical way to initialize services from app is like this.
WorldService(apiKey: "123456", secretKey: "abcdef")
I am curious how hard or easy to crack this this code for casual hacker to extract those keys?
(回答)
If the API key and secret key are hardcoded in the app's binary, they can be extracted by reverse engineering the app. Therefore, it is not recommended to hardcode sensitive information like API keys and secret keys in the app's binary. Instead, it is better to store them securely on the server side and require authentication before granting access to the keys. Another approach is to use environment variables or a configuration file that is loaded at runtime, rather than hardcoding the keys in the source code.
その対策についても聞いてみました。この課題に取り組む前に聞けばよかったような気もしますが、まぁいいとします。
(質問)
what is the typical way to avoid this?
(回答)
To avoid exposing API keys and secret keys in your code, you can use environment variables or configuration files to store these sensitive information.
For example, you can store your API keys and secret keys in a separate configuration file, and load them into your application at runtime. You can then configure your application to read the configuration file and use the keys to authenticate with your API.
Another option is to use a service like HashiCorp Vault or AWS Secrets Manager to store and manage your API keys and secrets securely. These services provide a secure way to manage and distribute secrets to your applications without exposing them in your code.
環境
執筆時点での環境は以下のとおりです。
$ swift --version
swift-driver version: 1.75.2 Apple Swift version 5.8 (swiftlang-5.8.0.124.2 clang-1403.0.22.11.100)
Target: arm64-apple-macosx13.0