2016년 10월 26일 수요일

PyBitmessage 소스 분석

https://github.com/Bitmessage/PyBitmessage


Protocol

  • 'version' : 주위 노드와 첫 연결시 보낸다.
  • 'verack' : 'version'에의 응답이다. payload없이 header만 보낸다.
  • 'addr' : 'verack'를 받으면 주위 노드들에게 노드 정보를 알린다.
  • 'inv' : 주위의 노드들에게 내가 원하는 inventory hash를 보낸다.
  • 'getdata' : 'inv'에의 응답이다. 'inv'의 object를 달라고 요청하는 것이다.
  • 'object' : 'getdata'에의 응답이다. 무조건 다른 node들에게 전달하고('inv'를 propagate한다.) 내가 처리해야 할 object인지를 본다. 아니면 무시한다.
    • 'getpubkey': public key를 요청한다.
    • 'pubkey': public key를 보낸다.
  • 'error' : 받은 데이터에 문제가 있는 경우 에러 메시지로 응답한다.

첫 연결시 보내는 정보

  • payload = protocol version number(3) + bitfield of features + 현재 시간(unix time) + boolservices of remote connection + remote host + bitflags of the services + my host + nonce + user agent의 길이를 숫자 인코딩한 값 + user agent + stream numbers를 숫자 인코딩한 값 + 내 stream number를 숫자 인코딩한 값
  • protocol version이 나보다 작으면 연결하지 않는다.
  • bitfield = NODE_NETWORK(1) | NODE_SSL(2, ssl을 지원하는 경우)
  • 보낸 시간과 받은 시간이 1시간 이상 차이가 나면 에러로 응답한다.
  • boolservices of remote connection = 1(8bytes)
  • remote host = 0x00000000000000000000FFFF(12bytes) + host의 16진수 표현(4bytes) + remote port(2bytes)
    • 이 값을 받은 노드는 외부에서 알고있는 자신의 IP를 알 수 있다.
  • bitflags of the services = 1(8bytes)
  • my host = 0x00000000000000000000FFFF(12bytes) + 2130706433을 숫자 인코딩한 값(4bytes, 127.0.0.1을 의미, 이 값을 받은 노드는 실제 연결된 IP를 사용한다.) + 내 port(2bytes)
  • nonce는 랜덤 값이다.
  • user agent = "/PyBitMessage:softwareVersion/" - softwareVersion은 현재 사용중인 프로그램의 버전을 적는다(예:0.6.1)
    • 이 값을 통해 상대방이 누구인지 알 수 있다.(예: 같은 프로그램인데 버전이 높다면 업그레이드 메시지를 사용자에게 보여줄 수 있다.)
  • stream numbers는 관심있는 stream의 수를 의미한다. PyBitMessage는 1을 준다.
  • 같은 stream number와만 연결이 가능하다.

주위 노드들에게 연결된 노드의 정보를 알리기

  • 노드의 수 + (verack를 받은 시간 + stream number + service bit flags(1) + host + port)를 노드의 수만큼 반복
  • host는 (0x00000000000000000000FFFF + host의 16진수 표현)으로 바꾸고 나머지는 다 숫자 인코딩을 한다.
  • 이 값을 받은 노드는 (time, host, port)를 내부에 저장한다.

주소 만들기

  • Deterministic address와 Random address 두가지 형태로 주소를 만들 수 있다.
  • Random address
    • 완전히 랜덤으로 생성된다: 주소값을 잊으면 다시는 같은 주소를 생성할 수 없다.
    • address version number로 4를 stream number로는 1을 사용한다.
    • 생성 방법
    • a. signing key를 생성한다.
      • potential private signing key를 생성한다: OpenSSL.rand(32)
      • potential private signing key로부터 potential public signing key를 생성한다: Elliptic Curve DSA 알고리즘을 사용하면 private key로부터 public key를 항상 생성할 수 있다.
    • b. encryption key를 생성한다.
      • potential private encryption key를 생성한다: OpenSSL.rand(32)
      • potential private encryption key로부터 potential public encryption key를 생성한다: ECDSA 알고리즘 사용
    • c. (potential public signing key + potential public encryption key)에 sha512를 적용한 후 ripemd160을 적용한다. ripemd160을 적용한 값이 0x00으로 시작하지 않으면 0x00이 나올 때까지 다시 계산한다.
    • d. (address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + 앞의 0x00을 제외한 ripe값)
    • e. 앞의 값에 sha512를 두번 적용해서 나온 값의 앞 4byte를 checksum으로 한다.
    • f. (d에서 나온 값을 hex로 표현한 형태로 바꾼 값 + e에서의 checksum을 hex로 표현한 형태로 바꾼 값)을 16진수값으로 바꾼 후 base58로 인코딩하고 앞에 'BM-'를 추가한다: 이 값이 주소!!!
  • Deterministic address
    • passphrase를 지정하여 주소를 필요할 때마다 다시 생성할 수 있도록 한다: 같은 passphrase로는 같은 주소가 만들어진다.
    • address version number로 3을 stream number로는 1을 사용한다.
    • 앞의 Random address에서와 같은 방법을 사용하나 key를 만드는 방법만이 다르다: passphrase로 키를 만들기 때문에 나중에 재생성이 가능해진다.
    • signingKeyNonce = 0, encryptionKeyNonce = 1로 설정하여 key 만들기를 시작한다.
    • (passphrase + signingKeyNonce)을 sha512를 하여 나오는 값 중 앞의 256bit를 potential private signing key로 한다.
    • (passphrase + encryptionKeyNonce)을 sha512를 하여 나오는 값 중 앞의 256bit를 potential private encryption key로 한다.
    • 이 key들을 가지고 Random address에서 주소를 만들 때 한 과정과 같은 방법으로 주소를 만든다.
      • 이때, 0x00을 확인하는 과정에서 0x00이 아니면 (signingKeyNonce += 2, encryptionKeyNonce += 2)를 하여 새로운 key를 만들어서 과정을 반복한다.

private key 저장하기

  • Wallet_import_format을 사용한다.
  • private key(private signing key와 private encryption key)의 앞에 0x80을 추가한다.
  • sha256을 두번 적용하여 나온 값의 앞 4byte를 checksum으로 한다.
  • (0x80 + private key + checksum)을 Base58로 인코딩한다.

주위 노드들에게 내가 알고 있는 정보를 알리기

  • 'inv' 보내기
  • 주소로부터 address version number와 stream number와 hash를 뽑아낸다.
  • key의 유지 시간(embeddedTime이라 하자)을 계산한다: 현재 시간 + TTL
    • TTL을 (28일 +- 5분)으로 한다.
  • payload = embeddedTime을 숫자 인코딩한 값 + 0x00000001 + address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + doubleHashOfAddressData의 뒤쪽 32byte(tag)
    • doubleHashOfAddressData = (address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + hash)에 sha512를 두번 적용한 값
  • dataToEncrypt = 주소의 bitfield 계산 + public signing key + public encryption key + nonceTrialsPerByte를 숫자 인코딩한 값 + payloadLengthExtraBytes를 숫자 인코딩한 값
    • WIF로 저장된 주소로부터 public key 값을 가져온다.
    • WIF로부터 가져온 key값은 0x80을 앞에 가지고 있으므로 이를 뺴주어야 한다.
  • (payload + dataToEncrypt)를 private signing key를 사용해서 ECDSA로 sign을 한다. sign해서 나온 값을 signature라 하자.
  • dataToEncrypt에 (signature의 길이를 숫자 인코딩한 값 + signature)를 추가한다.
  • dataToEncrypt를 encryption할 때 사용하기 위해 doubleHashOfAddressData의 앞쪽 32byte를 private key로 하는 public/private key 쌍을 만든다. message를 encryption할 때 사용하는 것과 같은 알고리즘인 ECDSA로 public key를 만든다.
  • dataToEncrypt를 앞의 public key를 사용해서 ECDSA로 encryption을 한다. encryption된 값을 payload에 추가한다.
    • 받는 노드에서 주소를 알고 있다면 이 주소의 double hash를 통해 private key를 알아낼 수 있고 decrypt할 수 있게 된다.
  • POW를 실행하여 trialValue와 nonce를 얻는다.
  • payload의 앞에 nonce를 숫자로 변환한 값을 추가한다.
  • 현재까지의 전체 payload로부터 inventory hash를 만든다.
    • inventory hash = payload에 sha512를 두번 적용하여 나온 값의 앞 32byte
  • inventory hash를 키로하여 (objectType(pubkey는 1이다), streamNumber, payload, embeddedTime, doubleHashOfAddressData의 뒤쪽 32byte)를 로컬에 저장한다.
  • (inventory hash의 개수 + inventory hash의 리스트)를 주위 노드들에게 보낸다.
    • 위에서 설명은 하나의 inventory hash를 만드는 것을 설명하였으나 이전에 저장되어 있는 다른 inventory hash가 있으면 한번에 보낸다.
    • (inventory hash의 개수 + inventory hash의 리스트)를 payload라 하자.
    • inventory hash는 길이가 32byte로 정해져 있으므로 리스트로부터 개수를 쉽게 알아낼 수 있다.
    • payload를 sha512한 값의 앞 4byte를 checksum이라 한다.
    • header = 0xE9BEB4D9 + command('inv') + payload_length + checksum
    • (header + payload)를 연결된 주위 노드들에게 보낸다.

inventory hash에 대한 응답 보내기

  • 'getdata'를 'inv'에 대한 응답으로 보낸다.
  • 'inv'로부터 받은 내용을 그대로 되돌려 보낸다.

public key 저장하기

  • 'object'로부터 payload를 받는다.
  • nonce, time, object type은 건너뛴다.
  • address version number, stream number를 뽑아낸다.
  • tag를 확인한다.
    • 내가 'getdata'를 요청한 public key인지를 확인한다.
  • payload의 encryption과 signature를 통해 정상적인지를 확인한다.
  • (address, address version number, storedData)를 저장한다.
    • storedData는 payload로부터 (address version number + stream number + 주소의 bitfield 계산 + public signing key + public encryption key + nonceTrialsPerByte + payloadLengthExtraBytes)를 뽑아내어 따로 가지고 있는 값이다.

subscribe와 unsubscribe

  • subscribe이면 address와 label을 입력받아서 (address, label, 구독 여부)를 저장한다.
  • unsubscribe이면 address를 입력 받아서 지운다.
  • 주소를 만든이가 메시지를 broadcast로 보내면 subscribe한 이들은 받아볼 수 있다.(broadcast는 주소를 알면 decrypt할 수 있다.)

channel 만들기

  • 채널 만들기
    • 채널 이름을 정한다.
    • 그 이름을 passphrase로 해서 주소를 생성한다.
  • 채널 가입하기
    • 채널의 주소와 이름을 입력한다.
    • 이름으로 주소를 만들어서 입력된 주소와 같은지 확인한다.
  • 상대방의 주소로 메시지를 보내는 것과 거의 같은 형태로 메시지를 보낸다.
    • 메시지를 보낼 때 상대방 주소가 내 주소와 같을 것이다.
    • POW를 할지 말지 확인하지 않고 항상 POW를 한다.
    • ack를 empty string으로 보낸다.

메시지 보내기

  • (toAddress, fromAddress, subject, message)를 입력 받는다.
  • TTL은 기본 (4일-5분)과 (4일 + 5분)사이로 설정한다.
  • random으로 ackdata를 생성한다.
  • ('', toAddress, toRipe, fromAddress, subject, message, ackdata, sent time, last action time, 0, 'msgqueued', 0, 'sent', 2, TTL)
  • toAddress의 public key가 있는지 확인한다.
    • public key는 한번이라도 이 주소로 메시지를 보낸적이 있으면 그에 관련된 로컬에 저장되어 있거나 사용한적이 없으면 inv를 통한 내부적으로 inventory에 저장되어 있을 수 있다. 그러므로 두군데를 모두 확인해 보아야 한다.
    • 없으면 'msgqueued'를 'doingpubkeypow'로 바꾸고 inv(with getpubkey)를 요청한다.
    • 이전에 inv 요청을 했으면 'awaitingpubkey'로 바꾼다.
    • 있으면 'msgqueued'를 'doingmsgpow'로 변경한다.
  • public key로부터 POW를 진행한다.
    • 로컬 설정에 지정된 maxacceptablenoncetrialsperbyte나 maxacceptablepayloadlengthextrabytes를 넘어서는 값을 가지고 있으면 'doingmsgpow'를 'toodifficult'로 바꾸고 메시지를 보내지 않는다.
  • payload = encodeVarint(fromAddressVersionNumber) + encodeVarint(fromStreamNumber) + behaviour bitfield(1) + pubSigningKey + pubEncryptionKey + encodeVarint(noncetrialsperbyte) + encodeVarint(payloadlengthextrabytes) + toRipe + SIMPLE(0x02) + encodeVarint(len(messageToTransmit)) + messageToTransmit + encodeVarint(len(fullAckPayload)) + fullAckPayload + encodeVarint(len(signature)) + signature
    • fullAckPayload = (현재시간 + TTL) + 0x00000002 + encodeVarint(1) + encodeVarint(toStreamNumber) + ackdata
    • POW로 trialValue와 nonce를 얻은 후 앞에 nonce를 추가한다.
    • fullAckPayload를 가지고 object를 생성한다.
    • ack를 위한 data가 자체로 object가 되기 때문에 메시지를 받은 노드는 이 object를 그대로 리턴하면 된다.
    • signature는 아래의 방법으로 계산한다.
    • dataToSign = (현재시간 + TTL) + 0x00000002 + encodeVarint(1) + encodeVarint(toStreamNumber) + payload
    • private signing key로 dataToSign을 signing한다.
  • encrypted = payload를 public encryption key로 encrypt한다.
  • encryptedPayload = (현재시간 + TTL) + 0x00000002 + encodeVarint(1) + encodeVarint(toStreamNumber) + encrypted
  • POW로 trialValue와 nonce를 얻는다.
  • encryptedPayload = nonce + encryptedPayload
  • encryptedPayload로부터 inventory hash를 계산하고 inv(with msg)를 보낸다.
  • 메시지를 보내고 나면 'doingmsgpow'를 'msgsent'로 바꾼다.

broadcast 보내기

  • (fromAddress, subject, message)를 입력 받는다.
  • TTL은 기본 (4일-5분)과 (4일 + 5분)사이로 설정한다.
  • random으로 ackdata를 생성한다.
  • ('', '[Broadcast subscribers]', toRipe, fromAddress, subject, message, ackdata, sent time, last action time, 0, 'broadcastqueued', 0, 'sent', 2, TTL)를 로컬에 저장한다.
  • payload = (현재시간 + TTL) + 0x00000003 + broadcast version(5)을 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + doubleHashOfAddressData의 뒤 32byte
  • dataToEncrypt = addressVersionNumber를 숫자 인코딩한 값 + streamNumber를 숫자 인코딩한 값 + behaviour bitfield(1) + public signing key + public encryption key + nonceTrialsPerByte를 숫자 인코딩한 값 + payloadLengthExtraBytes를 숫자 인코딩한 값 + 0x02 + message length를 숫자 인코딩한 값 + message
    • message = ('Subject:' + subject + '\n' + 'Body:' + message)
  • (payload + dataToEncrypt)를 private signing key로 signing을 해서 signature를 얻는다.
  • dataToEncrypt = dataToEncrypt + signature의 길이를 숫자 인코딩한 값 + signature
    • dataToEncrypt를 encryption할 때 사용하기 위해 doubleHashOfAddressData의 앞쪽 32byte를 private key로 하는 public/private key 쌍을 만든다. message를 encryption할 때 사용하는 것과 같은 알고리즘인 ECDSA로 public key를 만든다.
  • dataToEncrypt를 앞의 public key를 사용해서 ECDSA로 encryption을 한다. encryption된 값을 payload에 추가한다.
    • 받는 노드에서 주소를 알고 있다면 이 주소의 double hash를 통해 private key를 알아낼 수 있고 decrypt할 수 있게 된다.
  • POW를 실행하여 trialValue와 nonce를 얻는다.
  • payload의 앞에 nonce를 숫자로 인코딩한 값을 추가한다.
  • 현재까지의 전체 payload로부터 inventory hash를 만든다.
    • inventory hash = payload에 sha512를 두번 적용하여 나온 값의 앞 32byte
  • inventory hash를 키로하여 (objectType(3), streamNumber, payload, embeddedTime, doubleHashOfAddressData의 뒤쪽 32byte)를 로컬에 저장한다.
  • 앞에서 로컬에 저장한 값중 아래 내용을 변경한다.
    • last action time
    • broadcastqueued -> broadcastsent
    • 제일 앞에 비워두었던 값에는 inventory hash를 넣는다.

메시지에 파일 첨부하기

  • message와 파일의 구분을 "\n\n"으로 한다. 여러 파일을 첨부하는 경우에도 파일간의 구분을 "\n\n"으로 한다.
  • 파일의 data는 base64로 인코딩한다.
  • 파일이 이미지인 경우와 그외의 경우로 구분한다.
  • 이미지인 경우 아래와 같은 tag의 형태로 첨부한다: fileName, fileSize, fileName, filetype, data
    • <!-- Note: Image attachment below. Please use the right click "View HTML code ..." option to view it. -->
    • <!-- Sent using Bitmessage Daemon. https://github.com/Dokument/PyBitmessage-Daemon -->
    • Filename:%s
    • Filesize:%sKB
    • Encoding:base64
    • <center>
    • <div id="image">
    • <img alt = "%s" src='data:image/%s;base64, %s' />
    • </div>
    • </center>
  • 이미지가 아닌 경우: fileName, fileSize, fileName, filetype, data
    • <!-- Note: File attachment below. Please use a base64 decoder, or Daemon, to save it. -->
    • <!-- Sent using Bitmessage Daemon. https://github.com/Dokument/PyBitmessage-Daemon -->
    • Filename:%s
    • Filesize:%sKB
    • Encoding:base64
    • <attachment alt = "%s" src='data:file/%s;base64, %s' />

에러 메시지

  • fatal + ban time + inventory vector의 길이 + inventory vector + error text의 길이 + error text
  • inventory vector와 error text는 string 그대로 사용하고 나머지는 숫자 인코딩을 한다.

POW 계산하기

  • target = 2^64 / (requiredAverageProofOfWorkNonceTrialsPerByte(len(encryptedPayload) + 8 + requiredPayloadLengthExtraBytes + ((TTL(len(encryptedPayload)+8+requiredPayloadLengthExtraBytes))/(2 ** 16))))
  • initialHash = payload의 sha512
  • (nonce(8byte) + initialHash)를 sha512를 두번 적용하여 나온 값을 trialValue라 하자. nonce는 1부터 1씩 증가시켜서 계산한다.
  • trialValue가 target보다 작은 경우가 결과가 된다.
  • 이때의 (trialValue, nonce)를 결과로 내보낸다.

숫자 인코딩하기

  • 숫자 값이 어느 영역에 속하느냐에 따라 인코딩을 한다.
  • big-endian을 사용한다.
  • 0 <= ... < 253 : unsigned char(1)
  • < 65536(2^16) : 253(0xfd, unsigned char) + unsigned short(2)
  • < 4294967296(2^32) : 254(0xfe, unsigned char) + unsigned int(4)
  • < 18446744073709551616(2^64) : 255(0xff, unsigned char) + unsigned long long(8)

댓글 없음:

댓글 쓰기

Building asynchronous views in SwiftUI 정리

Handling loading states within SwiftUI views self loading views View model 사용하기 Combine을 사용한 AnyPublisher Making SwiftUI views refreshable r...