waxwing's avatar
waxwing 1 week ago
We are very close to being able to have communication that is practically undetectable even by an aggressive censor like the CCP. Here's the basis of my claim: in order to be uncensorable, it's not only required that the data can't be readable (that's trivially achieved with end to end encryption), but also, the channel used to convey the messages needs to be 100% not suspicious. This is what we call steganography, and it's not only as hard as it seems, it's even harder. For example, you can embed data easily into low order bits of an image, but that doesn't successfully pass the "innocent" test. The pattern embedded in the binary file is statistically detectable with (not even very advanced) mathematics, so the censor will catch you. So you find yourself having to solve a strange problem: how to "mix" your secret message with some "innocent" message (called in the literature "cover text") in such a way that the innocent message is not suspicious. People have tried things like: embedding data in whitespace in text; this is obviously not good enough, though it's an amusing idea. Recently I started mulling over whether LLMs might be *the* solution to this problem, because an LLM's state evolution is governed by random choice, so if you embedded your data into that random stream, the output could be plausible innocent text while the hidden message was encoded in the randomness. (If you already see problems with that idea, well done, we'll get to it...)> It turns out I am not even slightly original: Meteor already had this idea in 2021 before LLMs had taken off, and moreover it tweaks the basic idea in the specific way needed to make it work. I won't bother expanding on the details rn. But I'll point out the limitations: for a receiver of the hidden message to actually receive it, they have to use exactly the same LLM, in exactly the same state, as the sender did (because it extracts the message from the random choices of token, which are stateful; the next token depends on the previous ones). The upshot of this: if we had a scenario where a "decent" LLM could be loaded by users on both sides, and the sender generating output from that model is plausible innocent text (true for a decent model, not true for a really crappy small model), then you have a properly undetectable communication channel that no censor, even a suspicious one, would be able to see. But it'd be very stateful: Alice and Bob would need to set it up in advance with a key exchange. The idea of the model being *public* (which the paper discusses), could *kind of* work, again, if the model is very good. The bandwidth would still be fairly low (but much higher than bits in jpegs!). Not really clear, but we're a way off from this, but it could come in future. Plausible but difficult for communication that is extremely dangerous. Notice again the pattern of how access to compute could be crucial for freedom.

Replies (20)

I'm trying to understand why you need the LLM. Let's say that an LLM is around 500GB. If Alice and Bob both have access to shared 500GB, why does the fact that the 500GB is an LLM give them something special? For example, in 500GB, we could simply have a vast lookup table that converts innocuous text to secret text. GM = Meet you at the cafe at 5PM. It's sunny here = We have been discovered. .... What am I missing here?
waxwing's avatar
waxwing 1 week ago
But I am with you; the trend is to outlaw uncensored general purpose computing.
waxwing's avatar
waxwing 1 week ago
Maybe pedantic but: it's not that the LLM *is* the PRF, but that we hijack the PRF that you throw into the LLM's token generation. One interesting detail is, apparently (I am very far from an LLM expert so had to double check this) you can do 'greedy decoding' and just sample the most likely token at every step. "Bland but deterministic". An interesting side story is: that's what you're always hunting for when you're looking for a practical stego technique: randomness input to the protocols. Think IVs in stream ciphers, or nonces in digital signatures (see Gustave Simmons "subliminal channel").
waxwing's avatar
waxwing 1 week ago
Yes, this one needs in advance key agreement. I don't think that's the biggest blocker here, but, it is one.
waxwing's avatar
waxwing 1 week ago
It's not really comparable. The problem with a code book is it won't generate plausible messages longer than a word or two. Afaik your technique has been in wide use amongst spies for many years, but it's exceptionally bandwidth constrained if you want to remain undetectable, and can't support messages of an unpredictable type.
Hmmmm, interesting. So you would have to share prompts in common as well? Maybe an encryption prompt and a decryption prompt? So if I were to send you a secret text, I would feed it into the LLM with the encryption prompt, and then I could also check it by feeding the cypher-text back in with the decryption prompt to make sure it comes out reasonably close to what I intend.
It's different from what I was thinking. Here is a Chinese LLM generated example for irony. ## A Concrete, Step-by-Step Example To make this tangible, let's use a **toy model with a small vocabulary of 11 words** so you can see every number. The real scheme works identically, just with vocabularies of ~50,000 tokens and floating-point probabilities. ### Setup **The model:** A tiny "LLM" with a fixed vocabulary of 11 words: `{apple, date, banana, cherry, pie, juice, tart, sauce, for, with, and}` At each step, the model assigns a probability to **all 11 words** based on the context. The probabilities change as the context grows, and they always sum to 1.0. (A real LLM does the same thing, just with ~50,000 tokens instead of 11.) **The shared context/prompt:** `"I like to eat"` β€” both Alice and Bob have this. It is not secret, just shared. **The shared secret key:** Used to seed a pseudorandom number generator (PRNG). The key produces the random stream: `0.15, 0.62, 0.40, ...` (uniform random numbers between 0 and 1). **The secret message Alice wants to send:** The bits `0 1 1` (3 bits β€” in reality this would be a longer message, but let's keep it tiny). --- ### Encoding (Alice's Side) #### Step 1: Alice runs the model Alice feeds the shared context `"I like to eat"` into the model. The model outputs a probability distribution over all 11 words. After "I like to eat", fruits are the most natural continuation, so they get the highest probabilities: | Word | Probability | Interval | |--------|-------------|---------------| | apple | 0.20 | [0.00, 0.20) | | date | 0.15 | [0.20, 0.35) | | banana | 0.10 | [0.35, 0.45) | | cherry | 0.05 | [0.45, 0.50) | | pie | 0.20 | [0.50, 0.70) | | juice | 0.12 | [0.70, 0.82) | | tart | 0.08 | [0.82, 0.90) | | sauce | 0.05 | [0.90, 0.95) | | for | 0.03 | [0.95, 0.98) | | with | 0.015 | [0.98, 0.995) | | and | 0.005 | [0.995, 1.00) | This is just normal LLM behavior β€” the model is predicting what word comes next. Notice the probabilities sum to 1.0, and the intervals partition [0, 1). #### Step 2: Alice encodes her first secret bit Alice's first secret bit is `0`. She interprets this as: "look in the **first half** of the range [0, 1), i.e., [0.00, 0.50)." The words whose intervals fall within [0.00, 0.50) are: - apple: [0.00, 0.20) - date: [0.20, 0.35) - banana: [0.35, 0.45) - cherry: [0.45, 0.50) Alice uses her next random number from the shared key, `0.15`, to pick within [0.00, 0.50). The value `0.15` falls in [0.00, 0.20), which is **apple**. **Alice outputs: "apple"** Text so far: `"I like to eat apple"` #### Step 3: Alice runs the model again Now the context is `"I like to eat apple"`. The model outputs a new distribution over all 11 words. After "apple", food preparations like pie and tart become more likely, while the other fruits become less likely: | Word | Probability | Interval | |--------|-------------|---------------| | apple | 0.15 | [0.00, 0.15) | | date | 0.10 | [0.15, 0.25) | | banana | 0.08 | [0.25, 0.33) | | cherry | 0.07 | [0.33, 0.40) | | for | 0.06 | [0.40, 0.46) | | with | 0.04 | [0.46, 0.50) | | pie | 0.20 | [0.50, 0.70) | | tart | 0.16 | [0.70, 0.86) | | juice | 0.08 | [0.86, 0.94) | | sauce | 0.04 | [0.94, 0.98) | | and | 0.02 | [0.98, 1.00) | Alice's next secret bit is `1`. This means "look in the **second half** [0.50, 1.00)." The words whose intervals fall within [0.50, 1.00) are: - pie: [0.50, 0.70) - tart: [0.70, 0.86) - juice: [0.86, 0.94) - sauce: [0.94, 0.98) - and: [0.98, 1.00) Alice uses her next random number, `0.62`, to pick within [0.50, 1.00). She scales it into that range: `0.50 + 0.62 Γ— 0.50 = 0.81`. The value `0.81` falls in [0.70, 0.86), which is **tart**. **Alice outputs: "tart"** Text so far: `"I like to eat apple tart"` #### Step 4: Alice runs the model again Context: `"I like to eat apple tart"`. Model output β€” after "apple tart", connectors and prepositions like "with" and "and" become the most likely continuations, while the fruits and preparations drop in probability: | Word | Probability | Interval | |--------|-------------|---------------| | apple | 0.10 | [0.00, 0.10) | | date | 0.08 | [0.10, 0.18) | | banana | 0.07 | [0.18, 0.25) | | cherry | 0.06 | [0.25, 0.31) | | pie | 0.08 | [0.31, 0.39) | | juice | 0.06 | [0.39, 0.45) | | for | 0.05 | [0.45, 0.50) | | tart | 0.06 | [0.50, 0.56) | | sauce | 0.04 | [0.56, 0.60) | | with | 0.18 | [0.60, 0.78) | | and | 0.22 | [0.78, 1.00) | Alice's next secret bit is `1`. Look in the second half [0.50, 1.00): The words whose intervals fall within [0.50, 1.00) are: - tart: [0.50, 0.56) - sauce: [0.56, 0.60) - with: [0.60, 0.78) - and: [0.78, 1.00) Random number `0.40` β†’ scaled: `0.50 + 0.40 Γ— 0.50 = 0.70`. Falls in [0.60, 0.78), which is **with**. **Alice outputs: "with"** Text so far: `"I like to eat apple tart with"` #### Alice is done Alice sends Bob the text: **"I like to eat apple tart with"** (Plus whatever padding/continuation she wants to make it look like a complete sentence.) To any observer, this looks like someone generated a sentence about food. Nothing suspicious. --- ### Decoding (Bob's Side) Bob has: - The received text: `"I like to eat apple tart with"` - The same model - The same shared context: `"I like to eat"` - The same shared key (so the same random stream: `0.15, 0.62, 0.40, ...`) #### Step D1: Bob runs the model Bob feeds `"I like to eat"` into the model. He gets the **same** distribution Alice got: | Word | Probability | Interval | |--------|-------------|---------------| | apple | 0.20 | [0.00, 0.20) | | date | 0.15 | [0.20, 0.35) | | banana | 0.10 | [0.35, 0.45) | | cherry | 0.05 | [0.45, 0.50) | | pie | 0.20 | [0.50, 0.70) | | juice | 0.12 | [0.70, 0.82) | | tart | 0.08 | [0.82, 0.90) | | sauce | 0.05 | [0.90, 0.95) | | for | 0.03 | [0.95, 0.98) | | with | 0.015 | [0.98, 0.995) | | and | 0.005 | [0.995, 1.00) | Bob sees that Alice chose **apple**, which is in the interval [0.00, 0.20). Now Bob asks: "Which half was apple in?" Apple is in [0.00, 0.20), which is in the **first half** [0.00, 0.50). **First half β†’ bit `0`** **Bob recovers bit: `0`** βœ“ #### Step D2: Bob runs the model again Bob appends "apple" to context: `"I like to eat apple"`. Model output: | Word | Probability | Interval | |--------|-------------|---------------| | apple | 0.15 | [0.00, 0.15) | | date | 0.10 | [0.15, 0.25) | | banana | 0.08 | [0.25, 0.33) | | cherry | 0.07 | [0.33, 0.40) | | for | 0.06 | [0.40, 0.46) | | with | 0.04 | [0.46, 0.50) | | pie | 0.20 | [0.50, 0.70) | | tart | 0.16 | [0.70, 0.86) | | juice | 0.08 | [0.86, 0.94) | | sauce | 0.04 | [0.94, 0.98) | | and | 0.02 | [0.98, 1.00) | Bob sees Alice chose **tart**, interval [0.70, 0.86). "Which half was tart in?" [0.70, 0.86) is in the **second half** [0.50, 1.00). **Second half β†’ bit `1`** **Bob recovers bit: `1`** βœ“ #### Step D3: Bob runs the model again Context: `"I like to eat apple tart"`. Model output: | Word | Probability | Interval | |--------|-------------|---------------| | apple | 0.10 | [0.00, 0.10) | | date | 0.08 | [0.10, 0.18) | | banana | 0.07 | [0.18, 0.25) | | cherry | 0.06 | [0.25, 0.31) | | pie | 0.08 | [0.31, 0.39) | | juice | 0.06 | [0.39, 0.45) | | for | 0.05 | [0.45, 0.50) | | tart | 0.06 | [0.50, 0.56) | | sauce | 0.04 | [0.56, 0.60) | | with | 0.18 | [0.60, 0.78) | | and | 0.22 | [0.78, 1.00) | Bob sees Alice chose **with**, interval [0.60, 0.78). "Which half?" [0.60, 0.78) is in the **second half** [0.50, 1.00). **Second half β†’ bit `1`** **Bob recovers bit: `1`** βœ“ --- ### Result Bob has recovered: `0 1 1` β€” exactly the message Alice sent. --- ## Diagram ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ ALICE (ENCODER) β”‚ β”‚ β”‚ β”‚ Secret bits: 0, 1, 1 β”‚ β”‚ Shared context: "I like to eat" β”‚ β”‚ Shared key β†’ random stream: 0.15, 0.62, 0.40 β”‚ β”‚ β”‚ β”‚ For each bit: β”‚ β”‚ 1. Run model with current context β†’ distribution β”‚ β”‚ 2. Use bit to pick which HALF of [0,1) to look in β”‚ β”‚ 3. Use random# to pick a word within that half β”‚ β”‚ 4. Append word to context, repeat β”‚ β”‚ β”‚ β”‚ Output: "I like to eat apple tart with" β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β”‚ (sent over channel β€” looks innocent) β–Ό β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ BOB (DECODER) β”‚ β”‚ β”‚ β”‚ Received text: "I like to eat apple tart with" β”‚ β”‚ Shared context: "I like to eat" (SAME as Alice) β”‚ β”‚ Shared key β†’ random stream: 0.15, 0.62, 0.40 (SAME as Alice) β”‚ β”‚ β”‚ β”‚ For each word: β”‚ β”‚ 1. Run model with current context β†’ SAME distribution β”‚ β”‚ 2. Find which interval the word falls in β”‚ β”‚ 3. Determine which HALF that interval is in β”‚ β”‚ 4. That half = the recovered bit β”‚ β”‚ 5. Append word to context, repeat β”‚ β”‚ β”‚ β”‚ Recovered bits: 0, 1, 1 βœ“ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` --- ## Key Points 1. **The LLM never saw the secret message.** The secret bits controlled *which word was picked* from the distribution. The LLM just produced distributions. 2. **The output looks natural.** "I like to eat apple tart with" is a perfectly normal sentence. A censor seeing this has no reason to be suspicious. 3. **Both sides run the same model with the same context.** This is why they get the same distributions. If Bob had a different model or different context, the distributions would differ and decoding would fail. 4. **The shared key provides the random stream.** This is needed for the encoding to be secure β€” without it, the censor could run the same model and try to decode. With the key, the censor can't reproduce the random choices. 5. **Each token encodes roughly 1 bit** in this toy example (because we split into 2 halves). In reality, with arithmetic coding over a 50,000-word vocabulary, you can encode **multiple bits per token** β€” roughly 2–4 bits per token for a real LLM, depending on how uncertain the model is at each step. 6. **The "half" splitting is the simplified version.** The real scheme (from the Meteor paper, eprint.iacr.org/2021/686) uses full arithmetic coding, which is more efficient β€” it doesn't waste the probability mass the way a 2-way split does. But the principle is the same: the secret bits steer the selection within the model's probability distribution.
waxwing's avatar
waxwing 1 week ago
Both copies of the LLM have to start in the same state, so e.g. same initial prompt. Then the receiver's copy will track the state of the sender's by reading the message sent. They have to stay in lockstep. I believe it would work for 2 way communication using one model, but I'm not sure if that's practical. But I mean it's not practical yet, anyway, except for some very specialized scenarios I guess.
RamenCoffee's avatar
RamenCoffee 1 week ago
I have daydreamed of embedding sleeper messages into an LLM making it spit out a certain sentence when given a certain input a la Bourne identity. Seems like the same problem to me. You displace the image problem into a bigger canvas : Will the censor identify "tampering" of the LLM ? You're trying to essentially smuggle enigma machines with a modified steganography in the weightings of your LLM.
waxwing's avatar
waxwing 1 week ago
Since this got some interest, I decided to share a simple example you can run on your computer: The example here is with a 3B model, needing about 12GB in RAM (?) and only takes a few seconds to encode/decode: (venv)% python runme.py Using device: mps Model alias: qwen-3b Loading local model from ./models/Qwen--Qwen2.5-3B Loading checkpoint shards: 100%|β–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆβ–ˆ| 3/3 [00:00<00:00, 32.09it/s] Plaintext : b'Attack at dawn!' Encoding... Encoding: 152/152 bits, 82 tokens generated... Stegotext : In 1492, when Christopher Columbus landed on the New World, he thought he had reached India. But Indian people had already lived in the Americas for thousands of years with little contact with the outside world. They knew about the land and waters, unlike Columbus who was ignorant about this particular region. While Columbus' expedition of 1492-1493 can be considered America's "first landing," that is Decoded 82 bits so far... Recovered : b'Attack at dawn!' Success: round-trip verified. That's generous though, most of the other examples looked a lot more stupid. Also notice, the hidden text:stegotext ratio is 1:20, or so. While that's bad, it is viable for verbal communication, and most types of steganography are a lot worse. You can change the secret key, the hidden message and the initial prompt to play with it. View quoted note β†’
waxwing's avatar
waxwing 1 week ago
Honestly I think that's pretty good! It gets the gist of it, right.
It's scary how precise I, robot depicts this idea. The detective has to figure out which questions to ask in order to obtain clues and solve the case.
waxwing's avatar
waxwing 1 week ago
Just a non-technical aspect worth mentioning: there could be a remarkable *cultural* or even *artistic* component to this idea. Consider this: randomness and creativity are connected; if you use no randomness and follow a "paint by numbers" algorithm approach to creating something, it will usually be perceived as flat, stale. At the same time, it's deterministic, reliable. The way this can manifest in LLMs is that you can choose to make them completely deterministic, but their output will be boring and less useful, in many scenarios, because of that [1]. So the existence of "subliminal/covert channels" in the data streams that we will consume in future may be intimately connected with the creativity that is allowed. It's a very profound demonstration of the link between freedom and creativity. I may be overreaching but this connection could be profoundly important politically in the future. [1] as noted, there *are* benefits as well as disadvantages to what's called "greedy decoding" i.e. always pick the most likely token; for some tasks this works OK. But fascinatingly there are pathologies: a never-varying choice algorithm can lead to *infinite loops*. For anyone who studied nonlinear optimization techniques in the context of engineering, this might ring a bell: local minima can function as traps for search algorithms. Basically, a chatbot that has no randomness is too simple to navigate the noisiness of reality, unless the reality it's navigating is very clean and simple. View quoted note β†’
↑