Signito
블로그

종단간 암호화 메신저의 푸시 알림

  • 푸시 알림
  • 종단간 암호화

종단간 암호화를 구현하다 보면 곧 이상한 문제에 부딪힌다. 서버가 메시지 내용을 읽지 못하는데 사용자에게는 알림이 떠야 한다. 무슨 내용인지 모르면서 무엇을 알릴 것인가.

답부터 말하면 알림은 내용이 아니라 봉투로 만든다.

푸시가 필요한 순간은 정해져 있다

먼저 갈라 둘 것이 있다. 앱이 켜져 있고 서버와 연결이 붙어 있는 동안에는 푸시가 필요 없다. 그 연결로 메시지가 바로 들어오고 알림도 앱이 직접 만든다. 이 경로는 우리 서버와 기기 사이에서 끝난다.

문제는 앱이 꺼져 있거나 잠들어 있을 때다. 그때는 연결이 없으니 바깥에서 기기를 두드려야 하고 그 두드림을 운영체제의 푸시 서비스가 맡는다. 아래 이야기는 전부 이 경우다.

서버가 아는 것과 모르는 것

서버가 못 읽는 것은 메시지 본문이다. 그 바깥은 전달에 필요한 만큼 안다. 어느 방에 언제 무언가 들어왔는지와 그 방의 멤버가 누구인지다. 모르면 배달을 못 한다.

그래서 알림을 띄울지 말지는 판단할 수 있다. 내용을 몰라도 이 사람에게 읽지 않은 것이 있다는 사실은 셀 수 있기 때문이다.

여기에 하나가 더 있다. 닉네임과 방 이름은 서버가 안다. 계정을 찾아 주고 방 목록을 보여 주려면 서버가 다뤄야 하기 때문이다.

알림에 실을 수 있는 것

본문 자리에는 개수를 쓴다. 읽지 않은 것이 몇 개라는 식이다. 내용을 모르니 그 이상 쓸 것이 없고 사실 그 이상 쓰지 않는 편이 낫다.

제목 자리에는 선택지가 생긴다. 서버가 아는 값이므로 보낸 사람의 닉네임이나 방 이름을 넣을 수 있다. 다만 켜고 끌 수 있어야 한다. 잠금 화면에 누구와 이야기하는지가 뜨면 곤란한 사람이 있다.

언어도 서버가 고른다. 알림 문구는 받는 사람의 기기 언어로 나가야 하므로 같은 메시지라도 기기마다 다른 문장이 만들어진다.

기기가 여러 대면 하나가 더 필요하다. 한 기기에서 방을 읽으면 나머지 기기에 떠 있던 알림도 내려 줘야 한다. 그러려면 읽었다는 신호를 알림 경로로 한 번 더 보내야 한다.

저장할 때 잠그는 것은 무엇을 막나

이런 값도 저장할 때는 잠가 둘 수 있다. 여기서 잠근다는 것이 무엇을 막는지를 따져 볼 만하다.

막는 것은 찾기다. 제목이 평문으로 쌓여 있으면 특정 단어가 든 방을 한 줄짜리 조건으로 전부 골라낼 수 있다. 잠가 두면 그 조건이 걸리지 않는다. 같은 제목이라도 저장할 때마다 다른 암호문이 되게 해 두면 암호문끼리 맞춰 보고 묶는 것도 안 된다. 데이터베이스가 통째로 새어 나가거나 로그에 값이 찍히는 경우도 함께 막힌다.

막지 못하는 것은 열기다. 열쇠를 서버가 갖고 있으니 특정 방을 골라 푸는 것은 언제든 된다. 찾는 것은 막고 여는 것은 못 막는다. 저장할 때 잠그는 것과 종단간으로 잠그는 것의 차이가 여기다.

알림 제목을 만드는 순간이 바로 서버가 그 값을 푸는 순간이다.

알림은 우리 서버에서 곧장 기기로 가지 않는다. 가운데를 운영체제의 푸시 서비스가 맡고 제목에 넣은 값은 그 자리에서 그대로 보인다.

알림은 애플과 구글을 지난다

여기가 이 주제에서 가장 중요한 대목이다.

앱이 꺼져 있을 때 알림을 띄우는 길은 운영체제의 푸시 서비스뿐이다. 아이폰이면 애플이고 안드로이드면 구글이다. 우회할 방법이 없다. 그래서 알림 하나하나가 그쪽 서버를 지난다.

그 경로를 쓰려면 기기마다 발급되는 토큰이 필요하고 서버는 그 토큰을 계정에 붙여 보관해야 한다. 어느 기기로 보낼지 지정하는 값이라 없으면 배달이 안 된다. 토큰 자체가 애플과 구글이 발급한 식별자이기도 하다.

내용을 안 실으면 그쪽도 내용은 모른다. 그런데 남는 것이 있다. 어느 기기에 언제 알림이 갔는지다. 빈도와 시각만으로도 누가 누구와 활발히 이야기 중인지는 드러난다.

제목에 닉네임을 켜면 그 닉네임도 함께 지나간다. 그러니 이 설정은 잠금 화면만의 문제가 아니라 푸시 서비스에 무엇을 흘릴지의 문제이기도 하다. 알림이 다섯 번째 사본이 되는 이유가 여기에 있다.

내용을 띄우고 싶다면

내용을 알림에 띄우면서 서버도 푸시 서비스도 모르게 하고 싶다면 길이 하나 있다. 암호문을 그대로 알림에 실어 보내고 기기에 도착한 뒤에 앱이 풀어서 문구를 채우는 것이다.

아이폰에는 이 용도의 확장이 있다. 알림이 화면에 뜨기 전에 짧게 코드를 돌릴 수 있고 거기서 복호화해 내용을 바꿔 넣는다. 안드로이드는 앱이 알림을 직접 만들 수 있어 구조가 다르다.

대가도 분명하다. 복호화에 쓸 열쇠를 그 확장이 꺼낼 수 있어야 하고 실행 시간과 메모리에 제한이 걸린다. 그리고 암호문은 여전히 푸시 서비스를 지난다. 내용은 가려지지만 언제 누구에게 갔는지는 그대로 남는다.

한 걸음 더 간 방식도 있다. 알림을 띄우지 않고 앱을 잠깐 깨우기만 하는 푸시를 보낸 다음 깨어난 앱이 서버에서 직접 받아 알림을 만드는 것이다. 이러면 푸시 서비스는 내용은커녕 암호문도 보지 못한다.

다만 운영체제가 이 실행을 넉넉히 보장하지 않는다. 배터리 상태와 사용 습관에 따라 미뤄지거나 건너뛰어지므로 알림이 늦거나 오지 않을 수 있다. 정확히 도착해야 하는 알림을 여기에만 맡기기는 어렵다.

자주 묻는 질문

서버가 못 읽는데 알림이 뜨는 이유가 뭔가요

봉투는 보이기 때문이다. 어느 방에 언제 무언가 도착했는지는 전달을 하려면 알아야 하고 알림은 그 정보만으로 만들 수 있다. 본문은 열지 않아도 개수는 센다.

알림에 이름이 뜨면 암호화가 깨진 건가요

아니다. 닉네임과 방 이름은 애초에 종단간 암호화의 대상이 아니라 서버가 다루는 값이다. 다만 그 값이 알림에 실리면 푸시 서비스를 지나가므로 켜 둘지는 따로 판단할 문제다.

알림을 아예 끄면 그 경로가 없어지나요

푸시를 받지 않으면 그 경로로 나가는 것은 없다. 대신 앱을 직접 열어야 새 메시지를 알게 된다. 설정을 점검할 때 이 둘을 저울질하게 된다.

푸시 서비스를 안 거치는 방법은 없나요

앱이 꺼져 있는 동안에는 없다. 운영체제가 정해 둔 길이고 앱이 대신 연결을 붙들고 있을 수 없다. 앱이 켜져 있는 동안에는 자체 연결로 받으므로 그 경로를 거치지 않는다.