Skip to main content

Overview

The Memo utility provides AES encryption and decryption for private messages between Hive users. It uses ECDH (Elliptic Curve Diffie-Hellman) key exchange to derive shared secrets and AES encryption for secure communication. Messages must start with # to be encrypted/decrypted. Plain text messages (without #) are returned unchanged.

Methods

encode

Encrypts a memo using AES encryption for secure private messaging on Hive.
const encryptedMemo = Memo.encode(privateKey, publicKey, memo, testNonce?)
privateKey
string | PrivateKey
required
Sender’s private memo key (WIF string or PrivateKey instance)
publicKey
string | PublicKey
required
Recipient’s public memo key (string or PublicKey instance)
memo
string
required
Message to encrypt. Must start with # for encryption. Plain text (without #) is returned unchanged.
testNonce
any
Optional nonce for testing purposes (advanced usage)
returns
string
Encrypted memo string prefixed with #, or original string if not encrypted

Example

import { Memo, PrivateKey, PublicKey } from 'hive-tx'

// Alice's memo key
const alicePrivate = PrivateKey.fromLogin('alice', 'password', 'memo')

// Bob's memo key (public)
const bobPrivate = PrivateKey.fromLogin('bob', 'password', 'memo')
const bobPublic = bobPrivate.createPublic()

// Encrypt message from Alice to Bob
const message = '#Hello Bob, this is a secret message!'
const encrypted = Memo.encode(alicePrivate, bobPublic, message)

console.log('Encrypted:', encrypted)
// Output: #base58_encoded_encrypted_data...

// Plain text message (no encryption)
const plainText = Memo.encode(alicePrivate, bobPublic, 'Public message')
console.log('Plain text:', plainText)
// Output: Public message

decode

Decrypts an encrypted memo using AES decryption.
const decryptedMemo = Memo.decode(privateKey, memo)
privateKey
string | PrivateKey
required
Recipient’s private memo key (WIF string or PrivateKey instance)
memo
string
required
Encrypted memo string. Must start with # for decryption. Plain text (without #) is returned unchanged.
returns
string
Decrypted memo content with # prefix, or original string if not encrypted

Example

import { Memo, PrivateKey } from 'hive-tx'

// Bob receives an encrypted memo
const bobPrivate = PrivateKey.fromLogin('bob', 'password', 'memo')

// Encrypted memo received in a transfer
const encryptedMemo = '#4LmkPLQKBkfCHhfJ...' // From transaction

// Decrypt the memo
const decrypted = Memo.decode(bobPrivate, encryptedMemo)
console.log('Decrypted:', decrypted)
// Output: #Hello Bob, this is a secret message!

// Plain text memo (no decryption needed)
const plainText = Memo.decode(bobPrivate, 'Public message')
console.log('Plain text:', plainText)
// Output: Public message

Complete Example: Encrypted Transfer

import { Transaction, PrivateKey, Memo } from 'hive-tx'

async function sendEncryptedTransfer() {
  // Alice's keys
  const aliceActiveKey = PrivateKey.fromLogin('alice', 'password', 'active')
  const aliceMemoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
  
  // Bob's public memo key (get from blockchain or derive)
  const bobMemoKey = PrivateKey.fromLogin('bob', 'bobpassword', 'memo')
  const bobPublicMemo = bobMemoKey.createPublic()
  
  // Encrypt the memo
  const message = '#Payment for web development services - Invoice #12345'
  const encryptedMemo = Memo.encode(aliceMemoKey, bobPublicMemo, message)
  
  // Create transfer with encrypted memo
  const tx = new Transaction()
  await tx.addOperation('transfer', {
    from: 'alice',
    to: 'bob',
    amount: '50.000 HIVE',
    memo: encryptedMemo
  })
  
  // Sign with active key
  tx.sign(aliceActiveKey)
  
  // Broadcast
  const result = await tx.broadcast()
  console.log('Transfer sent:', result.tx_id)
  console.log('Encrypted memo:', encryptedMemo)
}

sendEncryptedTransfer()

Decrypting Received Memos

import { Memo, PrivateKey } from 'hive-tx'
import { Client } from '@hiveio/dhive'

const client = new Client(['https://api.hive.blog'])

async function readEncryptedMemos(username: string, memoKey: PrivateKey) {
  // Get recent account history
  const history = await client.database.getAccountHistory(username, -1, 100)
  
  // Filter for transfers to this account
  const transfers = history.filter(([, op]) => {
    return op[0] === 'transfer' && op[1].to === username
  })
  
  // Decrypt memos
  for (const [, op] of transfers) {
    const transfer = op[1]
    
    if (transfer.memo.startsWith('#')) {
      try {
        const decrypted = Memo.decode(memoKey, transfer.memo)
        console.log('From:', transfer.from)
        console.log('Amount:', transfer.amount)
        console.log('Memo:', decrypted)
      } catch (error) {
        console.error('Failed to decrypt memo:', error.message)
      }
    } else {
      console.log('Plain text memo:', transfer.memo)
    }
  }
}

// Usage
const bobMemoKey = PrivateKey.fromLogin('bob', 'password', 'memo')
readEncryptedMemos('bob', bobMemoKey)

Bidirectional Encryption

Memos can be decrypted by both sender and recipient:
import { Memo, PrivateKey } from 'hive-tx'

// Alice's keys
const aliceMemoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
const alicePublicMemo = aliceMemoKey.createPublic()

// Bob's keys
const bobMemoKey = PrivateKey.fromLogin('bob', 'password', 'memo')
const bobPublicMemo = bobMemoKey.createPublic()

// Alice encrypts message to Bob
const message = '#Secret meeting at noon'
const encrypted = Memo.encode(aliceMemoKey, bobPublicMemo, message)

console.log('Encrypted:', encrypted)

// Bob decrypts (as recipient)
const bobDecrypted = Memo.decode(bobMemoKey, encrypted)
console.log('Bob reads:', bobDecrypted)
// Output: #Secret meeting at noon

// Alice can also decrypt her own sent message
const aliceDecrypted = Memo.decode(aliceMemoKey, encrypted)
console.log('Alice reads:', aliceDecrypted)
// Output: #Secret meeting at noon

Unicode Support

Memos fully support Unicode characters:
import { Memo, PrivateKey } from 'hive-tx'

const aliceMemoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
const bobMemoKey = PrivateKey.fromLogin('bob', 'password', 'memo')
const bobPublicMemo = bobMemoKey.createPublic()

// Messages with Unicode
const messages = [
  '#Hello 世界 🌍',
  '#Café ☕',
  '#Привет мир',
  '#مرحبا بالعالم',
  '#🚀 To the moon! 🌙'
]

for (const message of messages) {
  const encrypted = Memo.encode(aliceMemoKey, bobPublicMemo, message)
  const decrypted = Memo.decode(bobMemoKey, encrypted)
  
  console.log('Original:', message)
  console.log('Encrypted:', encrypted)
  console.log('Decrypted:', decrypted)
  console.log('Match:', message === decrypted)
  console.log('---')
}

Fetching Public Memo Keys

To encrypt a memo, you need the recipient’s public memo key:
import { Memo, PrivateKey, PublicKey } from 'hive-tx'
import { Client } from '@hiveio/dhive'

const client = new Client(['https://api.hive.blog'])

async function encryptMemoForUser(
  senderMemoKey: PrivateKey,
  recipientUsername: string,
  message: string
): Promise<string> {
  // Fetch recipient's account data
  const [account] = await client.database.getAccounts([recipientUsername])
  if (!account) {
    throw new Error(`Account ${recipientUsername} not found`)
  }
  
  // Get memo key from account
  const recipientPublicMemo = PublicKey.fromString(account.memo_key)
  
  // Encrypt message
  const encrypted = Memo.encode(senderMemoKey, recipientPublicMemo, message)
  
  return encrypted
}

// Usage
const aliceMemoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
const encrypted = await encryptMemoForUser(
  aliceMemoKey,
  'bob',
  '#Private message for Bob'
)
console.log('Encrypted memo:', encrypted)

Error Handling

import { Memo, PrivateKey } from 'hive-tx'

const memoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
const bobPublicKey = PrivateKey.fromLogin('bob', 'password', 'memo').createPublic()

try {
  // Encryption error (environment check)
  const encrypted = Memo.encode(memoKey, bobPublicKey, '#Test')
  console.log('Encrypted successfully')
} catch (error) {
  console.error('Encryption failed:', error.message)
  // Might output: This environment does not support encryption.
}

try {
  // Decryption error (wrong key or corrupted data)
  const corrupted = '#InvalidBase58Data'
  const decrypted = Memo.decode(memoKey, corrupted)
} catch (error) {
  console.error('Decryption failed:', error.message)
}

Plain Text vs Encrypted

Memos without the # prefix are treated as plain text:
import { Memo, PrivateKey } from 'hive-tx'

const memoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
const bobPublicKey = PrivateKey.fromLogin('bob', 'password', 'memo').createPublic()

// Encrypted (starts with #)
const encrypted = Memo.encode(memoKey, bobPublicKey, '#Private message')
console.log('Encrypted:', encrypted.startsWith('#')) // true
console.log('Length:', encrypted.length) // Long base58 string

// Plain text (no #)
const plainText = Memo.encode(memoKey, bobPublicKey, 'Public message')
console.log('Plain text:', plainText) // 'Public message'
console.log('Unchanged:', plainText === 'Public message') // true

// Decoding plain text returns as-is
const decoded = Memo.decode(memoKey, 'Public message')
console.log('Decoded:', decoded) // 'Public message'

Security Considerations

Memo Security Best Practices:
  1. Use memo keys only: Never use active or owner keys for memo encryption
  2. Memo key compromise: If your memo key is compromised, past encrypted memos can be decrypted
  3. Public visibility: Encrypted memos are visible on the blockchain, only the content is encrypted
  4. Key management: Store memo private keys securely, separate from other keys
  5. Sensitive data: Avoid putting highly sensitive data in memos; they’re not as secure as off-chain encryption

Secure Memo Example

import { Memo, PrivateKey } from 'hive-tx'

// ✅ GOOD: Use memo key for encryption
const memoKey = PrivateKey.fromLogin('alice', 'password', 'memo')
const bobMemoPublic = PrivateKey.fromLogin('bob', 'password', 'memo').createPublic()
const encrypted = Memo.encode(memoKey, bobMemoPublic, '#Secret message')

// ❌ BAD: Using active key for memos
const activeKey = PrivateKey.fromLogin('alice', 'password', 'active')
// Don't do this - use dedicated memo keys

How It Works

Memo encryption uses the following process:
  1. Shared Secret: ECDH key exchange between sender’s private memo key and recipient’s public memo key creates a shared secret
  2. AES Encryption: The message is encrypted with AES using the shared secret as the key
  3. Nonce: A random nonce ensures different ciphertexts for identical messages
  4. Checksum: A checksum is included to detect corruption
  5. Base58 Encoding: The encrypted data is encoded in Base58 for transmission
  6. Format: #[base58_encoded(from_key + to_key + nonce + checksum + encrypted_message)]

Type Definition

type Memo = {
  encode(
    privateKey: string | PrivateKey,
    publicKey: string | PublicKey,
    memo: string,
    testNonce?: any
  ): string

  decode(privateKey: string | PrivateKey, memo: string): string
}

See Also

Build docs developers (and LLMs) love