QR코드 색상암호화
Last updated
Last updated
'hello'라는 문자열을 QR코드로 나타내면 아래와 같아진다.
여기서 검은 블럭에는 RGB값을 각각 255, randint(0,255), randint(0,255) 로 변환시킨다.
그리고 흰 블럭에서는 RGB값을 각각 0, randint(0,255), randint(0,255) 로 변환시킨다.
그러면 위와 같은 새로운 이미지를 얻는다.
보통의 경우, QR코드 인식 과정에서 이미지를 Grayscale로 변환(흑백사진으로 만듦)하기 때문에,
일반적인 QR코드 인식기는 위 이미지를 아래와 같이 받아들이게 된다.
당연히 이 이미지로 QR코드 인식을 시키면, QR코드가 있는지도 인식하지 못한다.
하지만 이미지의 RGB값이 (r, g, b)로 들어오면 이를 (r, 0, 0) 값으로 변환한다면 어떨까?
마치 붉은색 선글라스를 꼈을 때처럼 인식될 것이다. 그리고 QR코드의 형상도 비교적 선명하게 보일 것이다.
편의를 위해, 각 픽셀의 (r, g, b)값을 (r, r, r) 값으로 변환하고 이미지에 반전을 주었다.
그러면 붉은 부분이 강할수록 검은색이 되고, 약할수록 흰 색이 된다.
이러한 처리 이후에 QR코드 인식을 시켜보면, QR코드를 잘 인식한다.
QR코드가 포함하고 있던 hello 라는 문자열을 잘 읽었다.
위 예시에서는 빨강 값으로 암호화했지만, Green 또는 Blue 값으로 암호화할 수도 있다.
RGB방식이 아닌 HSV에서 색상, 채도 등을 주요 값으로 설정할 수도 있고,
혹은 선형대수학을 이용하면 무궁무진한 방법을 고안할 수 있을 것 같다.
이런 암호화의 방식을 서버에서 지정하거나 시간(시/분/초)에 따라 변하도록 설정하면,
App과 RaspPi단이 같은 암호화 방식을 공유할 때에만 QR코드를 인식할 수 있으므로
보안 면에서 더 탄탄해질 것이다.
같은 예시를 사용다. 'hello'라는 문자열을 아래와 같은 QR코드로 나타냈다.
이를 YELLOW 색상, RGB값으로는 (255,255,0)을 Key Color로 하여 암호화 해 보았다.
Noise는 Cyan(0,255,255) 과 Magenta(255,0,255) 로 주었다.
결과적으로 아래와 같은 이미지를 얻었다.
이번에는 좀 더 확실한 검증을 위해, 외부인의 도움을 받아보았다.
위 이미지를 컴퓨터 화면에 띄우고 핸드폰으로 찍은 사진이다.
이미지 속 픽셀의 RGB 값 중 (1,1,0) 성분을 기준으로 필터링 하였다.
그리고 각 픽셀의 (r, g, b)값에서 (1, 1, 0) 성분을 구한다.
해독기는 QR코드를 잘 인식하고 원래 문자열인 'hello'를 잘 인식하...지 못한다.
왜 못읽나 싶었는데, 아무래도 컴퓨터 액정을 찍을 때 생기는 노이즈 때문인 것 같다.
그래서 QR코드 인식 과정 중간에 3픽셀 크기의 가우시안 블러 필터를 거치도록 했다.
이미지가 전체적으로 뿌얘졌다.
너무 흐릿하면 읽기 힘들지 않을까 걱정했는데, 다행히 잘 읽는다.
같은 방법으로 Cyan(0, 1, 1), Magenta(1, 0, 1)를 Key Color로 한 색상 암호화도 해볼 수 있겠다.
색상 암호화 과정에서 변환된 이미지의 기저로서 (1,1,0), (0,1,1), (1,0,1) 의 세 벡터를 사용했다는 점이다. 이 경우, 당연히 (1, 1, 0) 성분을 계산하더라도 처음에 부여했던 성분 그대로의 값이 나오지 않는다. 기저를 이루는 다른 벡터인 (0, 1, 1), (1, 0, 1) 만을 사용하더라도 (1, 1, 0) 성분이 생성될 수 있기 때문이다. 즉, QR코드 자체가 오염될 수 있다.
그래도 일차적으로 안심할 수 있는 이유는, 첫째로 QR코드의 Parity Check 덕분이다.
원본 이미지에 그림판으로 노란색 블럭 하나를 그냥 박아보았다. (대충 오른쪽 아래 부분)
저 위치는 원래의 QR코드에서 흰색인 자리다.
결국 Yellow를 Key Color로 복호화했을 때 아래와 같은 노골적인 검정 블럭이 생겨버렸다.
QR코드가 오염된 것이다.
하지만 QR코드 속의 'hello' 문자열은 정상적으로 인식되었다.
지금과 같이 QR코드가 약간 변형되더라도 원본 데이터를 그대로 전달할 수 있는 이유는 QR코드에 오류 복원 기능이 적용되어있기 때문이다.
https://www.qrcode.com/ko/img/errorCorrection/errorCorrectionTable.png
아래와 같이 QR코드 내부에 작은 그림 혹은 로고를 삽입해도 문제가 없는 이유가 이와 같은 오류 복원 기능 덕분이다.
둘째로, 이미지 변환의 기저를 직교기저로 사용하면 Noise의 영향이 확실히 없어질 것이다.
직교기저에서는 몇 가지 기저를 아무리 조합하더라도, 조합에 포함시키지 않은 기저의 성분은 생성되지 않는다. 따라서 Key Color의 성분을 안전하게 전달하고 추출할 수 있다.
어떤 벡터공간에서 영벡터가 아닌 하나의 벡터를 잡으면, 그람-슈미트 과정을 통해 그 벡터를 포함하는 직교기저를 생성할 수 있다. Normalize까지 해주면 정규직교기저를 만들 수 있다.
RGB값을 표현한 어떤 벡터를 랜덤하게 생성하고 그 벡터로 정규직교기저를 만들어준다. 그리고 처음 생성한 벡터를 Key Vector, 그 벡터가 표현하는 색을 Key Color라고 하자. 그리고 정규직교기저를 이루는 나머지 두 벡터는 Noise를 주기 위한 벡터로 사용한다.
이를 모두 적용하여 QR코드를 임의의 Key Vector로 암호화 해 보았다.
Key Vector: [-0.65005812, -0.70617825, 0.28060063]
Key Vector: [ 0.20749598, -0.95767376, 0.19951537]
Key Vector: [ 0.59002732, -0.50573771, -0.62936248]
Key Vector: [-0.41927463, 0.89960868, 0.12211883]
위와 같이 형형색색의 QR코드로 암호화할 수 있다.
다만 바로 알 수 있는 문제가 있다. 암호화를 하긴 했지만 색감이 좀 좋은 사람이면 적당히 옮겨 그려서 복제를 할 수도 있을 것 같다. 물론 그 사이에 인증 절차는 끝나겠지만...
아무래도 칼같이 직교하는 벡터들을 사용하다 보니 생기는 문제인 것 같다. 이전의 Yellow 암호화 때는 기저를 임의로 Naive하게 잡았는데도 잘 작동했던 걸 생각하면 기저에도 약간의 Noise를 주고서 써먹어봐야겠다. 어차피 잘 짜여진 QR코드의 Parity Check 시스템이 알아서 인식시켜줄 것 같다.
Key Vector에 맞게 이미지를 변환하고 QR코드까지 인식시켜 봤다. 까먹고 Gaussian Blur도 안 뺐는데도 잘 인식했다.
또 한 가지 문제... 대충 랜덤하게 QR코드 만들고서 RED, GREEN, BLUE 중 아무 방식으로 읽어봤는데 생각보다 잘 읽힌다. 예를 들어 YELLOW로 암호화하고 GREEN으로 읽었는데 읽힌다.
다행히 짧은 문자열(hello)가 아닌 좀 복잡하고 긴 문자열을 사용해서 테스트해보니 막혔다.
아닌데... 되는데...;
하여간 이건 기저의 직교성을 좀 낮추든가 해서 해결을 해봐야겠다...
좀 더 해보니깐 YELLOW로 한건 RED, GREEN, BLUE로는 절대 안읽히는데, 직교기저를 사용한 암호화는 R, G, B중에 Key Color에서 높은 값을 갖는 색상으로 복호화를 시키니 너무 잘 읽힌다. 확실히 칼같은 직교성이 문제인 것 같다.
내용: [email protected]$DSi23Dqr23#a12asdf4sdf6d3h5hnc6alFASDdfn!$
Key Color: Yellow
무조건 Yellow로 읽어야만 읽힌다.
+혹시나 해서 위에서 색상암호화 했던 몇 개를 핸드폰 QR코드 리더기로 찍어봤는데, 얘는 너무 잘 읽힌다... 하여간 Key Vector 를 잘 골라서 쓰는 것도 중요할 것 같다.
Key Vector: [-0.65005812, -0.70617825, 0.28060063]
위에서 그람 슈미트가 어쩌고 하면서 정규직교기저를 열심히 만들어서 QR코드를 암호화했는데, 생각해보니 굳이 정규직교기저를 사용할 필요는 없다. Yellow로 암호화했을 때처럼 그냥 아무 기저를 사용하더라도, 편의성을 위해 기저에 정규성만 주어서 암호화한 뒤에 복호화시에는 같은 기저로 연립방정식 한 번만 풀면 된다.
그래서 최소거리를 지정해서 다른 단위벡터와 그 거리 이상으로 떨어지도록 하고, 최대거리를 2보다 약간 작은 수로 지정하여(거리가 2인 단위벡터는 방향이 정반대인 단위벡터이므로 서로 종속적임) 그 거리 미만의 단위벡터 세 개를 랜덤하게 생성하여 기저를 만들었다. 그러면 세 벡터는 서로 독립임이 보장되므로 기저를 이루게 된다.
암호화, 복호화 과정은 아래와 같은 선형변환을 통해 이루어진다.
이제 App과 IoT에서는 난수를 기반으로 정규기저를 동일하게 생성해서 암호화/복호화를 진행하면 되겠다.
아래는 예시다.
Key Basis: [array([-0.5082292 , -0.31186792, -0.80277113]), array([-0.56195831, -0.66054749, -0.49787535]), array([-0.49415952, -0.40927322, -0.76700834])]
잘 해독된다.
Red를 Key Vector로 한 QR코드를 스마트폰에 띄우고, 사진을 찍어 Decoding해 보았다.
QR코드의 형태가 아주 잘 보이는 것을 확인할 수 있다.
실제 반납기에서도 사용할 수 있는 방법임이 검증되었다!ㅎㅎ
QR코드가 일반 사용자에게 노출되면 누구나 그 속의 내용을 뜯어볼 수 있다. 물론 QR코드에 담긴 내용을 알더라도 Key값으로 사용되는 고유 시리얼 값을 알지 못한다면 비정상적인 반납 행위를 할 수는 없다. 하지만 QR코드를 뜯어보는 것을 방지하거나, TOTP 인증 이상의 높은 보안 수준을 원한다면 본 기술을 적용할 수 있다.
이를 위해 QR코드 색상 암호화를 시험해볼 수 있는 코드를 IoT Repository 안에 첨부해두었다. IoT Repository > test 폴더에 들어가 보면 qr_gen.py 파일과 qr_decoder.py 파일이 있다.
qr_gen.py 에 있는 주요 함수는 아래와 같다. 각 함수는 아래와 같은 구조로 되어있다.
qr_decoder.py 에 있는 주요 함수는 아래와 같다.