You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
This document provides a complete specification for a plausibly deniable encrypted storage system that can hold multiple independent sessions while hiding their existence from forensic analysis.
Part 1: Theory
1. Introduction
1.1 Goal
The goal is to store one or more encrypted sessions on a device without forensics being able to determine how many sessions exist, or to decipher any of them without the corresponding password.
The UX is simple: the user types a password, and the system automatically finds and decrypts the session locked by this password, if any. Otherwise it fails with "wrong password".
1.2 System Architecture
The system consists of two storage blobs:
Blob
Size
Purpose
Addressing blob
2 MB (fixed)
Maps passwords → session locations
Data blob
Variable (grows)
Stores encrypted session data
┌─────────────────────────────────────────────────────────────────┐
│ Addressing Blob (2 MB) │
│ ┌─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┬─────┐ │
│ │slot │slot │slot │slot │ ... │slot │slot │slot │slot │ │
│ │ 0 │ 1 │ 2 │ 3 │ │65533│65534│65535│ │ │
│ └─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┴─────┘ │
│ Each slot: 32 bytes (random or AEAD-encrypted address) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ Data Blob (variable size) │
│ ┌────────┬─────────┬────────┬─────────┬────────┬─────────┬─── │
│ │padding │ block A │padding │ block B │padding │ block C │... │
│ └────────┴─────────┴────────┴─────────┴────────┴─────────┴─── │
│ Blocks belong to different sessions; padding is random bytes │
└─────────────────────────────────────────────────────────────────┘
1.3 Design Constraints
Constraint
Value
Rationale
Average storage overhead
≤ 50%
Reasonable disk usage
Maximum single padding
Bounded
No catastrophic disk events
Minimum block size
2 MB
I/O efficiency
Address redundancy
High
Resilience to overwrites
Collision probability
< 10⁻⁹
Effectively impossible data loss
2. Addressing Blob Theory
2.1 Problem Statement
The addressing blob must:
Map a password to the byte offset of the session's root block in the data blob
Look entirely random (indistinguishable from random noise)
Support multiple sessions without revealing their count
Tolerate overwrites when new sessions are added
2.2 Solution: Redundant AEAD-Encrypted Slots
The addressing blob is divided into 65,536 slots of 32 bytes each (total: 2 MB).
Each session writes its address to k pseudo-random slots (derived from the password). Each copy is AEAD-encrypted with a different password-derived key, using a fresh nonce each time.
Why AEAD instead of XOR?
AEAD (AES-256-SIV) provides authenticated encryption with fresh nonces
Each write generates a new random nonce, preventing ciphertext reuse
Authentication tag allows detecting corrupted vs valid slots during unlock
Indistinguishable from random data to anyone without the key
Slot size calculation (AES-256-SIV):
Plaintext: 12 bytes (address u64 + length u32)
SIV tag: 16 bytes
Padded ciphertext: 16 bytes (AES block size)
Total: 32 bytes per slot
2.3 Collision Analysis
When a new session is added, its k slots may overwrite existing session slots.
Model:
Blob: N = 65,536 slots
Existing sessions: up to 1024 (each with k copies)
New session: k new slot writes
For a session to be lost, all k of its copies must be overwritten. The probability that a specific slot is overwritten by one new session is k/N.
Probability that one specific session loses all copies:
P(one session lost) = (k/N)^k
Probability that at least one of S sessions is lost (union bound):
P(any lost) ≤ S × (k/N)^k
2.4 Finding Optimal k
We want P(any lost) < 10⁻⁹ for S = 1024 sessions.
k (copies)
P(one session lost)
P(any of 1024 lost)
30
2.4 × 10⁻⁹
2.5 × 10⁻⁶
35
1.5 × 10⁻¹¹
1.5 × 10⁻⁸
40
8.3 × 10⁻¹⁴
8.5 × 10⁻¹¹
46
1.8 × 10⁻¹⁶
1.8 × 10⁻¹³
50
8.9 × 10⁻¹⁹
9.1 × 10⁻¹⁶
Conclusion: k = 46 provides P(any lost) < 10⁻¹² for 1024 sessions, which is effectively impossible.
Deniability is measured as the percentage of two-session scenarios where the unexplained space U falls within the 99.9th percentile of the one-session distribution.
k_A: number of blocks in known session (attacker has decrypted)
k_B: number of blocks in hidden session
Threshold: 99.9th percentile of sum of k_A padding draws
Hidden footprint: sum of k_B blocks + k_B padding draws
Deniability Matrix (% of hidden sessions that are statistically undetectable):
Known Session
1 block
2 blocks
5 blocks
10 blocks
20 blocks
5 blocks
99.5%
99.1%
97.7%
—
—
10 blocks
99.9%
98.8%
96.7%
83%
—
20 blocks
99.7%
99.5%
98.9%
87%
10%
50 blocks
99.8%
99.6%
99.1%
95%
54%
100 blocks
99.9%
99.8%
99.3%
98%
88%
Interpretation:
Small hidden sessions (1-5 blocks, ~35-175 MB): Virtually undetectable
Medium hidden sessions (10 blocks, ~350 MB): Good deniability (>80%) with ≥10 known blocks
Large hidden sessions (20 blocks, ~700 MB): Requires large known session (≥50 blocks)
3.9 Comparison: Pareto vs Log-Normal Padding
Scenario
Pareto
Log-Normal
Difference
k_A=20, k_B=5
98.9%
98.2%
+0.7%
k_A=20, k_B=10
87%
90%
−3%
k_A=20, k_B=20
27%
3%
+24%
Pareto's heavier tail provides significantly better deniability for large hidden sessions.
3.10 Block Key Derivation
Instead of storing encryption keys directly in the allocation table, block keys are derived from a block-specific identifier:
block_key = kdf(session_aead_key, [block_id])
Where block_id is a 32-byte random identifier generated when the block is created.
Nonce: 16 bytes (generated internally, included in ciphertext)
Auth tag: 16 bytes (SIV)
Padding: 16-byte block alignment
Ciphertext overhead: 32 bytes for small plaintexts
2.1 Key Derivation
defderive_session_keys(password: bytes) ->dict:
""" Derive all keys needed for a session from password. Returns dict with: - aead_key: 64 bytes for AEAD encryption - addressing_slots: list of 46 slot indices (u16) - addressing_keys: list of 46 AEAD keys (64 bytes each) """# Derive master key from password (handles low entropy safely)master=password_kdf(password)
# Prepare labels for all keys we needlabels= ["aead"]
foriinrange(COPIES_PER_SESSION):
labels.append(f"slot-{i}")
labels.append(f"addr-key-{i}")
# Derive all keys at oncederived=kdf(master, labels)
aead_key=derived[0] # 64 bytesaddressing_slots= []
addressing_keys= []
foriinrange(COPIES_PER_SESSION):
slot_bytes=derived[1+i*2][:2] # first 2 bytes → slot indexkey_bytes=derived[2+i*2][:64] # 64 bytes for AEAD keyaddressing_slots.append(int.from_bytes(slot_bytes, "big"))
addressing_keys.append(key_bytes)
return {
"aead_key": aead_key,
"addressing_slots": addressing_slots,
"addressing_keys": addressing_keys,
}
3. Addressing Blob Operations
3.1 Slot Format
┌─────────────────────────────────────────────────────────┐
│ AEAD ciphertext of (address u64 BE + length u32 BE) │
│ 32 bytes total │
│ ┌──────────────────┬──────────────────────────────┐ │
│ │ SIV tag (16 B) │ Encrypted padded data (16 B) │ │
│ └──────────────────┴──────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
3.2 Initialization
definit_addressing_blob() ->bytes:
"""Create new addressing blob filled with random bytes."""returnrandom_bytes(ADDRESSING_BLOB_SIZE)
3.3 Writing a Session Address
defwrite_session_address(
blob: bytearray,
slots: list[int],
keys: list[bytes],
address: int,
length: int
):
""" Write session address to 46 slots with redundancy. Each write uses AEAD with a fresh nonce. """plaintext=address.to_bytes(8, "big") +length.to_bytes(4, "big")
foriinrange(COPIES_PER_SESSION):
offset=slots[i] *SLOT_SIZE# AEAD encrypt with fresh nonce each timeciphertext=aead_encrypt(keys[i], plaintext)
blob[offset:offset+SLOT_SIZE] =ciphertext
3.4 Reading a Session Address (Timing-Safe)
defread_session_address(
blob: bytes,
slots: list[int],
keys: list[bytes]
) ->list[tuple[int, int, bool]]:
""" Read and attempt to decrypt all 46 copies of the session address. IMPORTANT: Always processes all 46 slots for timing safety. Returns: List of (address, length, valid) tuples. """candidates= []
# Phase 1: Read all slotsciphertexts= []
foriinrange(COPIES_PER_SESSION):
offset=slots[i] *SLOT_SIZEciphertexts.append(blob[offset:offset+SLOT_SIZE])
# Phase 2: Attempt decryption on all (constant-time iteration)foriinrange(COPIES_PER_SESSION):
try:
plaintext=aead_decrypt(keys[i], ciphertexts[i])
address=int.from_bytes(plaintext[0:8], "big")
length=int.from_bytes(plaintext[8:12], "big")
candidates.append((address, length, True))
exceptDecryptionError:
# Invalid slot (random data or overwritten)candidates.append((0, 0, False))
returncandidates
4. Block and Padding Size Sampling
4.1 Block Size (Log-Normal)
importmathimportnumpyasnpdefdraw_block_size() ->int:
""" Draw block size from truncated log-normal distribution. Returns: block size in bytes, range [2 MB, 256 MB] """whileTrue:
size=np.random.lognormal(mean=BLOCK_MU, sigma=BLOCK_SIGMA)
ifBLOCK_MIN<=size<=BLOCK_MAX:
returnint(size)
4.2 Padding Size (Pareto)
# Pre-compute for efficiency_PADDING_F_MAX=1- (PADDING_MIN/PADDING_MAX) **PADDING_ALPHAdefdraw_padding_size() ->int:
""" Draw padding size from truncated Pareto distribution. Returns: padding size in bytes, range [5 MB, 600 MB] """u=np.random.random()
size=PADDING_MIN/ (1-u*_PADDING_F_MAX) ** (1/PADDING_ALPHA)
returnint(min(size, PADDING_MAX))
4.3 Combined Allocation
defallocate() ->tuple[int, int]:
""" Allocate one block. Returns: (padding_size, block_size) in bytes """returndraw_padding_size(), draw_block_size()
5. Root Block Format
Each session has a root block containing its allocation table.
┌─────────────────────────────────────────────────────────────────┐
│ Root Block (plaintext, before encryption) │
├─────────────────────────────────────────────────────────────────┤
│ num_entries: u32 BE (4 bytes)│
├─────────────────────────────────────────────────────────────────┤
│ Allocation Table Entry 0: (56 bytes)│
│ inner_data_offset: u64 BE (logical offset in session data) │
│ inner_length: u32 BE (usable space in block) │
│ address: u64 BE (byte offset in data blob) │
│ outer_length: u32 BE (encrypted block size) │
│ block_id: 32 bytes (random ID for key derivation) │
├─────────────────────────────────────────────────────────────────┤
│ ... more entries ... │
├─────────────────────────────────────────────────────────────────┤
│ Random padding to fill block size │
└─────────────────────────────────────────────────────────────────┘
Entry size: 8 + 4 + 8 + 4 + 32 = 56 bytes
Capacity: A minimal root block (~2 MB) holds ~37,000 entries → ~1.3 TB of session data.
5.1 Block Key Derivation
defderive_block_key(session_aead_key: bytes, block_id: bytes) ->bytes:
""" Derive the encryption key for a data block. Args: session_aead_key: The session's main AEAD key (64 bytes) block_id: The block's unique identifier (32 bytes) Returns: 64-byte AEAD key for this block """derived=kdf(session_aead_key, [block_id])
returnderived[0][:64]
6. Data Block Format
┌─────────────────────────────────────────────────────────────────┐
│ Data Block (plaintext, before encryption) │
├─────────────────────────────────────────────────────────────────┤
│ used_length: u32 BE (bytes of actual data) │
├─────────────────────────────────────────────────────────────────┤
│ data: [used_length bytes] (actual session data) │
├─────────────────────────────────────────────────────────────────┤
│ padding: random bytes (fill to inner_length) │
└─────────────────────────────────────────────────────────────────┘
7. Session Operations
7.1 Create New Session
defcreate_session(
password: bytes,
addressing_blob: bytearray,
data_blob: bytearray
) ->dict:
"""Create a new session."""# 1. Derive keys from passwordkeys=derive_session_keys(password)
# 2. Draw sizes for root blockpadding_size, block_size=allocate()
# 3. Append padding to data blobdata_blob.extend(random_bytes(padding_size))
# 4. Record address (current end of blob)root_address=len(data_blob)
# 5. Create empty root blockroot_block=bytearray(4) # num_entries = 0root_block.extend(random_bytes(block_size-4))
# 6. Encrypt root blockencrypted_root=aead_encrypt(keys["aead_key"], root_block)
# 7. Append encrypted root block to data blobdata_blob.extend(encrypted_root)
# 8. Write address to addressing blob (46 copies)write_session_address(
addressing_blob,
keys["addressing_slots"],
keys["addressing_keys"],
root_address,
len(encrypted_root)
)
return {
"aead_key": keys["aead_key"],
"root_address": root_address,
"root_length": len(encrypted_root),
"allocation_table": [],
}
7.2 Unlock Existing Session (Timing-Safe)
defunlock_session(
password: bytes,
addressing_blob: bytearray,
data_blob: bytes
) ->dict|None:
""" Attempt to unlock a session. Returns None if wrong password. IMPORTANT: Always scans all 46 candidates before returning to prevent timing attacks. """keys=derive_session_keys(password)
# Read and attempt decrypt ALL 46 candidatescandidates=read_session_address(
addressing_blob,
keys["addressing_slots"],
keys["addressing_keys"]
)
# Track which slot indices had corrupted/invalid datacorrupted_indices= []
# Try to decrypt root block at each valid candidate# Continue through ALL candidates even after finding successsuccessful_result=Nonefori, (address, length, valid) inenumerate(candidates):
ifnotvalid:
# Slot failed AEAD decryption (corrupted or overwritten)corrupted_indices.append(i)
continueifaddress+length>len(data_blob):
# Address points outside blob (corrupted)corrupted_indices.append(i)
continueencrypted_root=data_blob[address:address+length]
try:
root_block=aead_decrypt(keys["aead_key"], encrypted_root)
allocation_table=parse_allocation_table(root_block)
# Store result but continue checking all candidatesifsuccessful_resultisNone:
successful_result= {
"aead_key": keys["aead_key"],
"root_address": address,
"root_length": length,
"allocation_table": allocation_table,
}
exceptDecryptionError:
# Address was valid but root block decryption failed (corrupted)corrupted_indices.append(i)
continue# Only after checking all candidates, heal corrupted copies and returnifsuccessful_resultisnotNoneandcorrupted_indices:
# Heal only the corrupted slots by reusing write_session_address# with filtered slot/key listscorrupted_slots= [keys["addressing_slots"][i] foriincorrupted_indices]
corrupted_keys= [keys["addressing_keys"][i] foriincorrupted_indices]
write_session_address(
addressing_blob,
corrupted_slots,
corrupted_keys,
successful_result["root_address"],
successful_result["root_length"]
)
returnsuccessful_result
7.3 Add Data Block
defadd_data_block(
session: dict,
data_blob: bytearray,
addressing_blob: bytearray
) ->dict:
"""Allocate a new data block for the session."""# 1. Draw sizespadding_size, block_size=allocate()
# 2. Append paddingdata_blob.extend(random_bytes(padding_size))
# 3. Record addressblock_address=len(data_blob)
# 4. Generate random block ID and derive encryption keyblock_id=random_bytes(BLOCK_ID_SIZE)
block_key=derive_block_key(session["aead_key"], block_id)
# 5. Create empty data blockdata_block=bytearray(4) # used_length = 0inner_length=block_size-4data_block.extend(random_bytes(inner_length))
# 6. Encrypt and appendencrypted_block=aead_encrypt(block_key, data_block)
data_blob.extend(encrypted_block)
# 7. Create allocation entryifsession["allocation_table"]:
last=session["allocation_table"][-1]
inner_offset=last["inner_data_offset"] +last["inner_length"]
else:
inner_offset=0entry= {
"inner_data_offset": inner_offset,
"inner_length": inner_length,
"address": block_address,
"outer_length": len(encrypted_block),
"block_id": block_id,
}
session["allocation_table"].append(entry)
save_root_block(session, data_blob, addressing_blob)
returnentry
7.4 Read/Write Session Data
defdecrypt_data_block(session: dict, data_blob: bytes, entry: dict) ->bytes:
"""Decrypt a data block using derived key."""block_key=derive_block_key(session["aead_key"], entry["block_id"])
encrypted=data_blob[entry["address"]:entry["address"] +entry["outer_length"]]
plaintext=aead_decrypt(block_key, encrypted)
used_length=int.from_bytes(plaintext[0:4], "big")
returnplaintext[4:4+used_length]
defread_session_data(session: dict, data_blob: bytes, offset: int, length: int) ->bytes:
"""Read data from session at logical offset."""result=bytearray()
forentryinsession["allocation_table"]:
entry_start=entry["inner_data_offset"]
entry_end=entry_start+entry["inner_length"]
ifoffset<entry_endandoffset+length>entry_start:
block_key=derive_block_key(session["aead_key"], entry["block_id"])
encrypted=data_blob[entry["address"]:entry["address"] +entry["outer_length"]]
block_data=aead_decrypt(block_key, encrypted)[4:] # skip used_lengthread_start=max(0, offset-entry_start)
read_end=min(entry["inner_length"], offset+length-entry_start)
result.extend(block_data[read_start:read_end])
returnbytes(result)
defwrite_session_data(session: dict, data_blob: bytearray, offset: int, data: bytes):
"""Write data to session at logical offset."""forentryinsession["allocation_table"]:
entry_start=entry["inner_data_offset"]
entry_end=entry_start+entry["inner_length"]
ifoffset<entry_endandoffset+len(data) >entry_start:
# Derive key and decryptblock_key=derive_block_key(session["aead_key"], entry["block_id"])
encrypted=data_blob[entry["address"]:entry["address"] +entry["outer_length"]]
block_plaintext=bytearray(aead_decrypt(block_key, encrypted))
# Calculate overlap and writewrite_start=max(0, offset-entry_start) +4# +4 for used_length headerwrite_end=min(entry["inner_length"], offset+len(data) -entry_start) +4data_start=max(0, entry_start-offset)
block_plaintext[write_start:write_end] =data[data_start:data_start+ (write_end-write_start)]
# Update used_length if neededcurrent_used=int.from_bytes(block_plaintext[0:4], "big")
new_used=max(current_used, write_end-4)
block_plaintext[0:4] =new_used.to_bytes(4, "big")
# Re-encrypt block (in place)new_encrypted=aead_encrypt(block_key, block_plaintext)
data_blob[entry["address"]:entry["address"] +entry["outer_length"]] =new_encrypted
8. Encryption
Use AEAD (AES-256-SIV) from Gossip WASM for all encryption:
Deterministic for same inputs (SIV mode), but nonce makes each call unique
Part 3: Conclusion
Summary of Achievements
This document specifies a complete plausibly deniable multi-session encrypted storage system with the following properties:
Addressing Blob
Property
Value
Fixed size
2 MB
Slots
65,536 × 32 bytes
Encryption
AEAD (AES-256-SIV) per slot
Copies per session
46
Collision probability (1024 sessions)
< 10⁻¹²
Fresh ciphertext on every write
✓
Timing-safe unlock
✓
Indistinguishable from random
✓
The 46-copy redundancy ensures that even with 1024 concurrent sessions, the probability of any session losing all its address copies is less than one in a trillion.
Data Blob
Property
Value
Block size distribution
Log-Normal(μ=ln(32MB), σ=0.4)
Block size range
[2 MB, 256 MB]
Mean block size
35 MB
Padding distribution
Pareto(min=5MB, α=1.25, max=600MB)
Mean padding
17.5 MB
Average overhead
50%
Block key derivation
KDF(session_key, block_id)
Allocation entry size
56 bytes
Plausible Deniability
Hidden Session Size
Deniability (with 20 known blocks)
1 block (~35 MB)
99.7%
2 blocks (~70 MB)
99.5%
5 blocks (~175 MB)
98.9%
10 blocks (~350 MB)
87%
20 blocks (~700 MB)
27%
Small to medium hidden sessions (up to ~350 MB) are virtually undetectable. Even large hidden sessions benefit from significant deniability when the known session is large.
Security Properties
Property
Achieved
Password-bound encryption
✓ AEAD with password-derived keys
No session count leakage
✓ Random ≡ encrypted slots
Fresh ciphertext
✓ New nonce on every AEAD operation
Resilient to forensic analysis
✓ Heavy-tailed padding distributions
Bounded storage growth
✓ Hard caps on all sizes
Self-healing addressing
✓ Re-write corrupted copies on unlock
Timing-safe operations
✓ Scan all candidates before returning
No stored key material
✓ Block keys derived from block_id
Final Assessment
The design satisfies all specified constraints:
Constraint
Target
Achieved
Average storage overhead
≤ 50%
50% ✓
Maximum single padding
Bounded
600 MB ✓
Minimum block size
≥ 2 MB
2 MB ✓
Catastrophic events
None
Hard caps ✓
Address collision (1024 sessions)
< 10⁻⁹
< 10⁻¹² ✓
Addressing blob encryption
AEAD
AES-256-SIV ✓
Block key storage
Derived
KDF(key, block_id) ✓
Timing safety
Required
All 46 scanned ✓
The system provides strong plausible deniability for hidden sessions while maintaining reasonable storage overhead and practical I/O characteristics. The design is robust against forensic analysis, timing attacks, and operational failures. All key material is either derived or protected by authenticated encryption with fresh nonces.
reacted with thumbs up emoji reacted with thumbs down emoji reacted with laugh emoji reacted with hooray emoji reacted with confused emoji reacted with heart emoji reacted with rocket emoji reacted with eyes emoji
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
Plausibly Deniable Multi-Session Encrypted Storage - V1
This document provides a complete specification for a plausibly deniable encrypted storage system that can hold multiple independent sessions while hiding their existence from forensic analysis.
Part 1: Theory
1. Introduction
1.1 Goal
The goal is to store one or more encrypted sessions on a device without forensics being able to determine how many sessions exist, or to decipher any of them without the corresponding password.
The UX is simple: the user types a password, and the system automatically finds and decrypts the session locked by this password, if any. Otherwise it fails with "wrong password".
1.2 System Architecture
The system consists of two storage blobs:
1.3 Design Constraints
2. Addressing Blob Theory
2.1 Problem Statement
The addressing blob must:
2.2 Solution: Redundant AEAD-Encrypted Slots
The addressing blob is divided into 65,536 slots of 32 bytes each (total: 2 MB).
Each session writes its address to k pseudo-random slots (derived from the password). Each copy is AEAD-encrypted with a different password-derived key, using a fresh nonce each time.
Why AEAD instead of XOR?
Slot size calculation (AES-256-SIV):
2.3 Collision Analysis
When a new session is added, its k slots may overwrite existing session slots.
Model:
For a session to be lost, all k of its copies must be overwritten. The probability that a specific slot is overwritten by one new session is k/N.
Probability that one specific session loses all copies:
Probability that at least one of S sessions is lost (union bound):
2.4 Finding Optimal k
We want P(any lost) < 10⁻⁹ for S = 1024 sessions.
Conclusion: k = 46 provides P(any lost) < 10⁻¹² for 1024 sessions, which is effectively impossible.
2.5 Why 46 Copies?
With 46 copies per session:
The redundancy provides extreme resilience with minimal overhead.
2.6 Timing and Read Pattern Security
When unlocking a session, the implementation must:
This prevents timing attacks and read-pattern analysis that could reveal which slot contained valid data.
2.7 Addressing Security Properties
3. Data Blob Theory
3.1 Threat Model
Attacker capabilities:
Attacker's analysis:
After decrypting session A with k blocks, the attacker computes:
The unexplained space U consists of:
Security goal:
The attacker must never be able to say with certainty:
3.2 Why Distribution Choice Matters
The problem with bounded distributions:
Consider uniform padding in [2 MB, 20 MB]:
If the attacker observes U > k × P_max, they have proof that additional sessions exist.
Requirements for deniability:
3.3 Distribution Analysis
3.4 Pareto vs Log-Normal for Padding
At the same mean (~17.5 MB), tail probabilities differ significantly:
Pareto has 2× more probability mass in the extreme tail, which is critical for hiding larger sessions.
3.5 Block Size Constraints
The 2 MB minimum block size constrains distribution choice:
Conclusion: Log-Normal is the only viable option for blocks given the constraints.
3.6 Optimal Parameters
Block Size Distribution — Truncated Log-Normal:
Padding Size Distribution — Truncated Pareto:
3.7 Storage Overhead
Per-session overhead varies due to randomness:
3.8 Deniability Results
Methodology:
Deniability is measured as the percentage of two-session scenarios where the unexplained space U falls within the 99.9th percentile of the one-session distribution.
Deniability Matrix (% of hidden sessions that are statistically undetectable):
Interpretation:
3.9 Comparison: Pareto vs Log-Normal Padding
Pareto's heavier tail provides significantly better deniability for large hidden sessions.
3.10 Block Key Derivation
Instead of storing encryption keys directly in the allocation table, block keys are derived from a block-specific identifier:
Where
block_idis a 32-byte random identifier generated when the block is created.Benefits:
3.11 Key Design Insights
Different distributions serve different purposes:
Pareto is optimal for padding because it maximizes tail weight within a bounded range.
Hard caps are essential to prevent catastrophic storage growth while preserving deniability.
Deniability scales with session size: Larger known sessions provide better cover for hidden sessions.
Part 2: Implementation
1. Constants
2. Cryptographic Primitives
Use the Gossip WASM crypto module:
password_kdf(password) → keykdf(key, labels[]) → bytes[]aead_encrypt(key, plaintext) → ciphertextaead_decrypt(key, ciphertext) → plaintextAEAD (AES-256-SIV) properties:
2.1 Key Derivation
3. Addressing Blob Operations
3.1 Slot Format
3.2 Initialization
3.3 Writing a Session Address
3.4 Reading a Session Address (Timing-Safe)
4. Block and Padding Size Sampling
4.1 Block Size (Log-Normal)
4.2 Padding Size (Pareto)
4.3 Combined Allocation
5. Root Block Format
Each session has a root block containing its allocation table.
Entry size: 8 + 4 + 8 + 4 + 32 = 56 bytes
Capacity: A minimal root block (~2 MB) holds ~37,000 entries → ~1.3 TB of session data.
5.1 Block Key Derivation
6. Data Block Format
7. Session Operations
7.1 Create New Session
7.2 Unlock Existing Session (Timing-Safe)
7.3 Add Data Block
7.4 Read/Write Session Data
8. Encryption
Use AEAD (AES-256-SIV) from Gossip WASM for all encryption:
The AEAD primitive provides:
Part 3: Conclusion
Summary of Achievements
This document specifies a complete plausibly deniable multi-session encrypted storage system with the following properties:
Addressing Blob
The 46-copy redundancy ensures that even with 1024 concurrent sessions, the probability of any session losing all its address copies is less than one in a trillion.
Data Blob
Plausible Deniability
Small to medium hidden sessions (up to ~350 MB) are virtually undetectable. Even large hidden sessions benefit from significant deniability when the known session is large.
Security Properties
Final Assessment
The design satisfies all specified constraints:
The system provides strong plausible deniability for hidden sessions while maintaining reasonable storage overhead and practical I/O characteristics. The design is robust against forensic analysis, timing attacks, and operational failures. All key material is either derived or protected by authenticated encryption with fresh nonces.
Beta Was this translation helpful? Give feedback.
All reactions