Android 보안 - Sizuha/devdog GitHub Wiki
꼭 필요한 경우가 아니면, 아래 권한은 삭제
- READ_EXTERNAL_STORAGE
- WRITE_EXTERNAL_STORAGE
AndroidManifest.xml에서, android:usesCleartextTraffic 설정을 false로 하면 HTTPS등 암호화된 통신만 허용된다.
<application
. . .
android:usesCleartextTraffic="false">
WebSettings의 allowFileAccess 속성을 false로 설정.
mWebView?.settings?.apply {
// ...
allowFileAccess = false // セキュリティー問題により、非活性化
}
https://developer.android.com/training/articles/security-config
SSL인증서의 공개키에 대한 해시값을 취득
openssl s_client -servername 도메인 -connect 도메인:443 | openssl x509 -pubkey -noout | openssl rsa -pubin -outform der | openssl dgst -sha256 -binary | openssl enc -base64
Example from the link above. Add to AndroidManifest.xml
:
<manifest ... >
<application android:networkSecurityConfig="@xml/network_security_config" ... >
...
</application>
</manifest>
Create a resource file xml/network_security_config.xml
:
<network-security-config>
<domain-config>
<domain includeSubdomains="true">example.com</domain>
<pin-set expiration="2018-01-01">
<pin digest="SHA-256">공개키 해시값</pin>
</pin-set>
</domain-config>
</network-security-config>
https://re-engines.com/2018/12/26/safetynet-attestation-api-basic/
Google의 SafetyNet Attestation API를 활용. Google Play Integrity API로 변경.
-
Google API Consoleのライブラリページに移動します。
-
Android Device Verification APIを検索して選択します。Android Device Verification API ダッシュボードの画面が表示されます。
-
API がまだ有効になっていない場合は、[有効にする] をクリックします。
-
[認証情報を作成] ボタンが表示された場合は、それをクリックして API キーを生成します。表示されなかった場合は、[すべての API 認証情報] プルダウン リストをクリックしてから、Android Device Verification API を有効にしたプロジェクトに関連付けられている API キーを選択します。
-
左のサイドバーで、[認証情報] をクリックします。表示された [API キー] をコピーします。
API결과값을 해석하기 위해서느 JWT라이브러가 필요하지만, 서버에 전송해서 결과를 해석하는 경우에 앱에서는 필요없다.
import com.nimbusds.jose.JWSObject // JWT 라이브러리 사용
// Root化確認
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS) {
// The SafetyNet Attestation API is available.
val key = getString(R.string.safety_net_api_key)
val nonce = getRequestNonce("attest")
SafetyNet.getClient(this).attest(nonce, key)
.addOnSuccessListener(this) {
val result = it.jwsResult
var isOk = false
try {
val jwsObject = JWSObject.parse(result)
//println("payload = " + jwsObject.payload.toJSONObject())
val payload = jwsObject.payload.toJSONObject()
// Package name
val pk = payload["apkPackageName"] as? String ?: ""
isOk = this.packageName == pk
// basicIntegrity
val chk_bi = payload["basicIntegrity"] as? Boolean == true
// ctsProfileMatch
val chk_cts = payload["ctsProfileMatch"] as? Boolean == true
isOk = isOk && chk_bi && chk_cts
// Nonce
//val r_nonce = payload["nonce"] as? String ?: ""
// . . .
} catch (e: java.text.ParseException) {
//e.printStackTrace()
isOk = false
}
if (isOk) {
// TODO 다음으로
} else {
// TODO 에러표시
}
}
.addOnFailureListener(this) { e ->
// An error occurred while communicating with the service.
if (e is ApiException) {
// An error with the Google Play services API contains some
// additional details.
val apiException = e as ApiException
Log.e("ATTEST", e.message + e.statusCode)
} else {
// A different, unknown type of error occurred.
Log.d("ATTEST", "Error: " + e.message)
}
// TODO 에러표시
}
} else {
// TODO Prompt user to update Google Play services.
}
buildTypes {
debug {
minifyEnabled false
}
release {
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
}
}
JavaScript Interface 클래스는 난독화에서 예외처리
# proguard-rules.pro
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
-keepclassmembers class 패키지.클래스명 {
public *;
}
Java JSON Web Token 라이브러리(JWT) 사용시
-keepattributes InnerClasses
-keep class io.jsonwebtoken.** { *; }
-keepnames class io.jsonwebtoken.* { *; }
-keepnames interface io.jsonwebtoken.* { *; }
-keep class org.bouncycastle.** { *; }
-keepnames class org.bouncycastle.** { *; }
-dontwarn org.bouncycastle.**