fix(auth): rewrite memo encryption to spec, interop with steem-js#16
Merged
Conversation
The memo encryption had two compounding defects that together made it
neither secure nor interoperable:
1. deriveSharedSecret computed SHA-256(privBytes || pubBytes), which is
not an ECDH key agreement — it is not symmetric. The sender derived
SHA-256(privS||pubR) while the recipient derived SHA-256(privR||pubS),
so the real recipient could never decrypt a memo addressed to them.
The existing tests passed only because they used the same WIF for
both encrypt and decrypt (self-to-self), hiding the asymmetry.
2. The wire format, key derivation, checksum, and plaintext framing were
all custom and did not match the canonical Steem memo layout, so even
with a correct key agreement the output could not interoperate with
steem-js memo.encode / memo.decode.
Rewrite auth/memo to the canonical algorithm verified against steem-js
(next @ d3945f1):
x = ECDH(priv, pub) # secp256k1 shared X coordinate
S = SHA512(x) # 64 bytes
ek = SHA512(uint64_le(nonce) || S)
key = ek[0:32]; iv = ek[32:48]
check = uint32_le(SHA256(ek)[0:4])
plaintext = varint32(len) || utf8(message) # bytebuffer writeVString
ciphertext = AES-256-CBC(key, iv, PKCS7(plaintext))
wire = from(33) || to(33) || nonce(uint64 LE) || check(uint32 LE) ||
varint32(len(ct)) || ct
ECDH uses secp256k1.GenerateSharedSecret from decred dcrec/secp256k1/v4
(already a dependency); no type conversion is needed because
btcec.PrivateKey is a type alias for secp256k1.PrivateKey.
The public Encode/Decode signatures are unchanged (consumed by
steemgosdk); EncodeWithNonce is added for deterministic tests, and
UniqueNonce mirrors steem-js Aes.uniqueNonce (time<<32 | random32).
Tests now include a vector generated from steem-js asserting byte-for-
byte equality of Go EncodeWithNonce vs steem-js memo.encode, cross-key
encryption (sender encrypts, recipient decrypts — the path the bug
broke), steem-js-memo decoding, tamper/wrong-key rejection, and a
hand-decoded wire-layout check.
Refs: steem-audit/projects/steemutil/audit-reports/2026-07-code-scan.md (finding [高])
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes the [高] memo encryption finding in
steem-audit/projects/steemutil/audit-reports/2026-07-code-scan.md:deriveSharedSecretwasSHA-256(privBytes ‖ pubBytes), not ECDH — so it was not symmetric, and the real recipient could never decrypt a memo addressed to them. The old tests passed only because they used the same WIF for both encrypt and decrypt (self-to-self), hiding the bug.The audit report said "just swap to real ECDH", but in fact four layers were wrong and had to be rewritten to reach interoperability: key derivation (SHA-512, not SHA-256), plaintext framing (writeVString varint prefix), checksum, and the entire wire format (steem-js canonical
EncryptedMemolayout, not the previous custom 1-byte-length-prefixed format).Fix
Rewritten to the canonical algorithm, verified byte-for-byte against steem-js
next@d3945f1(src/memo+src/auth/ecc/src/aes):secp256k1.GenerateSharedSecret(decreddcrec/secp256k1/v4, already a dep). No type conversion needed:btcec.PrivateKeyis a type alias forsecp256k1.PrivateKey.Encode/Decodesignatures unchanged (consumed by steemgosdk). AddedEncodeWithNonce(deterministic tests) andUniqueNonce(mirrors steem-jsAes.uniqueNonce:time<<32 ‖ random32).Verification
Test vectors were generated by running steem-js locally with fixed
(senderWif, recipientPub, memo, nonce); the generator script was temporary and removed (steem-js tree left clean).TestEncodeByteEqualityWithSteemJS— GoEncodeWithNonceoutput is identical to steem-jsmemo.encodefor the same inputs.TestCrossKeyEncryption— sender encrypts, recipient decrypts (the path the bug broke), both directions.TestDecodeSteemJSMemo— Go decodes a memo produced by steem-js.TestDecodeRejectsWrongKey/TestDecodeRejectsTamperedMemo— fail-closed.TestWireFormatLayout— hand-decodes the wire and re-serializes for byte equality.Refs: steem-audit/projects/steemutil/audit-reports/2026-07-code-scan.md