안드로이드 키스토어 RSA 암호화 서버 cross-platform, Android - Python

     

 

들어가기 전에 

멘토를 하면서 RSA(공개키-개인키)를 사용한 서버와 클라이언트간에 암복호화를 하는 프로그램을 만들게 되었다. 개인키와 공개키에 대해서는 이미 충분히 알고있기 때문에 쉽게 만들줄 알았는데, 기존 라이브러리를 사용하자니 서버쪽과 클라이언트간에 데이터가 어긋나는 경우가 발생했다. 서버는 파이썬을 이용한 dJango, 클라이언트는 안드로이드였는데, 안드로이드에서 제공하는 KeyStore를 이용해 공개키와 개인키를 생성하고, 개인키는 디바이스에 저장, 공개키는 서버로 전송하여, 서버에서 특정 데이터를 암호화 하고 안드로이드에서 개인키로 복호화하는 개념이다. 즉, 개인키를 저장하고있는 디바이스만 복호화할 수 있는 데이터 통신 프로그램을 만드는게 목표였다. 

안드로이드에서 RSA키를 생성하는 것은 매우 간단하고 샘플도 많다. 실제로 구현도 해보았고, 안드로이드에서 암호화해서 복호화 하는것까지 테스트를 했다. 파이썬도 마찬가지다. 상당히 많은 패키지가 있어서 골라서 해도 될정도로 쉽게 RSA를 구현했다. 

그런데 문제는 양쪽에서 만든 키와 데이터가 어긋난다는 것. 즉 안드로이드에서 키를 서버로 전달해서 서버에서 암호화를 해서 가져왔는데, 안드로이드에서 복호화가 안되는 것이다. 블럭사이즈 에러가 자꾸 떴다.

정말 여기저기 구글링 해봤지만 레퍼런스는 나오지 않고, 영어로도 검색을 많이 해봤는데 나오지 않았다. 그나마 cross-platfrom encrypt라고 치니까 AES방식의 암호화는 나왔다. 하지만 대칭키를 쓰면 생각한 서비스가 안나오기 때문에 그냥 참고하는 수준에 그치고, 서버, 클라이언트 관련 정보를 따로 따로 찾아서 겨우 성공했다.

- 일단 소스는 github에 올려놨으니 다운받아서 보기를 권장합니다.
https://github.com/tkdlek11112/cross-platform-RSA

안드로이드 

안드로이드는 공개키와 개인키를 생성하는 곳이다. 안드로이드에서 제공하는 KeyStore를 이용해 간단하게 공개키를 만들 수 있다.

        try {
            keyPairGenerator = KeyPairGenerator.getInstance(
                    KeyProperties.KEY_ALGORITHM_RSA, "AndroidKeyStore");
            keyPairGenerator.initialize(
                    new KeyGenParameterSpec.Builder(
                            "key1",
                            KeyProperties.PURPOSE_DECRYPT | KeyProperties.PURPOSE_ENCRYPT)
                            .setBlockModes(KeyProperties.BLOCK_MODE_CBC)
                            .setAlgorithmParameterSpec(new RSAKeyGenParameterSpec(2048, RSAKeyGenParameterSpec.F4))
                            .setDigests(KeyProperties.DIGEST_SHA256 , KeyProperties.DIGEST_SHA512)
                            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_RSA_PKCS1)
                            .build());
            kp = keyPairGenerator.generateKeyPair();
        }
        catch (Exception e){
            Log.e(TAG, ""+e);
        }​

풀 코드는 github을 참고하시고, 가장 중요한 부분인 키 생성 부분은 createKey 함수를 보면 된다. 그냥 눈으로 쭉 읽으면 그런가부다~ 하고 넘어갈 정도다. 여기서 중요한건 setEncryptionPaddings 부분이다. RSA는 블럭단위로 암호화를 하게 되는데 만약 평문이 블록사이즈와 일치 하지 않으면 블록사이즈만큼 패딩을 해줘야한다. 하지만 이 패딩방식도 여러가지라서 안드로이드에서 키를 생성하고 복호화할 때 사용하는 패딩방식과 서버에서 암호화 하는 패딩방식을 일치시켜야 정상적으로 동작한다. 이부분이 제일 중요하다.

 

다음은 암호화 및 복호화를 하는 코드를 봐보자.

    private static final String CIPHER_ALGORITHM = "RSA/ECB/PKCS1Padding";

	public byte[] encrypt(final String text) throws NoSuchAlgorithmException, NoSuchPaddingException,
            InvalidKeyException, IllegalBlockSizeException, BadPaddingException, KeyStoreException, CertificateException, IOException {
        Log.d(TAG, "encrypt test" + text+"");
        KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
        keyStore.load(null);
        PublicKey publicKey = keyStore.getCertificate("key1").getPublicKey();
        Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);



        Log.d(TAG,"publicKey1"+publicKey);
        String pubkey3 = publicKey.getEncoded().toString();
        Log.d(TAG,"publicKey3"+ pubkey3);
        Log.d(TAG,"publicKey2"+Base64.encodeToString(publicKey.getEncoded(), Base64.DEFAULT));
        cipher.init(Cipher.ENCRYPT_MODE,publicKey);

        byte[] publicKeyBytes = Base64.encode(publicKey.getEncoded(),0);
        String pubKey = new String(publicKeyBytes);
        Log.d(TAG,"publicKey4444"+ pubkey3);

        byte[] encryptedBytes = cipher.doFinal(text.getBytes("utf-8"));
        Log.d(TAG, "encry"+encryptedBytes.toString());
        return encryptedBytes;
    }

    public String decrypt(final byte[] encryptedText){
        try{
            KeyStore keyStore = KeyStore.getInstance("AndroidKeyStore");
            keyStore.load(null);
            PrivateKey privateKey = (PrivateKey) keyStore.getKey("key1", null);
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            cipher.init(Cipher.DECRYPT_MODE,privateKey);
            byte[] decryptedText = cipher.doFinal(encryptedText);
            Log.d(TAG, "end decrypt" + decryptedText.toString());
            return new String(decryptedText);
        }catch (UnrecoverableKeyException | IOException | KeyStoreException | CertificateException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException | BadPaddingException | IllegalBlockSizeException e){
            Log.e("decrypt catch", e.getMessage()+"");
            String text = new String(encryptedText);
            return text;
        }

    }

사실 암호화는 필요없다. 서버에서 암호화하고 안드로이드는 복호화만 하기 때문이다. 하지만 안드로이드 자체에서 암호화와 복호화가 잘 되는지 테스트 하기위해 만들었다. 여기서 중요한것은 Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM) 이부분이다.

위에 CIPHER_ALGORITHM 선언문을 보면 "RSA/ECB/PKCS1Padding"으로 되어있다. 어떤 암복호화방식을 사용하는지 알고리즘을 적는건데, 사실 가운데 ECB는 상관이 없다. RSA는 두번째 인자값을 안쓴다. "RSA/NONE/PKCS1Padding"이랑 똑같다는 소리다. 중요한건 3번째 값인 PKCS1Padding이다. 위에서 키를 만들때 선언한 것과 같은값이어야 하며 서버와도 같은값이어야한다.

이제 앱을 실행시키고 Create와 Test를 한번씩 눌러보자. Create버튼을 누르면 키가 생성된다. 주의할점은 생성할때마다 새로운 키가 생성되기 때문에 서버랑 테스트할라면 한번만 눌러야한다. 앱이 새로 설치될경우 키가 초기화되니 주의해야한다. (이것때문에 엄청 해맴...)

디버깅 로그를 보면 publickey가 찍힌다

로그를 보면 위와같이 publickey가 찍힌 부분을 볼 수 있다. 이 값을 서버에 넘겨주어야 한다.

 

파이썬

파이썬은.. 정말 여러개의 패키지를 돌아다니면서 해봤다. 일단 가장 많이 쓰이는건 pycrypto였는데, 가장 많이 쓰임에도 불구하고 잘 되지 않았다. PKCS1Padding을 못쓰는거 같기도 하고 해서 M2Crypto라는 패키지로 실행하니 잘 됬다.

import M2Crypto
import base64
​
# pubkey from client
pubkey_fromdb = 'MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAp3AkkjJl7RLYfaFZr5rgiuOo9fHp3PEJ\nLXBzN5azKC7oMrxE6PkOXJxe8zbsjlngM+v/reqNpRnJehzeGZ7ZiaWgfiNU6qaM9AMT1CMIdC2x\nlexod0HSz/XsyBcC8Pcoj6Oay9r+iJljRoiv2X1ErkbtIVqurDta8osADLAxoiBcEugr05o819by\nWpKwOaVW4HrqKfmQcyKcL3H4ZSIT1UXJtt8LMi1dxwB3QwjFLBli4r/TpVV0G0hRPBMvG4merFiH\nyLGikBoiKZgY7IoXkoATdLZDWKGCqukMZ8KlrJf2MjQA73rziPKL753bzTCW0JzMaDVdJx9yYLKU\ntA9o4QIDAQAB'
​
# save to .pem
pubkey_pem = '-----BEGIN PUBLIC KEY-----\n' + pubkey_fromdb + '\n-----END PUBLIC KEY-----'
print(pubkey_pem)
f = open("pub_key.pem", 'w')
f.write(pubkey_pem)
f.close()
​
public_key = M2Crypto.RSA.load_pub_key('pub_key.pem')
​
​# plain text
data = "여기에 암호화할 문장"
data_input = base64.b64encode(data.encode('utf-8'))
ciphertext = public_key.public_encrypt(data.encode('utf-8'), M2Crypto.RSA.pkcs1_padding)
encrypted_message = str(base64.b64encode(ciphertext), 'utf8')

# send to client encrypted_message
print("클라이언트로 보내줄 문장  = " + encrypted_message)

 

pubkey_fromdb는 안드로이드에서 생성한 pubkey를 넣으면 된다. fromdb라고 한건 서버에 db에 저장했다가 꺼내쓰니까. M2Crypto에서 공개키를 읽으려면 load_pub_key를 하면 되는데, 패키지를 까보면 string으로도 읽을 수 있는게 있는데 버그인지 개발자가 빼먹은건지 정상적으로 작동하지 않는다. load_pub_key라는 파일로 읽는 함수만 작동한다. 그래서 어쩔수 없이 클라이언트에서 올라온 키를 .pem으로 만들어줬다. .pem이라고 뭐 있는건 아니고 앞뒤에 ----BEGIN PUBLIC KEY-----랑 -----END PUBLIC KEY-----만 넣어주면 된다. 

키 파일을 생성하고 M2Crypto.RSA.load_pub_key로 읽은다음 암호화하면 끝~ 참고로 데이터의 흐름은 평문 -> B64인코딩 -> 암호화 -> 복호화 -> B64디코딩 -> 평문 이다.

이제 서버에서 암호화한 문장인 encrypted_message를 안드로이드에 복호화 텍스트에 넣고 돌려보면 정상적으로 암/복호화가 일어나는것을 볼 수 있다. 

 

마치며

 

진짜 별거 아니라고 생각했는데 환경이 다른상태에서 키를 주고받는게 생각보다 어려웠다. 실제로 키를 만들고나서 바이트형식이녜 베이스64형식이녜 스트링이녜 헷갈려서 이게 정삭적인 키인지 아닌지도 헷갈렸다. 특히 서버와 클라이언트 개발자가 따로따로 있다면 이 혼란은 더 가중될 것이다. 어느쪽에서 인코딩이 틀렸는지, 데이터가 틀렸는지 둘이 맞춰봐야 알 수 있기 때문이다. 이기종(cross-platform)간에 암호화 통신을 구현할때는 암호화 방법을 확실히 알고 들어가는게 정신건강에 좋을 것 같다.

 

 

 

 

 

 

반응형

댓글

Designed by JB FACTORY