每天进步一点点:终于搞定了memo加密/解密

经过了几个昼夜的学习和测试,终于搞定了memo的加密解密。原本以为这块不会有多复杂,但是实际沉浸下去才发现涉及的内容还很多。


(图源 :pixabay)

涉及内容

简单回顾一下涉及的主要内容:

  • 理论基础:Pub(Alice) * Priv(Bob) = Pub(Bob) * Priv(Alice)
  • 内容加密:AES (Advanced Encryption Standard),CBC模式
  • nonce、check:打包时字节序 / Byte Order的问题
  • 打包长内容时长度编码的问题:Varint编码

回头再看,可能都不是有多难,可是为了把他们弄明白,我还是下了一番苦功夫的,一调试就调试到下半夜。

MEMO“协议”

把这些都弄懂之后,自己写了程序能加密/解密/打包/解包MEMO了,然而发送给自己测试账户的钱包里的memo要么显示Invalid memo要么内容短缺一大块。

其实原因我是清楚的,涉及到通信,一个最重要的地方就是通信双方要依据相同的标准,这就是所谓的协议。我自己的打包解包都是自己的标准当然没问题,但是钱包中用的流程未必和我一样啊。

为了搞明白钱包中用的流程,我读了steem-python的代码/beem的代码/cli_wallet的代码,最终还是找到BM的一篇早期文章,才彻底搞懂。

BM文章中memo_data结构如下:

public_key from
public_key to
uint64_t nonce
uint32_t check
vector<char> encrypted

BM文章中核心代码如下:

shared_secret = from_private_key.get_shared_secret( to );
/// concatenate nonce and shared secret (binary)
encryption_key = sha512( nonce + shared_secret )
///< check is first 64 bit of sha256 hash treated as uint64_t truncated to 32 bits.
check = sha256( encryption_key )._hash[0]
/// pack the memo as a length-prefixed string, length is serialized as varint
plain_text = pack( memo_text)
encrypted = aes_encrypt( encryption_key, plain_text )

最后使用memo_data打包并转换成base58编码:

string result = '#' + to_base58( pack( memodata ) );

而我的代码之所以不被识别,是因为BM的代码中先对memo_text原文进行了一次处理,变成了加上varint前缀的二进制串;然后pack( memodata )再对encrypted数据加上varint前缀,我缺少了其中第一个步骤,所以内容总是没法别识别。

测试代码

有了上述理论基础以及通信双方遵循的协议,再实现起来就好办多了,当然,说是好办也不容易,测试N多次经历N次失败后,总算完成了两个函数:

encode_memo(wif, pk, message)
decode_memo(wif, message)

其中wif是账户的私钥,pk是账户的公钥。加密memo时,使用的是发送方私钥和接收方公钥,解密memo时可以使用任何一方私钥。

用如下代码测试一下:

message = "12345678"
memo_encoded = encode_memo(wif_test, receiver_pk, message)
print(memo_encoded)
print("decode with sender's memo private key: ", decode_memo(wif_test, memo_encoded))
print("decode with receiver's memo private key: ", decode_memo(wif_abc, memo_encoded))
client.transfer("oflyhigh.test", "oflyhigh.abc", "0.001 HBD", memo_encoded, wif_test_active)

我们会得到如下编码后的memo:

#C3JiuC9zrPkJRTK3bfz1HHJHvUuLPw4yiWS3bGYMABMvV84UYvmF7jqR58Hg5nor2F1uoSJjWMDsbuAjZJBxLyafj6qLqQcJW8hReTuLt48dpR7iG7kMWQr6VaQg4d7C4

分别用发送方和接收方私钥解密:

Reveal spoiler

image.png

Reveal spoiler

image.png

广播出去的transaction:

Reveal spoiler

image.png

可见,和普通的transaction无什么区别。在https://hiveblocks.com/ 查看一下:

Reveal spoiler

image.png

登录钱包查看一下,发现解密是正常的:

Reveal spoiler

image.png

长文本

另外,一个就是长文本的支持,既然可以用varint表示长度,理论上memo可以很长,然而我打算放一篇《出师表》进去,却被提示如下错误:

'Assert Exception:memo.size() < STEEM_MAX_MEMO_SIZE: Memo is too large'

查了一下代码,有如下定义:

#define STEEM_MAX_MEMO_SIZE 2048

所以长度不能太长哦,好在我试着放点其它文章还是没问题的,比如如下两条:

Reveal spoiler

image.png

Reveal spoiler

image.png

其实长文本和memo加密这块关系不大,不过想到哪就写到哪吧,记下来以免以后忘记了。

其它

代码中使用了手工encode/decode,这需要进一步改进,比如说消息前边有#号的自动进行encode处理,没有#号的则忽略。把这个功能放到transfer函数中,那么我们就可以使用如下方法直接进行了。

client.transfer("oflyhigh.test", "oflyhigh.abc", "0.001 HBD", "#Hello world", wif_test_active)

不过这样一改,transfer函数就有些复杂,比如我想传递明文的#文本,transfer就无能无力了,而现在的transfer是支持的。

Reveal spoiler

image.png

有意思的是,网页钱包反而把这个识别成为:Invalid memo

哎,还是老老实实按着标准来吧,凡是#号开头,一律先加密:)

相关链接