Logo

Hidoji: Hiding Secrets in Plain Sight

Steganography is the practice of concealing a message within another object or message. Unlike cryptography, which obscures the content of a message, steganography obscures the existence of the message itself.

I recently built a tool called Hidoji (Hidden Emoji) that implements this concept using a fun medium: Emojis.

The Concept

Hidoji takes a standard emoji (like a black flag 🏴) and injects a hidden payload into it. To the naked eye, and to most applications, the emoji looks exactly the same.

Hide: secret message + 🏴 → 🏴 Reveal: 🏴 → secret message

How is this possible? The answer lies in the vastness of the Unicode standard.

Invisible Characters

Unicode contains many characters that are not meant to be rendered visually. They are control characters used for formatting, text direction, or word joining.

Hidoji uses four specific characters to encode data:

  1. Word Joiner (\u2060): Used as a Start Marker.
  2. Invisible Separator (\u2063): Used as an End Marker.
  3. Zero Width Space (\u200B): Represents a binary 0.
  4. Zero Width Non-Joiner (\u200C): Represents a binary 1.

By converting a text string into binary (ASCII/UTF-8 bytes), and then mapping those bits to zero-width characters, we can append a long string of invisible data to any visible character.

The Implementation

I built Hidoji in Go because of its excellent string handling and cross-platform compilation.

Here is the core logic for hiding data. We take a base emoji string s and a payload. We write the emoji, then the start marker, then iterate through the bits of the payload, writing zero-width characters, and finally the end marker.

const (
    zwsp   = '\u200B' // 0
    zwnj   = '\u200C' // 1
    wjoin  = '\u2060' // start
    invsep = '\u2063' // end
)

func HideEmojiData(s, payload string) string {
    var buf bytes.Buffer
    buf.WriteString(s)
    buf.WriteRune(wjoin)
    for _, bit := range toBits([]byte(payload)) {
        if bit == '0' {
            buf.WriteRune(zwsp)
        } else {
            buf.WriteRune(zwnj)
        }
    }
    buf.WriteRune(invsep)
    return buf.String()
}

Revealing the data is simply the reverse process. We scan the string for the start marker, read until the end marker, and convert the zero-width characters back into bits.

Using Hidoji

The tool is available as a CLI.

# Build it
go build -o hidoji

# Hide a message
./hidoji encode "The passcode is 1234" --base "🔒"
# Output: 🔒 (with hidden data)

# Reveal it
./hidoji decode "🔒..."
# Output: The passcode is 1234

Important Note on Copying

If you try to select and copy the emoji output directly from your terminal window, the operating system often strips out the "invisible" zero-width characters, meaning the hidden message gets lost.

To reliably copy the output, you can use the --copy flag (macOS only) or pipe it directly to your system's clipboard.

# Use the copy flag
./hidoji encode "Secret" --base "🚩" --copy

# Or pipe to pbcopy (macOS)
./hidoji encode "Secret" --base "🚩" | pbcopy

Now you can paste (Cmd+V) the emoji anywhere, and the hidden data will be preserved.

Why do I see <200b> when I paste in my terminal?

If you paste the result back into your terminal (ZSH), you might see something like:

🏴<200b><200c><200b>...

This is normal! Your shell (ZSH) is being helpful by "escaping" invisible characters so you can see them. This confirms the data is there. If you paste the same emoji into Slack, WhatsApp, or a text editor, those characters will remain invisible.

Because the output is just a valid Unicode string, you can paste it almost anywhere: Slack, iMessage, Twitter/X, or even inside source code comments.

Limitations and Conclusion

While robust, this technique isn't bulletproof. Some platforms (like Twitter handles or certain form fields) sanitize input by stripping non-printable characters. If the invisible characters are removed, the message is lost.

However, for most "copy-paste" scenarios—email, chat apps, text files—it works perfectly.

Check out the source code on GitHub: tomweston/hidoji

What This Means

Hidoji demonstrates that Unicode is not just a character set, but a programmable medium. Steganography in text allows for the covert transmission of data in plain sight, highlighting the need for robust input sanitization in security-sensitive applications.