Before 2022/Android

안드로이드 KeyStore 비대칭(공개키, RSA) 암호화

Eljoe 2018. 12. 28. 09:37
/* @since 2018. 12. 21 */
/* Data암호화 로직 반드시 중요한 데이터만 암호화 시킬 것(성능 저하 발생) */
public class RSACryptor {
    private static final String TAG = "RSACryptor";
    private KeyStore.Entry keyEntry; // 비대칭 암호화(공개키) 알고리즘 호출 상수
    private static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";

    /*Singleton Pattern * Call ->RSACryptor.getInstance().method()*/
    private RSACryptor() {}

    private static class RSACryptorHolder {
        static final RSACryptor INSTANCE = new RSACryptor();
    }

    public static RSACryptor getInstance() {
        return RSACryptorHolder.INSTANCE;
    }

    // Android KeyStore 시스템에서는 암호화 키를 컨테이너(시스템만이 접근 가능한 곳)에 저장해야하므로 이 키를 기기에서 추출해내기가 더 어려움
    public void init(Context context) {
        try { // AndroidKeyStore 정확하게 기입
            KeyStore ks = KeyStore.getInstance("AndroidKeyStore");
            ks.load(null);
            // KeyStore에 해당 패키지 네임이 등록되어있는가?
            if (
                !ks.containsAlias(context.getPackageName())
            ) {
                if (
                    Build.VERSION.SDK_INT >= Build.VERSION_CODES.M
                ) {
                    initAndroidM(context.getPackageName());
                } else {
                    initAndroidK(context);
                }
            }
            keyEntry = ks.getEntry(
                context.getPackageName(), null
            );
        } catch (KeyStoreException | IOException |
            NoSuchAlgorithmException |
            CertificateException |
            UnrecoverableEntryException e) {
            Log.e(TAG, "Initialize fail", e);
        }
    }

    // API Level 23 이상(마쉬멜로우) 개인키 생성
    private void initAndroidM(String alias) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
                // AndroidKeyStore 정확하게 기입
                KeyPairGenerator kpg = KeyPairGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore"
                );
                kpg.initialize(
                    new KeyGenParameterSpec
                    .Builder(
                        alias,
                        KeyProperties.PURPOSE_ENCRYPT |
                        KeyProperties.PURPOSE_DECRYPT
                    )
                    .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(
                        2048, RSAKeyGenParameterSpec.F4
                    ))
                    .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                    .setEncryptionPaddings(
                        KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1
                    )
                    .setDigests(
                        KeyProperties.DIGEST_SHA512, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA256
                    )
                    .setUserAuthenticationRequired(false)
                    .build()
                );
                kpg.generateKeyPair();
                Log.d(TAG, "RSA Initialize");
            }
        } catch (GeneralSecurityException e) {
            Log.e(
                TAG, "이 디바이스는 관련 알고리즘을 지원하지 않음.", e
            );
        }
    }

    // API Level 19 이상(킷캣) 개인키 생성
    private void initAndroidK(Context context) {
        try {
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { 
            	// 유효성 기간
                Calendar start = Calendar.getInstance();
                
                Calendar end = Calendar.getInstance();
                end.add(Calendar.YEAR, 25);
                
                // AndroidKeyStore 정확하게 기입
                KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA", "AndroidKeyStore");
                kpg.initialize(
                    new KeyPairGeneratorSpec
                    .Builder(context)
                    .setKeySize(2048)
                    .setAlias(context.getPackageName())
                    .setSubject(
                        new X500Principal("CN=myKey")
                    )
                    .setSerialNumber(BigInteger.ONE)
                    .setStartDate(start.getTime())
                    .setEndDate(end.getTime())
                    .build()
                );
                kpg.generateKeyPair();
                
                Log.d(TAG, "RSA Initialize");
            }
        } catch (GeneralSecurityException e) {
            Log.e(
                TAG, "이 디바이스는 관련 알고리즘을 지원하지 않음.", e
            );
        }
    }

    // 문자열 위주로 작업하기 때문에 반드시 String형이나
    // toString을 쓸 것!! 암호화(set)
    public String encrypt(String plain) {
        try {
            byte[] bytes = plain.getBytes("UTF-8");
            
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            
            // Public Key로 암호화
            cipher.init(
                Cipher.ENCRYPT_MODE, (
                    (KeyStore.PrivateKeyEntry) keyEntry
                ).getCertificate().getPublicKey()
            );
            
            byte[] encryptedBytes = cipher.doFinal(bytes);
            
            Log.d(
                TAG, "Encrypted Text : " + new String(Base64.encode(
                    encryptedBytes, Base64.DEFAULT
                ))
            );
            
            return new String(Base64.encode(
                encryptedBytes, Base64.DEFAULT
            ));
        } catch (UnsupportedEncodingException |
            NoSuchAlgorithmException |
            NoSuchPaddingException |
            InvalidKeyException |
            IllegalBlockSizeException |
            BadPaddingException e) {
            Log.e(TAG, "Encrypt fail", e);
            return plain;
        }
    }

    // 복호화(get) 데이터가 유출되더라도 복호화는 이 로직에서만 가능함
    public String decrypt(String encryptedText) {
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            
            // Private Key로 복호화
            cipher.init(
                Cipher.DECRYPT_MODE, (
                    (KeyStore.PrivateKeyEntry) keyEntry
                ).getPrivateKey()
            );
            
            byte[] base64Bytes = encryptedText.getBytes("UTF-8");
            
            byte[] decryptedBytes = Base64.decode(base64Bytes, Base64.DEFAULT);
            
            Log.d(
                TAG, "Decrypted Text : " + new String(
                    cipher.doFinal(decryptedBytes)
                )
            );
            
            return new String(
                cipher.doFinal(decryptedBytes)
            );
        } catch (NoSuchAlgorithmException |
            NoSuchPaddingException |
            InvalidKeyException |
            UnsupportedEncodingException |
            BadPaddingException |
            IllegalBlockSizeException e) {
            Log.e(TAG, "Decrypt fail", e);
            return encryptedText;
        }
    }
}