Research

Sending Secret and Anonymous Memos with Stellar

Post by
Paul Selden
Welcome, Lumenaut!
Oops! Something went wrong while submitting the form

One great thing about decentralized ledgers/blockchains is that the data you put there is immutable, public, and able to be seen/verified by anyone. One bad thing about decentralized ledgers/blockchains is that the data you put there is immutable, public, and able to be seen/verified by anyone.

On the Stellar network, one common place you’ll see data stored is in the memo field of a transaction, which allows you to store a very small bit of information (max 32 bytes) along with the transaction. The memo field is often used when sending to Stellar accounts that are shared by many users, such as exchange deposit addresses, so that the exchange can link your deposit to your actual exchange account. To do this they typically tell you to enter a memo with containing your exchange user id.

The problem with this approach is that it leaks data and hurts your privacy. If you have multiple Stellar accounts and make a deposit from each of them to your exchange account, someone doing analysis on Stellar transactions can see that multiple transactions share the same memo and probably infer that your two accounts are related in some way, even though you have not directly send transactions between them.

StellarGuard is committed to keeping you safe and secure while using Stellar, and that includes finding ways to improve privacy. I set out to find out a way to send memos using Stellar that meet the following requirements:

  1. The memo must be able to encode arbitrary text, not just ids
  2. The memos must be able to be decoded back to the original text, but only if you know some additional secret key
  3. The generated secret memo must fit in a single transaction (Stellar has limits on the size of a memo)
  4. The generated secret memo must be different every time it is created even if it is run multiple times with the same input
  5. The solution must require no additional storage per entry (one solution to the exchange problem would be to store a new id per deposit into a centralized database that maps back to the original user id)

Enter Stellar Secret Memos.

To solve the first two requirements, it was natural to look at encryption algorithms. Any of the commonly used encryption algorithms out there are able to encode text so that only those who know the secret key used to encrypt it are able to decrypt it. The last three requirements (randomization, no external storage, and memo size) are the interesting bits of the solution.

First I went looking for an encryption algorithm that doesn’t bloat the final memo size and that could support randomization. I didn’t have to look very far, because one of the most common algorithms, AES-256 in CTR mode, is a perfect fit. The final length of the encrypted data scales well with the size of the input, it is incredibly resistant to attack, and it can be used with a random initialization vector so that the encrypted data changes every time.

So with the encryption algorithm chosen, I set out to implement it, trying to fit as much space as possible in the memo. My first attempt was just to use “text” type Stellar memos, which normally can fit 28 characters of data, and encrypt the data into that. However, the encryption scheme would use up 8 characters of that space in the memo to encode the randomized initialization vector that makes it so the data changes every time it run, leaving you with only 20 characters left.

// data can be up to 20 characters to fit into the final 28 character text memo
const encryptedMemo = encrypt(data, secretKey).toString('utf8');
const memo = StellarSdk.Memo.text(encryptedMemo);

// decrypt it to get back the original memo
const originalData = decrypt(memo.value, secretKey);

While 20 characters seems reasonable, I knew I could do better. Stellar has a different memo type, “hash” memos, which contain exactly 32 bytes of hexadecimal characters. If we encoded the memo into hex, I could potentially add up to 4 more characters of maximum storage (we would still need to use 8 of them for the randomization aspect). Here was my first attempt:

const encryptedMemo = encrypt(data).toString('hex');
const memo = StellarSdk.Memo.hash(encryptedMemo);

> Error, “hash” type memo must be 32 bytes of hexadecimal characters

So while we could indeed fit more data into a hash memo, it would only work if the data size was exactly 24 characters, enough to make the final hash have exactly 32 bytes.

To solve that problem, I could pad the data with extra random bytes until it fit the required size. However doing that causes another problem: we won’t know which bits of the memo were additional randomized padding and which bits of the memo was the original data. To solve that we just need to store the size of the padding inside the memo itself so when we decrypt it we can pull the padding off the end and get back the original data.

// encryption const padding = calculatePadding(data);
const encryptedMemo = encrypt(data + padding + padding.length, secretKey)
.toString('hex');
const memo = StellarSdk.Memo.hash(encryptedMemo);

// decryption
const decryptedMemo = decrypt(memo.value, secretKey);
const paddingSize = decryptedMemo.last();
const originalData = decryptedMemo.remove(paddingSize + 1); // remove the padding + the extra character used to store the padding size

We’ve done it! At the end of the day were were able to squeeze out an additional 3 characters of space by using hash type memos (lost 1 potential byte so that we could store the padding size).

All that was left was to package it up into a reusable library: I’ve published version 1 of @stellarguard/secret-memo which is available as a JavaScript/TypeScript library on NPM and the code is available on Github.

Here’s how code that uses it might look.

import { SecretMemo } from '@stellarguard/secret-memo';

const secretKey = 'its a secret to everybody'; // keep it secret, keep it safe
const secretMemo = new SecretMemo(secretKey);

// create the secret and add to a transaction
const transaction = new TransactionBuilder(sourceAccount)
.addMemo(secretMemo.toMemo('drink your ovaltine'))
.addOperation(operation)
.build();

// read the secret from the transaction
const mySecret = secretMemo.fromMemo(transaction.memo); // drink your ovaltine

I look forward to the days when exchanges can leverage this code or ideas to keep their customers data more private and secure. When combined with Stellar’s federation servers, exchanges should be able to automatically generate these secret memos without having to have their users copying and pasting anything.

If you’ve got ideas for how to fit even more characters into the memo or other improvements, let me know here or file an issue.

If you’re interested in security and privacy when using the Stellar network, please try out StellarGuard. Every day more and more users are signing up to protect their XLM and other Stellar assets from hackers and thieves. Check out the FAQ to learn more about how StellarGuard can keep you safe.

More From The Blog

Featured Posts