Android/Cipher

[Android] Cipher를 사용하여 암호화 복호화 해보기

O_Gyong 2022. 11. 24.

Cipher는 암호화와 복호화를 위한 기능을 제공한다.

예제로 EditText에 텍스트를 입력하고 '암호화' 버튼과 '복호화' 버튼을 클릭하여 입력한 텍스트가 어떻게 표시되는지 보여주려고 한다.

1. MessageDigest를 사용하여 Hash 키 생성
2. Cipher.getInstance로 객체 생성
3. SecretKeySpec을 통해 '1'의 Hash 키를 비밀키로 변환
4. Cipher.init으로 초기화하여 암호화 또는 복호화 작업 수행

MessageDigest를 사용하여 Hash 키 생성

Cipher를 사용하려면 해시 키가 필요하다. 해시 키는 MessageDigest 만들 수 있다.

/**
 * 주어진 바이트 배열과 알고리즘을 사용하여 해시키를 반환.
 */
private fun hashSHA256(): ByteArray {
    val hash: ByteArray
    try {
        val md = MessageDigest.getInstance("SHA-256") // 길이에 따라 16Byte → AES128, 24Byte → AES192, 32Byte → AES256 로 처리된다고 함.
        md.update(ByteArray(1)) // MessageDigest를 업데이트하여 초기화 시켜준다. 어떤 값을 넣어야 하는지는 잘 모르겠음.
        hash = md.digest() // Hash 계산을 완료하여 Hash 값의 바이트 배열을 생성
    } catch (e: CloneNotSupportedException) {
        throw DigestException("couldn't make digest of partial content")
    }
    return hash
}

 

MessageDigest를 초기화하기 위해 update를 사용하는데 인자로 ByteArray를 넘겨줘야 한다.

이후 digest를 통해 Hash 키를 생성한다.

참고 자료


암호화와 복호화에 사용할 변수 선언

/**
 * 암호화, 복호화에 사용할 변수
 */
private val iv = ByteArray(16) // 초기화 벡터에 사용할 값
private val keySpec = SecretKeySpec(hashSHA256(), "AES") // 바이트 배열의 데이터 키를 비밀키로 변환
private val cipherInstance = Cipher.getInstance("AES/CBC/PKCS5Padding") // Cipher 객체에 알고리즘 옵션 부여.
private var encText = "" // 암호화 한 text를 담을 변수

암호화와 복호화에 사용할 변수들을 선언한다.

Cipher.getInstance를 통해 Cipher 객체를 생성한다. 객체를 생성할 때 암호화 알고리즘을 전달한다.

알고리즘의 종류는 굉장히 많은데 Android Keystore에서 선택하면 된다.

 

SecretKeySpec은 넘겨받은 ByteArray를 알고리즘을 통해 비밀 키를 생성한다.

참고 자료


암호화와 복호화 작업

/**
 * 암호화 시작
 */
mBinding.btnEnc.setOnClickListener {
    // 암호화 작업
    cipherInstance.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(iv))
    val byteArrayEncText = cipherInstance.doFinal(mBinding.etMain.text.toString().toByteArray())
    encText = String(Base64.encode(byteArrayEncText, Base64.DEFAULT))
    mBinding.tvEnc.text = encText

}

/**
 * 복호화 시작
 */
mBinding.btnDnc.setOnClickListener {
    // 복호화 작업
    try {
        cipherInstance.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(iv))
        val byteArrayDecText = Base64.decode(encText, Base64.DEFAULT)
        mBinding.tvDec.text = String(cipherInstance.doFinal(byteArrayDecText))
    } catch (e: Exception) {
        // TODO : 복호화 실패 알림?
    }
}

암호화나 복호화를 하기 위해서는 Cipher.init을 통해 Cipher를 초기화 시켜줘야 한다.

Cipher를 초기화 시키는데 필요 한 값은 Cipher 동작 모드, 비밀 키, 초기화 벡터 이다.

비밀 키는 위에서 생성했었고 나머지 두 개는 다음과 같다.

Cipher 동작 모드 중에서  Cipher.ENCRYPT_MODE는암호를 암호화 모드로, Cipher.DECRYPT_MODE는 암호를 복호화 모드로 초기화하는데 사용되는 상수이다.

IvParameterSpec은 초기화 벡터를 만드는데 이 값에 따라서 암호화 결과 값이 달라진다고 한다.  (참고 자료)

 

Cipher의 초기화가 끝나면 암호화를 하는 경우 Cipher.doFinal을 통해 암호화된 ByteArray를 생성하고 Base64.encode를 통해 암호화된 ByteArray를 String으로 변환한다.

복호화의 경우에는 Base64.decode를 먼저해서 암호문을 ByteArray로 바꾸고 Cihper.doFinal을 통해 해독하면 된다.

Cipher.doFinal은 다음과 같다.

Cipher.doFinal은 Cipher가 초기화 될 때 전달받은 MODE에 따라  ByteArray를 암호화 하거나 복호화하는 작업을 한다.

Cipher 참고 자료


Activity 전체 코드

class MainActivity : AppCompatActivity() {

    private lateinit var mBinding: ActivityMainBinding

    /**
     * 암호화, 복호화에 사용할 변수
     */
    private val iv = ByteArray(16) // 초기화 벡터에 사용할 값
    private val keySpec = SecretKeySpec(hashSHA256(), "AES") // 바이트 배열의 데이터 키를 비밀키로 변환
    private val cipherInstance = Cipher.getInstance("AES/CBC/PKCS5Padding") // Cipher 객체에 알고리즘 옵션 부여.
    private var encText = "" // 암호화 한 text를 담을 변수

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        mBinding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(mBinding.root)

        mBinding.etMain.setOnEditorActionListener { _, actionId, _ ->
            // 키보드에서 완료 버튼이 눌렸을 때 처리
            if (actionId == EditorInfo.IME_ACTION_DONE) {
                mBinding.etMain.clearFocus()
            }
            false
        }

        /**
         * 암호화 시작
         */
        mBinding.btnEnc.setOnClickListener {
            // 암호화 작업
            cipherInstance.init(Cipher.ENCRYPT_MODE, keySpec, IvParameterSpec(iv))
            val byteArrayEncText = cipherInstance.doFinal(mBinding.etMain.text.toString().toByteArray())
            encText = String(Base64.encode(byteArrayEncText, Base64.DEFAULT))
            mBinding.tvEnc.text = encText
        }

        /**
         * 복호화 시작
         */
        mBinding.btnDnc.setOnClickListener {
            // 복호화 작업
            try {
                cipherInstance.init(Cipher.DECRYPT_MODE, keySpec, IvParameterSpec(iv))
                val byteArrayDecText = Base64.decode(encText, Base64.DEFAULT)
                mBinding.tvDec.text = String(cipherInstance.doFinal(byteArrayDecText))
            } catch (e: Exception) {
                // TODO : 복호화 실패 알림?
            }
        }
    }

    /**
     * 주어진 바이트 배열과 알고리즘을 사용하여 해시키를 반환.
     */
    private fun hashSHA256(): ByteArray {
        val hash: ByteArray
        try {
            val md = MessageDigest.getInstance("SHA-256") // 길이에 따라 16Byte → AES128, 24Byte → AES192, 32Byte → AES256 로 처리된다고 함.
            md.update(ByteArray(1)) // MessageDigest를 업데이트하여 초기화 시켜준다. 어떤 값을 넣어야 하는지는 잘 모르겠음.
            hash = md.digest() // Hash 계산을 완료하여 Hash 값의 바이트 배열을 생성
        } catch (e: CloneNotSupportedException) {
            throw DigestException("couldn't make digest of partial content")
        }
        return hash
    }
}

전체 코드

댓글