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' : 받은 데이터에 문제가 있는 경우 에러 메시지로 응답한다.
- 'getpubkey': public key를 요청한다.
- 'pubkey': public key를 보낸다.
첫 연결시 보내는 정보
- 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와만 연결이 가능하다.
- 이 값을 받은 노드는 외부에서 알고있는 자신의 IP를 알 수 있다.
- 이 값을 통해 상대방이 누구인지 알 수 있다.(예: 같은 프로그램인데 버전이 높다면 업그레이드 메시지를 사용자에게 보여줄 수 있다.)
주위 노드들에게 연결된 노드의 정보를 알리기
- 노드의 수 + (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를 만들어서 과정을 반복한다.
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)를 연결된 주위 노드들에게 보낸다.
- TTL을 (28일 +- 5분)으로 한다.
- doubleHashOfAddressData = (address version number를 숫자 인코딩한 값 + stream number를 숫자 인코딩한 값 + hash)에 sha512를 두번 적용한 값
- WIF로 저장된 주소로부터 public key 값을 가져온다.
- WIF로부터 가져온 key값은 0x80을 앞에 가지고 있으므로 이를 뺴주어야 한다.
- 받는 노드에서 주소를 알고 있다면 이 주소의 double hash를 통해 private key를 알아낼 수 있고 decrypt할 수 있게 된다.
- inventory hash = payload에 sha512를 두번 적용하여 나온 값의 앞 32byte
- 위에서 설명은 하나의 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)를 뽑아내어 따로 가지고 있는 값이다.
- 내가 'getdata'를 요청한 public key인지를 확인한다.
- 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으로 보낸다.
- 채널 이름을 정한다.
- 그 이름을 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'로 바꾼다.
- public key는 한번이라도 이 주소로 메시지를 보낸적이 있으면 그에 관련된 로컬에 저장되어 있거나 사용한적이 없으면 inv를 통한 내부적으로 inventory에 저장되어 있을 수 있다. 그러므로 두군데를 모두 확인해 보아야 한다.
- 없으면 'msgqueued'를 'doingpubkeypow'로 바꾸고 inv(with getpubkey)를 요청한다.
- 이전에 inv 요청을 했으면 'awaitingpubkey'로 바꾼다.
- 있으면 'msgqueued'를 'doingmsgpow'로 변경한다.
- 로컬 설정에 지정된 maxacceptablenoncetrialsperbyte나 maxacceptablepayloadlengthextrabytes를 넘어서는 값을 가지고 있으면 'doingmsgpow'를 'toodifficult'로 바꾸고 메시지를 보내지 않는다.
- 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한다.
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 = ('Subject:' + subject + '\n' + 'Body:' + message)
- dataToEncrypt를 encryption할 때 사용하기 위해 doubleHashOfAddressData의 앞쪽 32byte를 private key로 하는 public/private key 쌍을 만든다. message를 encryption할 때 사용하는 것과 같은 알고리즘인 ECDSA로 public key를 만든다.
- 받는 노드에서 주소를 알고 있다면 이 주소의 double hash를 통해 private key를 알아낼 수 있고 decrypt할 수 있게 된다.
- inventory hash = payload에 sha512를 두번 적용하여 나온 값의 앞 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' />
- <!-- 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>
- <!-- 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' />
댓글 없음:
댓글 쓰기