Skip to main content
The Memo class provides secure message encryption for private communication between Hive users. This guide shows you how to encrypt and decrypt memos using AES encryption with ECDH key exchange.

Understanding Memo Encryption

Hive uses the following encryption scheme for memos:
  1. ECDH Key Exchange: Compute shared secret from sender’s private key and recipient’s public key
  2. AES Encryption: Encrypt message using the shared secret
  3. Base58 Encoding: Encode encrypted data for transmission
Memos must start with # to be encrypted. Messages without # are sent as plain text.

Basic Memo Encryption

1

Import required classes

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

Get sender's private key and recipient's public key

// Sender's memo private key
const senderPrivateKey = PrivateKey.fromLogin('alice', 'password', 'memo')

// Recipient's memo public key (get from blockchain)
const recipientPublicKey = PublicKey.fromString(
  'STM8m5UgaFAAYQRuaNejYdS8FVLVp9Ss3K1qAVk5de6F8s3HnVbvA'
)
3

Encrypt the message

const message = '#This is a secret message'
const encrypted = Memo.encode(
  senderPrivateKey,
  recipientPublicKey,
  message
)

console.log(encrypted)
// Output: #7vR3...9xKp (long base58 string)
4

Use encrypted memo in transfer

import { Transaction } from 'hive-tx'

const tx = new Transaction()
await tx.addOperation('transfer', {
  from: 'alice',
  to: 'bob',
  amount: '1.000 HIVE',
  memo: encrypted  // Encrypted memo
})

Decrypting Memos

The recipient can decrypt the memo using their private memo key:
import { Memo, PrivateKey } from 'hive-tx'

// Recipient's private memo key
const recipientPrivateKey = PrivateKey.fromLogin('bob', 'password', 'memo')

// Encrypted memo received in transfer
const encryptedMemo = '#7vR3...9xKp'

// Decrypt
const decrypted = Memo.decode(recipientPrivateKey, encryptedMemo)
console.log(decrypted)
// Output: #This is a secret message
The decrypted message includes the # prefix. Remove it if needed: decrypted.substring(1)

Plain Text Memos

Messages without # prefix are not encrypted:
import { Memo } from 'hive-tx'

const plainMemo = 'Public payment note'

// Encode returns the message unchanged (no encryption)
const result = Memo.encode(privateKey, publicKey, plainMemo)
console.log(result) // 'Public payment note'

// Decode also returns unchanged
const decoded = Memo.decode(privateKey, plainMemo)
console.log(decoded) // 'Public payment note'

Complete Transfer with Encrypted Memo

Here’s a complete example of sending a transfer with an encrypted memo:
import { Transaction, PrivateKey, PublicKey, Memo, callRPC } from 'hive-tx'

async function sendPrivateTransfer(
  from: string,
  to: string,
  amount: string,
  message: string,
  senderPassword: string
) {
  // Get sender's keys
  const activeKey = PrivateKey.fromLogin(from, senderPassword, 'active')
  const memoKey = PrivateKey.fromLogin(from, senderPassword, 'memo')
  
  // Get recipient's public memo key from blockchain
  const accounts = await callRPC('condenser_api.get_accounts', [[to]])
  const recipientMemoKey = PublicKey.fromString(accounts[0].memo_key)
  
  // Encrypt the message
  const encryptedMemo = Memo.encode(
    memoKey,
    recipientMemoKey,
    '#' + message  // Add # prefix for encryption
  )
  
  // Create and send transaction
  const tx = new Transaction()
  await tx.addOperation('transfer', {
    from,
    to,
    amount,
    memo: encryptedMemo
  })
  
  tx.sign(activeKey)
  const result = await tx.broadcast(true)
  
  return result.tx_id
}

// Usage
const txId = await sendPrivateTransfer(
  'alice',
  'bob',
  '5.000 HIVE',
  'Payment for consulting services',
  'alice-password'
)

console.log('Transfer sent:', txId)

Decrypting Received Memos

Retrieve and decrypt memos from incoming transfers:
import { callRPC, Memo, PrivateKey } from 'hive-tx'

async function getPrivateTransfers(
  account: string,
  password: string,
  limit: number = 100
) {
  // Get account history
  const history = await callRPC('condenser_api.get_account_history', [
    account,
    -1,    // start from most recent
    limit
  ])
  
  // Get memo private key
  const memoKey = PrivateKey.fromLogin(account, password, 'memo')
  
  // Filter and decrypt transfers
  const transfers = history
    .filter((item: any) => {
      return item[1].op[0] === 'transfer' &&
             item[1].op[1].to === account
    })
    .map((item: any) => {
      const op = item[1].op[1]
      let memo = op.memo
      
      // Decrypt if encrypted
      if (memo.startsWith('#')) {
        try {
          memo = Memo.decode(memoKey, memo)
          // Remove # prefix
          memo = memo.substring(1)
        } catch (error) {
          memo = '[Decryption failed]'
        }
      }
      
      return {
        from: op.from,
        amount: op.amount,
        memo,
        timestamp: item[1].timestamp
      }
    })
  
  return transfers
}

// Usage
const transfers = await getPrivateTransfers('bob', 'bob-password', 50)
transfers.forEach(t => {
  console.log(`${t.from} -> ${t.amount}: ${t.memo}`)
})

Working with Shared Secrets

Understand how shared secrets work under the hood:
import { PrivateKey, PublicKey } from 'hive-tx'

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

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

// Both parties can compute the same shared secret
const aliceShared = alicePrivate.getSharedSecret(bobPublic)
const bobShared = bobPrivate.getSharedSecret(alicePublic)

// These are identical
console.log('Secrets match:', 
  Buffer.from(aliceShared).equals(Buffer.from(bobShared))
) // true

console.log('Secret length:', aliceShared.length) // 64 bytes
The shared secret is a 64-byte hash computed using ECDH key exchange. The Memo class uses this internally for AES encryption.

Sender Can Also Decrypt

Both sender and recipient can decrypt the memo:
import { Memo, PrivateKey, PublicKey } from 'hive-tx'

const alicePrivate = PrivateKey.fromLogin('alice', 'password', 'memo')
const bobPrivate = PrivateKey.fromLogin('bob', 'password', 'memo')
const bobPublic = bobPrivate.createPublic()

// Alice encrypts
const encrypted = Memo.encode(alicePrivate, bobPublic, '#Secret message')

// Bob decrypts (recipient)
const bobDecrypted = Memo.decode(bobPrivate, encrypted)
console.log(bobDecrypted) // '#Secret message'

// Alice can also decrypt (sender)
const aliceDecrypted = Memo.decode(alicePrivate, encrypted)
console.log(aliceDecrypted) // '#Secret message'
This allows senders to keep records of encrypted messages they’ve sent.

Security Considerations

Memo keys are separate from posting/active keys. Always use memo keys for encryption, never posting or active keys.
Memos are stored on the blockchain forever. Even encrypted memos will be visible to anyone who has both the sender’s and recipient’s private keys.
Never send highly sensitive data like passwords, seed phrases, or financial credentials via memos, even encrypted.
For maximum privacy, consider using off-chain messaging systems for truly confidential communications.

Unicode and Special Characters

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

const privateKey = PrivateKey.from('5J...')
const publicKey = PublicKey.fromString('STM...')

// Unicode characters work
const encrypted = Memo.encode(
  privateKey,
  publicKey,
  '#Hello 世界! 🌍 Привет'
)

const decrypted = Memo.decode(privateKey, encrypted)
console.log(decrypted) // '#Hello 世界! 🌍 Привет'

Error Handling

Handle encryption and decryption errors:
import { Memo, PrivateKey, PublicKey } from 'hive-tx'

try {
  const encrypted = Memo.encode(
    PrivateKey.from('5J...'),
    PublicKey.fromString('STM...'),
    '#My message'
  )
} catch (error) {
  if (error.message.includes('does not support encryption')) {
    console.error('Encryption not available in this environment')
  } else {
    console.error('Encryption failed:', error.message)
  }
}

try {
  const decrypted = Memo.decode(
    PrivateKey.from('5J...'),
    '#corrupted-data'
  )
} catch (error) {
  console.error('Decryption failed:', error.message)
  // Memo might be encrypted with different keys
}

Testing Memo Encryption

The library includes a self-test for encryption:
import { Memo } from 'hive-tx'

// This runs automatically on first encode/decode call
// Tests encryption roundtrip with known keys
const testWif = '5JdeC9P7Pbd1uGdFVEsJ41EkEnADbbHGq6p1BwFxm6txNBsQnsw'
const testPubkey = 'STM8m5UgaFAAYQRuaNejYdS8FVLVp9Ss3K1qAVk5de6F8s3HnVbvA'

try {
  const encrypted = Memo.encode(testWif, testPubkey, '#memo爱')
  const decrypted = Memo.decode(testWif, encrypted)
  
  if (decrypted === '#memo爱') {
    console.log('Encryption test passed')
  }
} catch (error) {
  console.error('Encryption not supported in this environment')
}

Next Steps

Build docs developers (and LLMs) love