프로토콜

내 코드, PC 서버, 폰 사이의 통신 형식입니다. 한 줄에 JSON 객체 하나이므로 어떤 언어로든 폰을 움직일 수 있습니다.

번역된 페이지입니다. 영어 문서와 내용이 다르면 영어 문서가 기준입니다. English

이 페이지는 프로토콜을 요약합니다. 키 스케줄과 릴레이 프레임을 포함한 공식 명세는 저장소의 PROTOCOL.md이고, 명령 자체는 commands.json에 정의되어 있습니다.

포트

포트프로토콜수신 주소용도
8778UDP모든 인터페이스와이파이에서 폰이 PC를 찾을 때
8779TCP모든 인터페이스폰 접속. 한 포트에서 일반 TCP, TLS, WebSocket
8780TCP127.0.0.1내 코드. 한 포트에서 NDJSON, HTTP, WebSocket

내 코드에서 서버로

UTF-8 JSON을 한 줄에 객체 하나씩 보냅니다. 요청마다 직접 정한 id와 cmd가 있습니다.

{"id":1,"cmd":"touch","device":"shelf-01","by":"id","value":"login"}
{"id":1,"ok":true,"via":"node","ms":412}
  • 온라인인 폰이 딱 한 대면 device를 생략합니다.
  • 파라미터 이름은 레퍼런스와 같습니다. 위치 인자로도 넘길 수 있습니다: {"id":2,"cmd":"tap","args":[540,1200]}.
  • touchById 같은 축약형도 cmd로 받습니다.
  • 결과 값이 하나면 value에 담겨 옵니다: {"id":3,"ok":true,"value":false}.
  • 에러: {"id":4,"ok":false,"error":"NOT_FOUND","msg":"…","retryable":true,"screen":"com.example/.Main"}.
  • 요청은 파이프라이닝할 수 있습니다. 서로 다른 폰에 대한 응답은 순서가 바뀔 수 있고, 한 폰에 대한 응답은 순서를 지킵니다.

이벤트

{"id":9,"cmd":"subscribe","events":["notification","device"]}를 보내면 그 연결로 id가 없는 줄도 받게 됩니다.

{"event":"device","device":"k7d2q9xa","name":"shelf-01","state":"online","route":"lan"}
{"event":"notification","device":"k7d2q9xa","key":"0|com.kakao.talk|1|null|10123","package":"com.kakao.talk","title":"Kim","text":"See you at 3"}

종류: device, notification, screen(포그라운드 앱이 바뀜), toast, result(accepted로 응답했던 명령의 늦은 결과).

네트워크를 끊는 명령

data(false), wifi(false), airplane(true)와 이 명령을 포함한 batch는 먼저 {"ok":true,"accepted":true}로 응답하고, 폰이 돌아오면 {"event":"result","id":…}를 보냅니다. 최종 결과를 응답으로 받으려면 "wait":true를 추가하세요. 명령이 오프라인 폰을 기다리는 시간은 "offline_wait": seconds로 바꿀 수 있습니다.

서버에서 폰으로

폰이 PC에 접속하면 평문 핸드셰이크 두 줄이 오갑니다. 폰이 자신의 임시 P-256 키와 논스를 보내고, 서버가 자신의 것으로 답합니다. 그다음 양쪽은 두 ECDH 결과에서 HKDF-SHA256으로 키를 유도합니다. 하나는 임시 키끼리(전방 비밀성), 다른 하나는 고정 키끼리(양쪽이 서로의 신원을 증명)입니다. QR 페어링에서는 일회용 토큰도 함께 섞고, 코드 페어링에서는 같은 키에서 유도한 6자리 코드를 양쪽이 보여 줍니다.

그 뒤의 모든 줄은 envelope 형식입니다.

{"seq":0,"blob":"<base64url AES-256-GCM ciphertext>"}

seq는 방향마다 1씩 늘어나고 논스로도 쓰입니다. 그래서 재전송되거나 순서가 바뀐 줄은 거부됩니다. 512 KiB가 넘는 blob은 "more":true를 붙여 여러 조각으로 나눕니다.

envelope 안에서 서버는 자체 id를 붙여 명령을 보내고, 폰은 보내는 모든 것에 n으로 번호를 매깁니다.

{"id":17,"cmd":"touch","by":"id","value":"com.kakao.talk:id/login","nth":0,"timeout":10}
{"id":17,"n":42,"ok":true,"via":"node","ms":412}

폰은 서버가 n을 확인할 때까지 메시지를 보관하고, 최근 명령 ID 2000개를 기억합니다. 그래서 다시 접속해도 잃는 것도, 반복되는 것도 없습니다. 연결 유지용 ping은 25초마다 보냅니다.

탐색

{"droidline":"discover","v":1,"server":"k3j9d0a2mq"}
{"droidline":"here","v":1,"server":"k3j9d0a2mq","name":"OFFICE-PC","port":8779,"pub":"BD3x…","pairing":true}

이 응답만으로는 신뢰하지 않습니다. PC의 키는 핸드셰이크에서 확인합니다.

테스트 벡터

test-vectors.json에는 고정 키, 핸드셰이크 줄, 유도된 키, 6자리 코드, 암호화된 envelope가 들어 있습니다. Go 서버, 안드로이드 앱, 릴레이 모두 이 파일로 테스트합니다. 다른 언어로 만든 구현체도 이 파일로 테스트할 수 있습니다.