Things take time

[Android] SharedPreference 암호화? 안드로이드 KeyStore에 대한 설명 본문

Android(기능)

[Android] SharedPreference 암호화? 안드로이드 KeyStore에 대한 설명

겸손할 겸 2019. 6. 28. 17:27

SharedPreference 암호화?

SharedPreference에 들어가는 데이터는 xml형태로 저장되며, 접근이 가능하다.

그러므로 디컴파일을 통해 접근이 가능하단 뜻이다. 크게 중요하지 않은 데이터를 저장했다면 의미 없겠지만, 중요한 데이터가 들어있다면 암호화를 하자는 것이다.

 

다만 일반적인 암호화방법으로 해도, 암/복호화에 사용되는 키가 노출되기 때문에 이 키를 저장하는 곳을 안전한 곳! KeyStore에 넣자는 것이다.

참고사이트

https://medium.com/hexlant/android-keystore-%EB%B3%B4%EC%95%88-fe8e0c5de359 : 한글

https://hyperconnect.github.io/2018/06/03/android-secure-sharedpref-howto.html : 한글

https://medium.com/@josiassena/using-the-android-keystore-system-to-store-sensitive-information-3a56175a454b : 이해 도움

https://gist.github.com/JosiasSena/3bf4ca59777f7dedcaf41a495d96d984 :위 사이트 연결 소스(AES)

https://academy.realm.io/posts/secure-storage-in-android-san-francisco-android-meetup-2017-najafzadeh/ : RSA

 

암호화에 대해 너무 모르고있었던 터라, 지금보니 쉬운 개념들을 이해하는데 오래걸렸다. 레퍼런스도 참 많이 뒤져봤기때문에 링크들을 먼저 소개한다.

 

데이터를 암호화할 때 알고리즘인 AES, RSA 등 중에서 선택하면 되는 것이고

실제 소스를 돌려본 결과 둘 다 잘 동작한다. 다만 AES는 버전을 타서(마시멜로 이상) 예외처리가 필요하고 RSA는 문제 없었다. 

AES & RSA

사실 암호화에 대해 초보자인 편이기 때문에, 설명을 깊게할 순 없다. 학교에서도 배웠는데 잊어버림

이 기능을 넣으면서 필요한 개념이었던 키에 대해서만 설명한다.

두 기법의 공통점은 데이터를 암호화, 복호화할 때 키를 사용한다는 점이며, 차이점은 암호화 복호화키를 공통된 키를 사용하느냐(AES), 따로 사용하느냐(RSA)의 차이로만 이해한다.

 

그래서 AES는 비밀키 라는 공통된 키를, RSA는 공개키(암호화 용), 개인키(복호화 용)쌍을 사용한다.

그러므로 이 각 기법에 따른 키들을 기존에는 소스코드에 노출되었기 때문에, 디컴파일시 위협이 되었지만 이 키들을 KeyStore라는 클래스를 이용해 접근할 수 없는 곳에 넣는 것이다.

 

그리고 이 키들을 생성하는 클래스 또한 AES는 KeyGenerator, RSA는 KeyPairGenerator다.

키가 하나쓰는 알고리즘과 두개 쓰는 알고리즘이기 때문!

 

그러므로 KeyStore는 말 그대로 키를 저장하고, 코드로 접근할 수 있는 영역이다.

아래의 해석본은 AES로 작성된 소스의 분석본이다. 문법 설명

1. 새로운 키를 만들자
- 암호화를 할때 사용할, 별칭으로 쓸 것을 먼저 정해야 함(String 값)
- 이 별칭은 안드로이드 키스토어 내에서 보여질 키
- KeyGenerator 클래스를 사용하여 생성한다.

AES 암호화를 사용하여, AndroidKeyStore안에 저장되므로 문법처럼 사용하자.

final KeyGenerator keyGenerator = KeyGenerator.getInstance(KeyProperties.KEY_ALGORITHM_AES, "AndroidKeyStore"); 

KeyGenerator 인스턴스를 만들면  KeyGenerators의 생성자로 전달하기 위해, KeyGenParameterSpec.Builder를 사용하여 KeyGenParameterSpec 객체를 만들어야한다.
KeyGenParameterSpec는 생성할 키에 대한 속성으로써 정의 된다.(언제 이 키가 만료될 것인지 등)

final KeyGenParameterSpec keyGenParameterSpec = new KeyGenParameterSpec.Builder(alias, 
        KeyProperties.PURPOSE_ENCRYPT | KeyProperties.PURPOSE_DECRYPT) 
        .setBlockModes(KeyProperties.BLOCK_MODE_GCM) 
       .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE) 
        .build(); 

첫 번째 인자는 우리가 사용할 별칭 명(String, alias)을 전달하고
두 번째 인자는 이 키를 사용할 목적(암호, 복호화)을 명시하고
세 번재 인자는 BlockMode는 암, 복호화할 데이터에 사용될 block mode를 명세함(https://developer.android.com/reference/android/security/keystore/KeyProperties.html)
네 번째 인자는 변환 알고리즘을 명세한다.AES/GCM/NoPadding를 사용하기 때문에 (KeyProperties.ENCRYPTION_PADDING_NONE)

2. 암호화하기

keyGenerator.init(keyGenParameterSpec); 
final SecretKey secretKey = keyGenerator.generateKey(); 

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 
cipher.init(Cipher.ENCRYPT_MODE, secretKey); 

keyGenParameterSpec를 이용하여 keyGenerator를 초기화 하고, Secret Key(비밀키)를 만들자.
Cipher 객체(실제 암호화를 처리함)를 초기화 할때 이 비밀키를 사용한다. 이 비밀키를 갖고 Cipher객체는 암호화에 대한 작업을 수행한다.

iv = cipher.getIV(); 
encryption = cipher.doFinal(textToEncrypt.getBytes("UTF-8")); 


IV = intinitialization vector 라고하며, 이 것은 복호화할 때나 암호화를 마무리할 때 사용한다.

3. 복호화하기
Keystore 인스턴스를 생성한다. 

keyStore = KeyStore.getInstance("AndroidKeyStore"); 
keyStore.load(null); 


keyStore 객체는 암호화할때 사용했던 별칭으로 만들었던 비밀키를 얻어올 때 사용한다.

final KeyStore.SecretKeyEntry secretKeyEntry = (KeyStore.SecretKeyEntry) keyStore 
        .getEntry(alias, null); 

final SecretKey secretKey = secretKeyEntry.getSecretKey(); 

KeyStore의 SecretKeyEntry는 비밀키를 얻어올때 실제 사용한다.

이전에 BLOCK_MODE_GCM blog mode로  GCMParameterSpec(128, 120.. 등의 인증 태그길이를 가진)애를 필요로하며, 이전에 암호화할 때 사용한 IV값을 전달해야 복호화가 이루어진다

final Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding"); 
final GCMParameterSpec spec = new GCMParameterSpec(128, encryptionIv); 
cipher.init(Cipher.DECRYPT_MODE, secretKey, spec); 

복호화방법 

final byte[] decodedData = cipher.doFinal(encryptedData);

final byte[] decodedData = cipher.doFinal(encryptedData); 


암호화되지 않는 데이터 가져오기

final String unencryptedString = new String(decodedData, "UTF-8");

final String unencryptedString = new String(decodedData, "UTF-8"); 

** KeyStore에 저장된 별칭들 얻어오기

private ArrayList getAllAliasesInTheKeystore() throws KeyStoreException { 
    return Collections.list(keyStore.aliases()); 
}

 

아래 소스코드는 RSA용 소스이다.

public class OKCipher {

    private static final String ANDROID_KEY_STORE = "AndroidKeyStore";
    private static final String ALIAS = "패키지명";

    private static KeyStore.Entry createKeys(Context context) throws NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException, NoSuchProviderException, InvalidAlgorithmParameterException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        keyStore.load(null);
        boolean containsAlias = keyStore.containsAlias(ALIAS);

        if (!containsAlias) {
            KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
            Calendar start = Calendar.getInstance(Locale.ENGLISH);
            Calendar end = Calendar.getInstance(Locale.ENGLISH);
            end.add(Calendar.YEAR, 1);
            KeyPairGeneratorSpec spec = new KeyPairGeneratorSpec.Builder(context)
                    .setAlias(ALIAS)
                    .setSubject(new X500Principal("CN=" + ALIAS))
                    .setSerialNumber(BigInteger.ONE)
                    .setStartDate(start.getTime())
                    .setEndDate(end.getTime())
                    .build();
            kpg.initialize(spec);
            kpg.generateKeyPair();
        }

        return keyStore.getEntry(ALIAS, null);
    }
    private static byte[] encryptUsingKey(PublicKey publicKey, byte[] bytes)  throws NoSuchAlgorithmException,  NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException {
        Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        inCipher.init(Cipher.ENCRYPT_MODE, publicKey);
        return inCipher.doFinal(bytes);
    }

    private static byte[] decryptUsingKey(PrivateKey privateKey, byte[] bytes) throws NoSuchAlgorithmException,  NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException{
        Cipher inCipher = Cipher.getInstance("RSA/ECB/PKCS1Padding");
        inCipher.init(Cipher.DECRYPT_MODE, privateKey);
        return inCipher.doFinal(bytes);
    }

    public static String encrypt(Context context, String plainText) throws NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, NoSuchAlgorithmException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException, NoSuchProviderException, InvalidAlgorithmParameterException  {
        KeyStore.Entry entry = createKeys(context);
        if (entry instanceof KeyStore.PrivateKeyEntry) {
            Certificate certificate = ((KeyStore.PrivateKeyEntry) entry).getCertificate();
            PublicKey publicKey = certificate.getPublicKey();
            byte[] bytes = plainText.getBytes("UTF-8");
            byte[] encryptedBytes = encryptUsingKey(publicKey, bytes);
            byte[] base64encryptedBytes = Base64.encode(encryptedBytes, Base64.DEFAULT);
            return new String(base64encryptedBytes);
        }
        return null;
    }
    public static String decrypt(String cipherText) throws NoSuchAlgorithmException,  NoSuchPaddingException, InvalidKeyException, BadPaddingException, IllegalBlockSizeException, KeyStoreException, CertificateException, IOException, UnrecoverableEntryException {
        KeyStore keyStore = KeyStore.getInstance(ANDROID_KEY_STORE);
        keyStore.load(null);

        KeyStore.Entry entry = keyStore.getEntry(ALIAS, null);
        if (entry instanceof KeyStore.PrivateKeyEntry) {
            PrivateKey privateKey = ((KeyStore.PrivateKeyEntry) entry).getPrivateKey();
            byte[] bytes = cipherText.getBytes("UTF-8");
            byte[] base64encryptedBytes = Base64.decode(bytes, Base64.DEFAULT);
            byte[] decryptedBytes = decryptUsingKey(privateKey, base64encryptedBytes);
            return new String(decryptedBytes);
        }
        return null;
    }
}

AES가 마시멜로 이상부터 적용되기도 하고, 간편해보여서 이 소스를 쓰긴 했는데 사실 위 부분의 소스 해석한 것처럼 진행했기에, 딱히 이해가 어렵진 않았다.

 

암호화나 복호화 함수를 호출하고, 각 함수는 공개, 개인키를 생성하거나 있을 경우 기존 키를 리턴해준다. 이 때 각 키는 KeyStore를 사용하므로, 외부에서 접근할 수 없는 영역에 존재한다.

 

그리고 이후 작업은 일반 암호화 작업과 같다.

 

사용방법도 간단하다.

        SharedPreferences sharedPreferences = getSharedPreferences("test", MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPreferences.edit();
        try {
            editor.putString("key", OKCipher.encrypt(getApplicationContext(), "data"));
        }catch (Exception e){
            Log.e("encrypt error", e.toString());
        }
        editor.commit();

        try {
            String encString = sharedPreferences.getString("key", "");
            String decString = OKCipher.decrypt(encString);
        }catch (Exception e){
            Log.e("decrypt error", e.toString());
        }

이런식으로 사용하면 되겠다!

 

처음에 말한 것처럼 지식이 얕으므로 틀린점이 있다면 알려주시길 (__)