본문 바로가기

Kotlin

[코틀린] 플레이페어 암호 알고리즘 (PlayfairCihper)

728x90

코틀린을 익힐 겸 토이프로젝트로 플레이페어 암호 알고리즘을 구현하기로 했습니다.

 

 

개요

플레이페어 알고리즘이란 1800년대 휘트스톤이 만든 암호기법으로 대체법에 해당됩니다.
요약해서 설명드리자면

1. 알파벳 26자를 5*5 테이블에 넣는다. (I / J 는 같은 칸에 I로 넣는다.)

2. 키문자열을 맨 위 배열부터 채워 넣는다.

3. 암호화: 입력한 문자열을 키 테이블에 입력하여 특정 규칙에 따라 치환한다.

4. 복호화: 암호화 규칙의 반대로 치환한다.

 

등.. 다른 디테일한 규칙은 다른 사이트에서 확인해보면 될 듯 합니다.

 

코드 설명

 

main.kt

package com.main

import KeyTable
import PlayfairAlgorithm
import java.util.*

fun main() {

    //클래스선언
    var keyClass:KeyTable = KeyTable()
    var cipherClass:PlayfairAlgorithm = PlayfairAlgorithm()

    //변수선언
    var plainText:String="instruments" // 평문
    var cipherText:String="Monarchy" // 암호문자
    var cipherKey = cipherClass.makeCipherKey(cipherText.uppercase(Locale.getDefault()))  //암호문 변형 APPLE -> APLE

    var keyTable = keyClass.makeKey(cipherKey)   // 암호표

    var encString = cipherClass.encrypt(plainText,keyTable)
    println("암호화 값: $encString")

    var decString = cipherClass.decrypt(encString,keyTable)
    println("디코딩 값: $decString")
}

키를 입력하고 출력하는 main.kt 입니다. 
특별한 거 없이 평문문자와 암호화 할 키를 입력하고, 알고리즘 메소드를 호출해 출력시킵니다.

 

 

KeyTable.kt

import java.lang.Exception

class KeyTable() {

    var key = Array<Array<Char?>>(5) { Array<Char?>(5) { null } }

    fun makeKey(cipherKey: String): Array<Array<Char?>> {
        try {
            var rows = 0
            var columns = 0
            for (char in cipherKey) {
                key[rows][columns] = char
                columns++
                if (columns > 4) {
                    columns = 0
                    rows++
                }
            }
        } catch (e: ArrayIndexOutOfBoundsException) {
            println("배열 대입할 수 있는 값 초과: $e")
            return key
        } catch (e: Exception) {
            println("미지정 오류: $e")
        }


        combineAlphabetArrayAndKeyTable(key, makeAlphabetArray(cipherKey))
        return key
    }

    private fun makeAlphabetArray(cipherKey: String): CharArray {
        var alphabetArray = ArrayList<Char>()
        for (i in 'A'..'Z') {
            if(i!='J') {    // I랑 J는 같은 문자로 취급한다.
                alphabetArray.add(i)
            }
        }
        return alphabetArray.subtract(cipherKey.toCharArray().asIterable().toSet()).toCharArray()
    }

    private fun combineAlphabetArrayAndKeyTable(keyTable: Array<Array<Char?>>, alphabetArray: CharArray):Array<Array<Char?>> {
        var columns = 0
        var cnt = 0
        for (i in keyTable.indices) {
            for (j in keyTable[i]) {
                if (j == null) {
                    keyTable[i][columns] = alphabetArray[cnt]
                    cnt++
                }
                columns++
                if(columns>=5) columns=0
            }
        }
        return keyTable
    }
}

암호화 표를 생성하는 KeyTable입니다.

 

 

 

PlayfairAlgorithm.kt

import java.lang.Exception

class PlayfairAlgorithm {

    fun encrypt(plainText:String, keyTable:Array<Array<Char?>>):String{

        // 암호화 결과 담을 변수
        var encryptText = StringBuilder()

        try {
            // 텍스트 알고리즘 처리를 위한 배열화
            var textArray = makeTextArray(plainText)

            println("key: ${keyTable.contentDeepToString()}")
            println("array: $textArray")


            // target의 쌍이 keyTable에서의 좌표값 구하기
            for (textCouple in textArray) {
                var compareArray = ArrayList<Pair<Int, Int>>()
                for (i in textCouple) {
                    for (keyArray in keyTable) {
                        for (j in keyArray) {
                            if (i == j) {
                                var row = keyTable.indexOf(keyArray)
                                var col = keyArray.indexOf(i)
                                compareArray.add(Pair(row, col))
                            }
                        }
                    }
                }
                var firstElement: Char
                var secondElement: Char

                // 규칙 1. 암호화 하려는 두 문자가 서로다른 행과 다른 열에 존재할 경우
                // - 사각형을 그려 서로 수평하는 위치의 반대쪽 문자로 치환
                if (compareArray[0].first != compareArray[1].first && compareArray[0].second != compareArray[1].second) {
                    firstElement = keyTable[compareArray[0].first][compareArray[1].second]!!
                    secondElement = keyTable[compareArray[1].first][compareArray[0].second]!!

                // 규칙 2. 두 문자가 같은 열에 있는 경우
                // - 한 칸 씩 밑으로 내려간다. 내려갈 자리가 없다면 맨 위로
                } else if (compareArray[0].first != compareArray[1].first && compareArray[0].second == compareArray[1].second) {
                    if (keyTable.size - 1 > compareArray[0].first) {
                        firstElement = keyTable[compareArray[0].first + 1][compareArray[0].second]!!
                    } else {
                        firstElement = keyTable[0][compareArray[0].second]!!
                    }

                    if (keyTable.size - 1 > compareArray[1].first) {
                        secondElement = keyTable[compareArray[1].first + 1][compareArray[1].second]!!
                    } else {
                        secondElement = keyTable[0][compareArray[1].second]!!
                    }

                // 규칙 3. 두 문자가 같은 행에 있는 경우
                // - 한 칸씩 으론쪽으로 이동한다. 이동할 자리가 없다면 맨 왼쪽으로
                } else if (compareArray[0].first == compareArray[1].first && compareArray[0].second != compareArray[1].second) {
                    if (keyTable.size - 1 > compareArray[0].second) {
                        firstElement = keyTable[compareArray[0].first][compareArray[0].second-1]!!
                    } else {
                        firstElement = keyTable[compareArray[0].first][0]!!
                    }

                    if (keyTable.size - 1 > compareArray[1].second) {
                        secondElement = keyTable[compareArray[1].first][compareArray[1].second-1]!!
                    } else {
                        secondElement = keyTable[compareArray[1].first][0]!!
                    }
                } else {
                    throw Exception("규칙에 없는 인자값")
                }
                encryptText.append(firstElement)
                encryptText.append(secondElement)
            }
        } catch (e:Exception){
            println("예외발생 ${e.printStackTrace()}")
        }
//        복호화 알고리즘
//            - 암호화 때와 같으나 규칙 2,3은 움직인 방향과 반대로 이동한다.
        return encryptText.toString()
    }

    fun decrypt(plainText:String, keyTable:Array<Array<Char?>>):String{

        // 암호화 결과 담을 변수
        var encryptText = StringBuilder()

        try {
            // 텍스트 알고리즘 처리를 위한 배열화
            var textArray = makeTextArray(plainText)

            println("key: ${keyTable.contentDeepToString()}")
            println("array: $textArray")


            // target의 쌍이 keyTable에서의 좌표값 구하기
            for (textCouple in textArray) {
                var compareArray = ArrayList<Pair<Int, Int>>()
                for (i in textCouple) {
                    for (keyArray in keyTable) {
                        for (j in keyArray) {
                            if (i == j) {
                                var row = keyTable.indexOf(keyArray)
                                var col = keyArray.indexOf(i)
                                compareArray.add(Pair(row, col))
                            }
                        }
                    }
                }
                var firstElement: Char
                var secondElement: Char

                // 규칙 1. 암호화 하려는 두 문자가 서로다른 행과 다른 열에 존재할 경우
                // - 사각형을 그려 서로 수평하는 위치의 반대쪽 문자로 치환
                if (compareArray[0].first != compareArray[1].first && compareArray[0].second != compareArray[1].second) {
                    firstElement = keyTable[compareArray[0].first][compareArray[1].second]!!
                    secondElement = keyTable[compareArray[1].first][compareArray[0].second]!!

                    // 규칙 2. 두 문자가 같은 열에 있는 경우
                    // - 한 칸 씩 위로 올라간다. 올라갈 자리가 없다면 맨 밑으로
                } else if (compareArray[0].first != compareArray[1].first && compareArray[0].second == compareArray[1].second) {
                    if (0 < compareArray[0].first) {
                        firstElement = keyTable[compareArray[0].first-1][compareArray[0].second]!!
                    } else {
                        firstElement = keyTable[4][compareArray[0].second]!!
                    }

                    if (0 < compareArray[1].first) {
                        secondElement = keyTable[compareArray[1].first-1][compareArray[1].second]!!
                    } else {
                        secondElement = keyTable[4][compareArray[1].second]!!
                    }

                    // 규칙 3. 두 문자가 같은 행에 있는 경우
                    // - 한 칸씩 으론쪽으로 이동한다. 이동할 자리가 없다면 맨 왼쪽으로
                } else if (compareArray[0].first == compareArray[1].first && compareArray[0].second != compareArray[1].second) {
                    if (0 < compareArray[0].second) {
                        firstElement = keyTable[compareArray[0].first][compareArray[0].second+1]!!
                    } else {
                        firstElement = keyTable[compareArray[0].first][4]!!
                    }

                    if (0 < compareArray[1].second) {
                        secondElement = keyTable[compareArray[1].first][compareArray[1].second+1]!!
                    } else {
                        secondElement = keyTable[compareArray[1].first][4]!!
                    }
                } else {
                    throw Exception("규칙에 없는 인자값")
                }
                encryptText.append(firstElement)
                encryptText.append(secondElement)
            }
            println("plainText = ${plainText}")
            println("plainText.length = ${plainText.length}")
            if(plainText.length%2==1){
                encryptText.deleteCharAt(plainText.lastIndex)
            }

        } catch (e:Exception){
            println("예외발생 ${e.printStackTrace()}")
        }
//        복호화 알고리즘
//            - 암호화 때와 같으나 규칙 2,3은 움직인 방향과 반대로 이동한다.
        return encryptText.toString()
    }



    // HELLO -> [[H,E],[L,X],[O,X]] 암,복호화 알고리즘에 사용되는 텍스트 배열 생성
    private fun makeTextArray(plainText: String): ArrayList<ArrayList<Char>> {
        var plainTextList: ArrayList<ArrayList<Char>> = ArrayList<ArrayList<Char>>()
        plainTextList.add(ArrayList())

        var flag = 0 // for문 2개씩 끊기 위한 flag 변수
        var cnt = 0   //for 카운터 변수
        for (i in plainText.uppercase()) {
            if (flag >= 2) {
                flag = 0
                cnt++
                plainTextList.add(ArrayList())
            }
            if (plainTextList[cnt].isNotEmpty() && plainTextList[cnt][0] == i) {   // 같은 문자일 시 X 삽입
                plainTextList[cnt].add('X')
                plainTextList.add(ArrayList())
                cnt++
                plainTextList[cnt].add(i)
            } else {
                plainTextList[cnt].add(i)
                flag++
            }
        }

        // 마지막 자리 부족할 시 X 추가
        if (plainTextList[plainTextList.size - 1].size <= 1) {
            plainTextList[plainTextList.size - 1].add('X')
        }
        return plainTextList
    }

    //암호키 만드는 함수 ex) APPLE -> APLE
    fun makeCipherKey(plainText:String):String{
        var cipherKey:String=""
        for(i in plainText.indices){
            if(plainText.indexOf(plainText.elementAt(i))==i){
                cipherKey+=plainText.elementAt(i)
            }
        }
        return cipherKey
    }
}

문자와 키테이블을 변수로 받아 암호화, 복호화를 수행하는 알고리즘(코어)클래스입니다.
중간에 찾았는데 코틀린에는 좌표 값을 저장하기 적당한 Pair라는 자료형이 있더라고요.
뒤늦게 발견해서 다소 삽질을 했습니다. (Triple이라는 3차원 자료형도 있음)

 

 

동작화면

X 값 처리는 아무리 자료를 찾아봐도 따로 처리하는 방법이 없는 것 같네요
그냥 X를 넣거나 제거하는 수밖에 없을 것 같습니다.

 

객체지향으로 구현하려고 해보았는데 아무래도 단일적인 기능을 하는 프로젝트다 보니 장점을 살리기가 쉽지 않았습니다.

 

요새 번아웃이 온 것 같아요.
조금 쉬엄쉬엄 하려고 합니다..

 

 

전체코드입니다.
https://github.com/Jonghyun99/playfair_cipher

'Kotlin' 카테고리의 다른 글

[Kotlin] 코틀린 기초문법 요약 (Kotlin Summary)  (0) 2022.05.07