EncryptedSharedPreference, EncryptedFile

SharedPreferences

앱 데이터 경로의 /data/data/{package_name}/shared_prefs/{filename}.xml 에 Key-Value가 xml 형식의 파일로 저장되는 간단한 로컬 데이터 저장소이다.

사용법

쓰기

SharedPreferences sharedPref = getSharedPreferences("score_prefs", Context.MODE_PRIVATE);

SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(getString(R.string.saved_high_score_key), newHighScore);
editor.putString(getString(R.string.saved_name_key), userName);
editor.putBoolean(getString(R.string.saved_istop_key), isTop);
editor.apply();

결과

/data/data/{package_name}/shared_prefs/score_prefs.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <int name="high_score" value="214748364800" />
    <string name="user_name">dhkim@gmail.com</string>
    <boolean name="is_top" value="true" />
</map>

읽기

SharedPreferences sharedPref = getSharedPreferences("score_prefs", Context.MODE_PRIVATE);

val score = sharedPref.getInt(getString(R.string.saved_high_score_key), 0);
val name = sharedPref.getString(getString(R.string.saved_name_key), "");
val istop = sharedPref.getBoolean(getString(R.string.saved_istop_key), false);

EncryptedSharedPreferences

SharedPreferences 를 Keystore 기반으로 암호화하여 루팅된 기기에서 직접 파일을 읽어 유출되는것이 방지된다.
동적으로 앱을 조작할 수 있는 Frida나 메모리 후킹에서는 취약할 수 있다.

사용법

키 생성

MasterKey masterKey = new MasterKey.Builder(context)
    .setKeyScheme(MasterKey.KeyScheme.AES256_GCM)
    .build();

쓰기

sharedPrefs 를 EncryptedSharedPreferences로 생성해서 가져오면 된다.

EncryptedSharedPreferences sharedPref = EncryptedSharedPreferences.create(
    context,
    "score_prefs",  // 암호화된 파일 이름
    masterKey,      // 위에서 생성한 키
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);

// 사용법은 같다. 
SharedPreferences.Editor editor = sharedPref.edit();
editor.putInt(context.getString(R.string.saved_high_score_key), newHighScore);
editor.putString(context.getString(R.string.saved_name_key), userName);
editor.putBoolean(context.getString(R.string.saved_istop_key), isTop);
editor.apply();

결과

알 수 없는 아주 어지러운 결과가 표시된다.
/data/data/{package_name}/shared_prefs/score_prefs.xml

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="AXfKRRG1W4U19wMCtF7/zHjF8/QmYzzZQ2DrahsEDA==">AX3m6IEgmC75ZIgynQIqbRDGTFB+Urb2biif8verVPb+vFQyrQDhFnU=</string>
    <string name="AXfKRRFA6LrybBXcRj5J1h8MlgSztOeFGI1hmm3o">AX3m6IE6l5T2t9VEhVrUncNUOQrxrcncFUdqwCD/2+AVEFSLMT3ZUCmQjZhj6q6171tijVRH9NU=</string>
    <string name="AXfKRREbFUKw4XyD0PgPjzwI3eglfnfmI9yc">AX3m6IHyqFwrg+aUmQRaFsfYt7DTDM5SzztivZW2BMIaS07ss0o=</string>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a901804a802908fbc0fb51c3e6f862be8ddb2e961af7abd88245febf914c7306917c546b4f1c3dbf64f4799972a192ae095a6f16dd80cffdce57959abf06e22b5bbad4afd6d6e3167d46f2a7cd5600e3f0fc0b46ec4af841e7c965c437d6b26f87bac4fcdd9069cc4b687a225cc7b7fbc63ecfd7ee65f64163ead4ef2300cf5e968cf0c83b995296c778b0c269f6b96eb35880a06aacb232f7362fcca87a6d90f35a3e975f438b04ec98211a4408918aa9be07123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118918aa9be072001</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">1288018eb9419856bc19c30b92c7751619fd3f3209b65c9b024caba1e1ccd929cd94c9ff2de95275ad388452c2f57cdcac670b62869f2030a97855c14cc69c0b71b66aa65ef8d959879e2615bfd19f081569c8c203518f2d4bfd599db16d5dc34b05f58fa8104bdd0b0f9f8e36bbf912fae5b4eb38e405ee9a2e1bade8ee4ce5dbade4922e2781a83aee621a440881d19bef07123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b657910011881d19bef072001</string>
</map>

이 파일을 보면 key_keyset과 value_keyset이 보인다.

SharedPreferences 의 Key와 Value를 각각 암호화하는 키를 Keystore로 얻은 MasterKey를 이용해서 암호화한 값을 메타데이터와 함께 저장한것이다.
읽어올땐 이 값들에서 키를 꺼내 MasterKey로 복호화하고 각각 xml파일에서 Key와 Value를 복호화하여 데이터를 복원한다.

hex로 읽어보면 EncryptedSharedPreferences.create 호출 때 사용한 알고리즘을 확인할 수 있다.

9ee9c0ae-602b-417d-9e3b-0bf3e8df1541
9ee9c0ae-602b-417d-9e3b-0bf3e8df1541

읽기

EncryptedSharedPreferences sharedPref = EncryptedSharedPreferences.create(
    context,
    "score_prefs",
    masterKey,
    EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
    EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
);

int score = sharedPref.getInt(context.getString(R.string.saved_high_score_key), 0);
String name = sharedPref.getString(context.getString(R.string.saved_name_key), "");
boolean isTop = sharedPref.getBoolean(context.getString(R.string.saved_istop_key), false);

복호화하는 과정

Keystore를 사용한 로직이기 때문에 런타임에 Frida로 붙어서 복호화해야한다.
예시 파일은 com.wire 앱이다.

EncryptedSharedPreference 파일 확인

shared_prefs/app-preference.xml 파일이 EncryptedSharedPreference를 사용한 파일인데, __androidx_security_crypto_encrypted_prefs_key_keyset__, __androidx_security_crypto_encrypted_prefs_value_keyset__ 외에는 Base64로 key-value 전부 읽을 수 없게 변해있는 것을 확인할 수 있다.

<?xml version='1.0' encoding='utf-8' standalone='yes' ?>
<map>
    <string name="__androidx_security_crypto_encrypted_prefs_key_keyset__">12a901be6b2c3a17a7495347fa2660563a433709b0e8e3d85f96c63ef0cdcd90779170a8b9cb1a91ba0149739438bc7efc136eedca44186859aa1d4a610ce9a4990fc6bc4a1755ac732faf12345cbc0dce82b59aa877ea707393a98b823e7c8b63abf6eece890db407e466f9cf8892d7d99db5659cfc0613020112779f06a632ea7d7ceea59ce7e2e8960c99efd8fa7c29ef3208d4c6237a064d2c6dbf717bf3c455f3669aa4c91f3f9d949b1a4408b6a2a7f402123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579100118b6a2a7f4022001</string>
    <string name="AS6J0TZmRI0mynw2HQH563OYBGctIdjmnZPUGWTKqKYZgd7muqx4ID8qNPHNe9sSUxdq7ujpigDeD7rCOFpxjTX+1GelH59SRMagaEgwRcBMJ5htDgAw">ARoVyLIQvPcWG5diVbLro2dBKvEPbVAkfiakxSo1tqM99pNQ3cVyy6D7Xb+AKWAY1LFnYDBsBLSvcImzxL4n1UJfDPadg6FzT7hRT/n+RTuWggON6Q==</string>
    <string name="AS6J0TZZMzUqhNEtAw6am9GhKwQl7hMjJajPBFTi9GwpBkElnNEjN2Xf+fayVQOa2PaSOR/SgD5Y/97r/+cpcC8Pq94eObqpI1y4P2EDLzjZ3zWb4dUziT9HX+dJ">ARoVyLJJ2pIMFepeGKHyOmAVMuyLGVo5mHgWglvr3T7zg+eSrG9GY4ZP2rYTyvldbZV5L2K7DXewPLzxA9m52WfKt9Z0I62DkxkqHf1xVB16P22n3w==</string>
    <string name="AS6J0TbJV5j1W/1aelzlmswXqfgcXRHnTiAnWY0p+IRBSkXoukwOgqmMy9BXFnHLMOa5ESJRqLOkV4zqTkgRO8iDwuG7Ryj4ozGMvVzY">ARoVyLIEjIt7nCZxYOhVHlW7SSLumfCHtVMh5uBOVZ6COh4dDZsOJjNUuU7/1EG5//7ekbk/OXRw1zqM46u+8bST9xgPQubdFSjyScRGQG8ioS9ycNjSRq49wuY4r3O1njPQMZRQKZYBSMm4M3ec5ZHIvmjCztg1g1Oczj1WUgo6cZ5DFDh+hH25KnxDoLxq8HGhSGYpzslQShNkdbZ6pQVyEFOUL0wBqVh8vmrzRZxABWy/aKeEJzCjSBrwrtEnNJtIMaqUf3GKTw5xIwYKuccv1rj1HM0YAgCssjZ2iVXWBP4zJuNcgfxLYC/wTQ4xA5kIy10bifaFZbvJKQlkNoYx6m1C36sc5xX+2Kn1AoibllDoscIvMKtGhs7ealI45vecNXb9IKSluNV76QJKezJDZXNOWpGZF/rJsN0FdZEQ3v8pK+WJxnXM+DDsU+0JplaF1QazOLG2Dz3EpSrR5FsBiIxFx7nPBeZVqArUIfoqrfJ2FTGsRstekU9D266vGb6GrNO9I8i3pBDS1DpiAyKPzce6sW1N/N8FzNu0Z7GveA80sb4CoOCDxbhghQ9LPLnvC2TSipUORS8T/C1Y+2gd/rcHR1XnlHCNtv9c97psjoDr8/rnllEk6jFNCbZPFLK9dm99imTusHja92zrLtHA9EC4kaZuLWNrWAFp0bSnFM96qLNtkAnqF/KIelJ5gtHZb2emuBQSn78AI31mwzq/jFqFrPth5my0uB5nffCnVDNW2B4aWfZOMAqEaAgfQNY1ce6H3OK9tsLivxP4+Q6c0oas8l0rx26hkXrND2/G7Y1QzY8euw==</string>
    <string name="AS6J0Ta7FEeuT7T7BrnVlZyv+kOLMXTf9XvsoySJ8mCpwpU16U6Hn2kfFGJAhHWcJDpxIiFi0zUDHCM4eQ96LFxHY5O5p9fK3KV38AZj7SwMUSnJmVacvKM=">ARoVyLJba16KMD4GzkO1/3U/EnKpG4SN4yBHYMgZdgAtWOBgblF+ahyXrbIorKvz6sFFT/XGx/ADzywuK6PXH6ZXo7eRQ8o8mNmWnlQApj2rLspnaQ==</string>
    <string name="__androidx_security_crypto_encrypted_prefs_value_keyset__">128801f7bf08d83eb2bea221034ab470707fad748ef097ec7a876fffa48dc610f366a786f3def7aabac30cfd4a87c7f10693d48c7588f76cd3434e56ad838411a572b422b2bdde1ca0aa4b300b18fab86941f001491cc2adff092fb5e2056d4a0f0e94bf5287a6cebc009de56dda1bbaca8b91d3a46a3e87df7e52c420a8df5da4f269294c14865f9798a71a4408b291d7d001123c0a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b6579100118b291d7d0012001</string>
    <string name="AS6J0Ta4kPae7u49+4nhe+/T/7ugx2Q2pucOpNh9j61FFfetod5ZkKK+sE7VaYo=">ARoVyLKxAXrBu97lW1Hb6yy8gU0RZXPOpfLq42D28WtWue9EiegcheX5D7J5iVRJ0iCEJc/R3zYMJXVrbup6q7MSAes3gazQx0tcfUxfSjoUYc/I7Q==</string>
    <string name="AS6J0TbJ9vuRU6g+2dcED6Zj10aBB+u8SJN698XQceqHlZgQGpwe">ARoVyLJk/s1crmNwbTJwWiaZpe5iUn0UH565w+B9EgiTa3iaHRhWiuQv5z6X4tSOJ6wD4n84sTPpyHnavzgWF5mRbh/FwyFwgAhLxpImxLiS77Z5yyfUUxgIyzwRMXilE+nYbyJt0WcTB4Ojxj7/ZyJdalNe/mv6lLDiqWr7kkAqpTAb08nEJQ089U+pI9NDhQCSi+BcxhEYOF9yoPPtbNHDxDHpbF0aJbqFqfAWXqpMhcJ9ku4mrtjHXnIu06rDQlPMPAtbb1AepEpRG4s666cM0HOElZ6N8yBfdAB6bTb4zAsgWQ4Gb9XyKp9KL+gQprdE4Q==</string>
</map>

key_keyset 은 base64된 key(name)를 복호화 하는 키셋이고, value_keyset은 value를 복호화하는 키셋이다.

keyset 복호화

keyset이 keystore로 암호화 되어있는 녀석이다.
keyset은 protobuf로 직렬화 되어있기 때문에 여기에서 한번 풀어볼 수 있다.

2번 필드에 어떤 hexstring이 있는게 보이고, google.crypto.tink.AesSivKey 라는 문자열도 보인다.

# C:\Users\dhkim>echo "<protobuf bytes>" | xxd -r -p | protoc --decode_raw | python3 -c "import sys, re; print(re.sub(r'(?m)^2: \"(.+?)\"', lambda m: '2: \"' + m.group(1).encode().decode('unicode_escape').encode('latin-1').hex() + '\"', sys.stdin.read()))"

2: "be6b2c3a17a7495347fa2660563a433709b0e8e3d85f96c63ef0cdcd90779170a8b9cb1a91ba0149739438bc7efc136eedca44186859aa1d4a610ce9a4990fc6bc4a1755ac732faf12345cbc0dce82b59aa877ea707393a98b823e7c8b63abf6eece890db407e466f9cf8892d7d99db5659cfc0613020112779f06a632ea7d7ceea59ce7e2e8960c99efd8fa7c29ef3208d4c6237a064d2c6dbf717bf3c455f3669aa4c91f3f9d949b"
3 {
  1: 780783926
  2 {
    1: "type.googleapis.com/google.crypto.tink.AesSivKey"
    2: 1
    3: 780783926
    4: 1
  }
}

이 데이터를 키스토어를 통해 복호화하면 되는데, 분석하면 확인할 수 있는 _app_settings_master_key_ alias로 획득한 keystore 키를 사용한다.

12byte iv와 나머지데이터로 나눠서 "AES/GCM/NoPadding" 복호화하면 된다.

function main() {
    Java.perform(function () {
        var masterKeyAlias = "_app_settings_master_key_";
        var KeyStore = Java.use('java.security.KeyStore');
        var SecretKeyEntry = Java.use('java.security.KeyStore$SecretKeyEntry');
        var Cipher = Java.use('javax.crypto.Cipher');
        var GCMParameterSpec = Java.use('javax.crypto.spec.GCMParameterSpec');

        var ks = KeyStore.getInstance("AndroidKeyStore");
        ks.load(null);
        var entry = ks.getEntry(masterKeyAlias, null);

        // SecretKeyEntry로 캐스팅
        var ske = Java.cast(entry, SecretKeyEntry);
        var secretKey = ske.getSecretKey();

        var keyKeysetHex = "<key_keyset protobuf bytes>"
        console.log("\n[*] Decrypting key keyset...");
        var dec = decryptKeyset(keyKeysetHex, secretKey, Cipher, GCMParameterSpec);
        console.log("[+] Decrypted: " + dec);

        // value keyset도 동일한 방식으로 복호화 
        var valueKeysetHex = "<value_keyset protobuf bytes>"
        console.log("\n[*] Decrypting value keyset...");
        var dec = decryptKeyset(valueKeysetHex, secretKey, Cipher, GCMParameterSpec);
        console.log("[+] Decrypted: " + dec);
    });
}

function decryptKeyset(hex, secretKey, Cipher, GCMParameterSpec) {
    var bytes = hexToBytes(hex);

    // Protobuf: 0x12 field, varint length
    var offset = 1;
    var len = 0, shift = 0;
    while (true) {
        var b = bytes[offset++];
        len |= (b & 0x7f) << shift;
        if ((b & 0x80) === 0) break;
        shift += 7;
    }
    var enc = bytes.slice(offset, offset + len);
    var iv = enc.slice(0, 12);
    var ct = enc.slice(12);

    var cipher = Cipher.getInstance("AES/GCM/NoPadding");
    var ivBytes = Java.array('byte', iv.map(function(x) { return x > 127 ? x - 256 : x; }));
    var spec = GCMParameterSpec.$new(128, ivBytes);

    cipher.init(2, secretKey, spec);

    var ctBytes = Java.array('byte', ct.map(function(x) { return x > 127 ? x - 256 : x; }));
    var dec = cipher.doFinal(ctBytes);

    return javaToHex(dec);
}

function hexToBytes(h) {
    var b = [];
    for (var i = 0; i < h.length; i += 2) {
        b.push(parseInt(h.substr(i, 2), 16));
    }
    return b;
}

function bytesToHex(b) {
    var h = "";
    for (var i = 0; i < b.length; i++) {
        var x = (b[i] & 0xff).toString(16);
        h += (x.length == 1 ? "0" : "") + x;
    }
    return h;
}

function javaToHex(j) {
    var h = "";
    for (var i = 0; i < j.length; i++) {
        var x = (j[i] & 0xff).toString(16);
        h += (x.length == 1 ? "0" : "") + x;
    }
    return h;
}

frida 스크립트를 실행하면 복호화된 keyset들을 확인할 수 있다.

[*] Decrypting key keyset...
[+] Decrypted: 08b6a2a7f4021284010a780a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e4165735369764b6579124212402e4d52f6310b40aad19c687d566d58c7eac84a3a112ee9943bdfd2925be06c3d126de8aa272a1597a495eff3de6d2a4d468abac6d3cf222b98610380deab29f31801100118b6a2a7f4022001

[*] Decrypting value keyset...
[+] Decrypted: 08b291d7d00112640a580a30747970652e676f6f676c65617069732e636f6d2f676f6f676c652e63727970746f2e74696e6b2e41657347636d4b657912221a2085aee182b77d7cdef72d97f525a6fb19b78d631dec64f4e1f2f9dcb7126ee0a21801100118b291d7d0012001

이것도 protobuf로 확인해보면 진짜 AesSivKey, AesGcmKey 를 획득할 수 있다.

# echo "<protobuf bytes>" | xxd -r -p | protoc --decode_raw | python -c "import sys,re; print(re.sub(r'(?s)2: \x22((?:[^\x22\\]|\\.)*)\x22', lambda m: '2: \x22'+m.group(1).encode('utf-8').decode('unicode_escape').encode('latin-1').hex()+'\x22', sys.stdin.read()))"

1: 780783926
2 {
  1 {
    1: "type.googleapis.com/google.crypto.tink.AesSivKey"
    2 {
      2: "2e4d52f6310b40aad19c687d566d58c7eac84a3a112ee9943bdfd2925be06c3d126de8aa272a1597a495eff3de6d2a4d468abac6d3cf222b98610380deab29f3"
    }
    3: 1
  }
  2: 1
  3: 780783926
  4: 1
}

# echo "<protobuf bytes>" | xxd -r -p | protoc --decode_raw | python -c "import sys,re; print(re.sub(r'(?s)3: \x22((?:[^\x22\\]|\\.)*)\x22', lambda m: '3: \x22'+m.group(1).encode('utf-8').decode('unicode_escape').encode('latin-1').hex()+'\x22', sys.stdin.read()))"
1: 437635250
2 {
  1 {
    1: "type.googleapis.com/google.crypto.tink.AesGcmKey"
    2 {
      3: "85aee182b77d7cdef72d97f525a6fb19b78d631dec64f4e1f2f9dcb7126ee0a2"
    }
    3: 1
  }
  2: 1
  3: 437635250
  4: 1
}

SharedPreference 복호화

위에서 얻은 AesSivKey와 AesGcmKey를 사용해서 SharedPreference의 Key와 Value를 복호화할 수 있다.
파일이름이 Siv 과정에서 AAD로 사용되고, base64 상태의 암호화된 Key이름이 Value의 AAD로 사용된다.

from cryptography.hazmat.primitives.ciphers.aead import AESGCM, AESSIV
import base64

# protobuf로 추출된 키
AES_GCM_KEY = bytes.fromhex("85aee182b77d7cdef72d97f525a6fb19b78d631dec64f4e1f2f9dcb7126ee0a2")
AES_SIV_KEY = bytes.fromhex("2e4d52f6310b40aad19c687d566d58c7eac84a3a112ee9943bdfd2925be06c3d126de8aa272a1597a495eff3de6d2a4d468abac6d3cf222b98610380deab29f3")

# SharedPreferences 파일명 (AAD로 사용됨)
FILENAME = "app-preference"

ENCRYPTED_DATA = {
    "AS6J0TZmRI0mynw2HQH563OYBGctIdjmnZPUGWTKqKYZgd7muqx4ID8qNPHNe9sSUxdq7ujpigDeD7rCOFpxjTX+1GelH59SRMagaEgwRcBMJ5htDgAw":
        "ARoVyLIQvPcWG5diVbLro2dBKvEPbVAkfiakxSo1tqM99pNQ3cVyy6D7Xb+AKWAY1LFnYDBsBLSvcImzxL4n1UJfDPadg6FzT7hRT/n+RTuWggON6Q==",
    "AS6J0TZZMzUqhNEtAw6am9GhKwQl7hMjJajPBFTi9GwpBkElnNEjN2Xf+fayVQOa2PaSOR/SgD5Y/97r/+cpcC8Pq94eObqpI1y4P2EDLzjZ3zWb4dUziT9HX+dJ":
        "ARoVyLJJ2pIMFepeGKHyOmAVMuyLGVo5mHgWglvr3T7zg+eSrG9GY4ZP2rYTyvldbZV5L2K7DXewPLzxA9m52WfKt9Z0I62DkxkqHf1xVB16P22n3w==",
    "AS6J0TbJV5j1W/1aelzlmswXqfgcXRHnTiAnWY0p+IRBSkXoukwOgqmMy9BXFnHLMOa5ESJRqLOkV4zqTkgRO8iDwuG7Ryj4ozGMvVzY":
        "ARoVyLKtayyBPaNCC+M38yQdTfKseZL8Ep6+24HNclL2XHF1M7ty79BVhb5QBcp1uWtUAcI5nhQluVWD3XQMrPQpZzN1d/Hise+2pnfgW5cGPTyKqOyDLZr63oj78oBcBJF0ZuryAmlKCaKfmHLhLIOxVrMDYkoBrCdkAR2Vu1n4tO11g/3+JaC7+ckIXPVilcvWP39p8vhXSlRay6M07tWDxx8Dus6T81gpU3BDmFr7cNvcvELi09fihn70FJMc5jXuzfs6PKtPj1nBbnAwjydRwfJiYAw3yEIF0Ra87y3eB093EN91kmDl7a1Ti0leMSwN9h7OK7YNxCZxYzTqFFqaxc/eDuXrAPM4P8JYKtIODC954tUaUC8iV6a7SIdG5MVl22nztlbPrZVp3w3s0xy4AcAIaKpZ7ZMVbUfwFtavaX0QSMZJQV/zlRyO5wXxpTw6RlI7DrE1js1NUPJy3FfV1QNQObpUVCECFCjkBer+q9QsSPxl+gTU6xkEvZN5x4Np6ZIuvTgtoCWUrLclOHLdWFlksWnO9jrXDzgtMH84DZaIpwXyjhcsjylCw4qzXEH4xmzHKGqT/sTSGyR/Hy9PDbkJctwKxXDJL7Kzsd+F8jD2Yp2McjaI6uWhlLj31KrYPsigEXk5JYR0qi+uo+nxmSnHtRYnVvilZusbZ6BpDp0zIRe5hX/fgU3Ie+u+4rfxP1tvytB02yv6ksF6W0GLBXYdz4cm9SpdQ16V1f4YxX/le+32Eg3Enj67OjCdVmP4IpkPpetgAV0mJnYr2fOFKN2hCNgjd+cB71UxWq5evL+sMT+vKQ==",
    "AS6J0Ta7FEeuT7T7BrnVlZyv+kOLMXTf9XvsoySJ8mCpwpU16U6Hn2kfFGJAhHWcJDpxIiFi0zUDHCM4eQ96LFxHY5O5p9fK3KV38AZj7SwMUSnJmVacvKM=":
        "ARoVyLJba16KMD4GzkO1/3U/EnKpG4SN4yBHYMgZdgAtWOBgblF+ahyXrbIorKvz6sFFT/XGx/ADzywuK6PXH6ZXo7eRQ8o8mNmWnlQApj2rLspnaQ==",
    "AS6J0Ta4kPae7u49+4nhe+/T/7ugx2Q2pucOpNh9j61FFfetod5ZkKK+sE7VaYo=":
        "ARoVyLKxAXrBu97lW1Hb6yy8gU0RZXPOpfLq42D28WtWue9EiegcheX5D7J5iVRJ0iCEJc/R3zYMJXVrbup6q7MSAes3gazQx0tcfUxfSjoUYc/I7Q==",
    "AS6J0TbJ9vuRU6g+2dcED6Zj10aBB+u8SJN698XQceqHlZgQGpwe":
        "ARoVyLJk/s1crmNwbTJwWiaZpe5iUn0UH565w+B9EgiTa3iaHRhWiuQv5z6X4tSOJ6wD4n84sTPpyHnavzgWF5mRbh/FwyFwgAhLxpImxLiS77Z5yyfUUxgIyzwRMXilE+nYbyJt0WcTB4Ojxj7/ZyJdalNe/mv6lLDiqWr7kkAqpTAb08nEJQ089U+pI9NDhQCSi+BcxhEYOF9yoPPtbNHDxDHpbF0aJbqFqfAWXqpMhcJ9ku4mrtjHXnIu06rDQlPMPAtbb1AepEpRG4s666cM0HOElZ6N8yBfdAB6bTb4zAsgWQ4Gb9XyKp9KL+gQprdE4Q==",
}


def decrypt_aes_gcm(encrypted_b64: str, key: bytes, aad: bytes = None) -> bytes:
    """
    Tink AES-GCM 포맷 복호화
    [1 byte version][4 bytes key ID][12 bytes IV][ciphertext + 16 bytes tag]
    """
    encrypted = base64.b64decode(encrypted_b64)

    version = encrypted[0]
    key_id = encrypted[1:5]
    iv = encrypted[5:17]
    ciphertext = encrypted[17:]

    aesgcm = AESGCM(key)
    plaintext = aesgcm.decrypt(iv, ciphertext, aad)

    return plaintext


def decrypt_aes_siv(encrypted_b64: str, key: bytes, aad: bytes) -> bytes:
    """
    Tink AES-SIV 포맷 복호화 (Deterministic AEAD)
    [1 byte version][4 bytes key ID][16 bytes SIV][ciphertext]
    """
    encrypted = base64.b64decode(encrypted_b64)

    version = encrypted[0]
    key_id = encrypted[1:5]
    ciphertext = encrypted[5:]  # SIV + actual ciphertext

    aessiv = AESSIV(key)
    # AAD는 파일명 바이트
    plaintext = aessiv.decrypt(ciphertext, [aad])

    return plaintext


def main():
    filename_bytes = FILENAME.encode('utf-8')

    for enc_key_b64, enc_value_b64 in ENCRYPTED_DATA.items():
        print("-" * 60)

        key_name = decrypt_aes_siv(enc_key_b64, AES_SIV_KEY, filename_bytes)
        key_name_str = key_name.decode('utf-8')

        print(f"Key: {key_name_str}")

        aad = enc_key_b64.encode('utf-8')
        value = decrypt_aes_gcm(enc_value_b64, AES_GCM_KEY, aad)

        try:
            value_str = value.decode('utf-8')
            print(f"Value: {value_str}")
        except:
            print(f"Value (hex): {value.hex()}")
        print()


if __name__ == "__main__":
    main()

복호화 결과

SharedPreference의 평문을 확인할 수 있다.

python .\decrypt_app-pref.py
------------------------------------------------------------
Key: user_db_secret_alias_d63cfa9c-1838-4007-9eb2-d919f2ba38f6@wire.com
Value: ,mLSZI2XXIRkuDviEJAkZ1z5DxvmtOKXUYeWdeVKu6E8=

------------------------------------------------------------
Key: proteus_db_secret_alias_v2_d63cfa9c-1838-4007-9eb2-d919f2ba38f6@wire.com
Value: ,efOK484OUbXFdupuFX/Z4hWCTIEegVN2xVkMZgvmkCU=

------------------------------------------------------------
Key: user_tokens_d63cfa9c-1838-4007-9eb2-d919f2ba38f6@wire.com
Value: ?{"user_id":{"value":"d63cfa9c-1838-4007-9eb2-d919f2ba38f6","domain":"wire.com"},"access_token":"NVxYvBuMSWkJfqvzhrOefXSxoGOpGsV5wvuREhW6Q42UXovVOH1l4q2lJpXEqE9bQOvqUzxEZI4P3uv04QHyAg==.v=1.k=1.d=1768361874.t=a.l=.u=d63cfa9c-1838-4007-9eb2-d919f2ba38f6.i=5d0345c6962a1c50.c=10313408593721778635","refresh_token":"hnPCDeZh1wFVf6d1JRmBiuaXwXpnephF55JpiHMvu_bGVeH_Xv1LqSVoZi9tHIZrPvzZn4ZKzg-QF2eBhATxAA==.v=1.k=1.d=1773199374.t=u.l=.u=d63cfa9c-1838-4007-9eb2-d919f2ba38f6.r=ad009cd0.i=5d0345c6962a1c50","token_type":"Bearer","cookie_label":"574c0d32-977c-4dd5-9e18-792f8c7c1893"}

------------------------------------------------------------
Key: mls_db_secret_alias_v2_d63cfa9c-1838-4007-9eb2-d919f2ba38f6@wire.com
Value: ,rtw8tIcRuBqZMkqwhIA5o/ticMkv5gBKxx9aiXwEA9k=

------------------------------------------------------------
Key: global_db_passphrase_alias
Value: ,4i88UzKl7XXl4HBPub8isF6JFze6kP/bORPZw/HByEc=

------------------------------------------------------------
Key: notification_token
Value (hex): 00000000000000cb7b22746f6b656e223a22646e6b70496b6f77534e572d77654b65674d524e30683a415041393162477a38514b4f6433414f7873375f4a36583842395f4a315443504a684949674b48584239514270686842554f4351696761324c627249565a616d794a61583335674f56674f34386c4b376a727a3545556f5676516c55526232657376754159784756495952795162585848465f43744549222c227472616e73706f7274223a2247434d222c226170706c69636174696f6e4964223a22373832303738323136323037227d

EncryptedFile

EncryptedSharedPreference 는 key-value 쌍이 저장되는 SharedPreference 파일에서 key value를 각각의 키셋으로 암호화하는 방식이였지만, EncryptedFile은 파일을 암호화하는 keyset을 google.crypto.tink 방식으로 암호화했기 때문에 keyset의 복호화 방식은 동일하다.

Comments

ESC
Type to search...