레이블이 BitMessage인 게시물을 표시합니다. 모든 게시물 표시
레이블이 BitMessage인 게시물을 표시합니다. 모든 게시물 표시

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)

2015년 6월 17일 수요일

BitMessage 분석

command, address version number, stream number, label, 개수, passphrase, hash값의 앞의 몇자리를 0x00으로 할 것인가, nonce trials per byte, payload length extra bytes
현재 최신 address version number는 4
nonce trials per byte는 현재 320이 최소, payload length exxtra bytes는 현재 1400이 최소

command: createChan, joinChan, createRandomAddress, createDeterministicAddress, getDeterministicAddress

address 만들기
1. 32bytes random으로 private signing key와 private encryption key를 만든다.
2. Elliptic Curve DSA 알고리즘 사용하여 public key 생성한다. 64bytes가 만들어진다.
3. 앞에서 생성된 두개(signing과 encryption)의 public key를 앞뒤로 붙인후(public signing key + public encryption key) sha512에 의한 hash를 구한다.
4. 구해진 digest를 가지고 ripemd160에 의한 hash를 구한다.
5. ripe.digest의 앞자리(1 or 2)가 0x00일 때까지 반복한다.(address의 크기를 줄이기 위한 작업)
6. address version number의 인코딩 + stream number의 인코딩 + ripe.digest
7. verify를 위해 앞의 값에 sha512를 두번 적용한 후 앞의 4bytes를 checksum으로 사용한다.
8. (6에서의 값의 hex + 7에서의 checksum의 hex) 를 integer로 변경한다.
9. integer를 base58로 인코딩한다.
10. 앞에 BitMessage를 의미하는 BM-를 덧붙인다.

양의 정수 인코딩하기
1. 253보다 작으면 big endian unsigned char
2. 253에서 65535(2^16)사이이면 (>B, 253) + >H unsigned short
3. 65536에서 2^32사이이면 (>B, 254) + >I unsigned int
4. 2^32에서 2^64사이이면 (>B, 255) + >Q unsigned long long
5. 2^64보다 큰 수는 없다.

Wallet Import Format
1. 0x80 + private key
2. sha256을 두번 적용한 hash를 구한다.
3. hash된 결과의 앞 4bytes를 checksum으로 사용한다.
4. 1의 끝에 checksum을 추가한다.
5. base58로 인코딩한다.

signature 계산
expires time + object type + address version number + stream number + tag + 0x00000001 + public signing key + public encryption key + nonce trials per byte + payload length extra bytes
 위의 내용을 private signing key로 sign한 것이 signature이다.

encrypted 계산
0x00000001 + public signing key + public encryption key + nonce trials per byte + payload length extra bytes + signature의 length + signature
위의 내용을 address의 double hash의 앞 32bytes로 암호화 한다.

실제 다른 node에 전달되는 값은 아래와 같다.
payload = POW에 의해 계산된 nonce + expires time + object type + address version number + stream number + tag + encrypted

inventoryhash: payload에 sha512를 두번 적용한 hash의 앞 32bytes
inventoryhash -> object type, stream number, payload, expires time, tag)

Generic interfaces 요점

 https://go.dev/blog/generic-interfaces  Generic interface를 정의할 때 최소한의 제약만을 정의하고 실제 구현체들이 자신만의 필요한 제약을 추가할 수 있도록 하는 것이 좋다. pointer receiver를...