一般社団法人 全国個人事業主支援協会

COLUMN コラム

  • モバイルアプリのセキュリティ対策:証明書ピンニングとデータ暗号化

モバイルアプリ特有のセキュリティリスク

モバイルアプリケーションは、Webアプリケーションとは異なるセキュリティ上の課題を抱えています。端末がユーザーの手元にあるため、中間者攻撃、リバースエンジニアリング、ローカルデータの漏洩といったリスクに直面します。特に金融系アプリやヘルスケアアプリなど、機密性の高いデータを扱う場合は、堅牢なセキュリティ対策が不可欠です。

本記事では、モバイルアプリセキュリティの中でも特に重要な「証明書ピンニング」と「データ暗号化」にフォーカスし、Android(Kotlin)とiOS(Swift)の両プラットフォームでの実装方法を解説します。

証明書ピンニングの仕組みと重要性

証明書ピンニング(Certificate Pinning)とは、アプリが通信するサーバーの証明書を事前にアプリ内に埋め込み、通信時にその証明書と一致するかを検証する技術です。これにより、中間者攻撃(MITM攻撃)を効果的に防止できます。

通常のSSL/TLS通信では、OSにインストールされたルート証明書を信頼チェーンとして検証します。しかし、攻撃者が端末に不正なルート証明書をインストールした場合、正規の通信を傍受される可能性があります。証明書ピンニングは、この脆弱性を解消するための重要な対策です。

Androidでの証明書ピンニング実装

Androidでは、OkHttpのCertificatePinnerを使った実装が一般的です。

import okhttp3.CertificatePinner
import okhttp3.OkHttpClient

class SecureApiClient {
private val certificatePinner = CertificatePinner.Builder()
.add(
"api.example.com",
"sha256/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
)
.add(
"api.example.com",
"sha256/BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
)
.build()

val client = OkHttpClient.Builder()
.certificatePinner(certificatePinner)
.connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)
.build()
}

ピンの値は、サーバー証明書のSPKI(Subject Public Key Info)のSHA-256ハッシュです。バックアップピンとして複数のハッシュを登録することを強く推奨します。

iOSでの証明書ピンニング実装

iOSではURLSessionDelegateを使って証明書ピンニングを実装します。

import Foundation
import CryptoKit

class PinnedSessionDelegate: NSObject, URLSessionDelegate {
private let pinnedHashes: Set<String> = [
"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=",
"BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB="
]

func urlSession(
_ session: URLSession,
didReceive challenge: URLAuthenticationChallenge,
completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Void
) {
guard let serverTrust = challenge.protectionSpace.serverTrust,
let certificate = SecTrustGetCertificateAtIndex(serverTrust, 0) else {
completionHandler(.cancelAuthenticationChallenge, nil)
return
}

let serverCertData = SecCertificateCopyData(certificate) as Data
let serverHash = SHA256.hash(data: serverCertData)
let hashString = Data(serverHash).base64EncodedString()

if pinnedHashes.contains(hashString) {
completionHandler(.useCredential, URLCredential(trust: serverTrust))
} else {
completionHandler(.cancelAuthenticationChallenge, nil)
}
}
}

ローカルデータの暗号化

Androidでの暗号化ストレージ

AndroidではJetpack SecurityライブラリのEncryptedSharedPreferencesを活用するのが最も簡便かつ安全な方法です。

import androidx.security.crypto.EncryptedSharedPreferences
import androidx.security.crypto.MasterKeys

class SecureStorage(context: Context) {
private val masterKeyAlias = MasterKeys.getOrCreate(MasterKeys.AES256_GCM_SPEC)

private val sharedPreferences = EncryptedSharedPreferences.create(
"secure_prefs",
masterKeyAlias,
context,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)

fun saveToken(token: String) {
sharedPreferences.edit().putString("auth_token", token).apply()
}

fun getToken(): String? {
return sharedPreferences.getString("auth_token", null)
}
}

iOSでのKeychain活用

iOSでは、Keychainが最も安全なデータ保存先です。

import Security

class KeychainManager {
static func save(key: String, data: Data) -> Bool {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data,
kSecAttrAccessible as String: kSecAttrAccessibleWhenUnlockedThisDeviceOnly
]
SecItemDelete(query as CFDictionary)
let status = SecItemAdd(query as CFDictionary, nil)
return status == errSecSuccess
}

static func load(key: String) -> Data? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var result: AnyObject?
SecItemCopyMatching(query as CFDictionary, &result)
return result as? Data
}
}

セキュリティ対策のベストプラクティス

証明書ピンニングとデータ暗号化に加えて、以下の対策も併せて実施することを推奨します。

  • 難読化:ProGuard/R8(Android)、コンパイル最適化(iOS)でリバースエンジニアリングを困難に
  • ルート検出・Jailbreak検出:改ざんされた端末での動作を制限する
  • スクリーンキャプチャ防止:機密画面でのスクリーンショット制御
  • ログ出力の制御:本番ビルドでは機密情報をログに出力しない

モバイルアプリのセキュリティは多層防御が基本です。単一の対策に依存するのではなく、複数の防御層を組み合わせることで、攻撃者にとってのハードルを高くすることが重要です。

この記事をシェアする

  • Twitterでシェア
  • Facebookでシェア
  • LINEでシェア