Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 42 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
# Blockcrypt (beta)
# Blockcrypt

## Encrypt one or more secrets with plausible deniability by design.
## Compact multi-secret encryption

Blockcrypt is used to encrypt one or more secrets (up to 4 by default) using encrypted headers which are indistinguishable from each other, data and padding resulting in plausible deniability by design.
Blockcrypt is a compact encryption scheme for storing one or more secrets in
fixed-size ciphertext blocks, optimized for constrained media like QR codes. It
uses encrypted headers that are indistinguishable from data and noise, with
plausible deniability as a natural side effect of the design.

> Maintained and used in production by [Superbacked](https://superbacked.com/).

## Features

- **Zero dependencies**: implemented using Node.js built-ins only
- **Fixed-size ciphertext blocks**: uniform output size, regardless of number or
size of secrets
- **Multi-secret support**: encrypt one or more secrets independently
- **Strong cryptography**: ChaCha20-Poly1305 AEAD with HKDF-based key separation
- **Efficient padding**: 8-byte alignment using ISO/IEC 7816-4 style padding
- **Noise filling**: unused space is filled with random data
- **Plausible deniability**: when the first secret is revealed, no evidence of
additional secrets can be proven
- **Optimized for constrained media**: particularly suited for QR codes or
similar fixed-capacity channels

## Installation

Expand All @@ -13,42 +32,40 @@ $ npm install blockcrypt
## Usage (simplified for demonstration purposes)

```typescript
import { encrypt, decrypt, Secret } from "blockcrypt"
import { decrypt, encrypt, Secret } from "blockcrypt"

const secrets: Secret[] = [
{
message:
key: Buffer.from([
4, 72, 156, 132, 66, 216, 156, 26, 55, 162, 221, 77, 214, 13, 146, 94,
146, 239, 47, 156, 123, 68, 210, 35, 142, 146, 52, 193, 214, 82, 109, 220,
]),
message: Buffer.from(
"trust vast puppy supreme public course output august glimpse reunion kite rebel virus tail pass enhance divorce whip edit skill dismiss alpha divert ketchup",
passphrase: "lip gift name net sixth",
),
},
{
message: "this is a test\nyo",
passphrase: "grunt daisy chow barge pants",
key: Buffer.from([
158, 198, 159, 43, 229, 18, 213, 1, 55, 116, 184, 62, 75, 237, 50, 184,
123, 168, 31, 97, 208, 209, 209, 238, 42, 139, 98, 45, 31, 146, 7, 56,
]),
message: Buffer.from("this is a test\nyo"),
},
{
key: Buffer.from([
180, 252, 249, 18, 136, 98, 214, 30, 168, 200, 64, 253, 65, 47, 210, 164,
66, 60, 44, 101, 109, 239, 173, 17, 50, 217, 41, 106, 3, 129, 59, 132,
]),
message: Buffer.from("yo"),
passphrase: "decor gooey wish kept pug",
},
]

const block = await encrypt(secrets, kdf)
const block = await encrypt(secrets, 1024)

console.log(block)
// {
// salt: <Buffer 0a 89 b8 fd a1 6d 06 36 86 76 f6 e3 82 2e 54 37>,
// iv: <Buffer bb 4e 6e 86 14 1e dc d0 ed 09 fd fd ae cc 67 8a>,
// headers: <Buffer 82 a2 59 64 c4 a2 cb 3c 38 a6 88 5c f8 52 6e 45 81 0e 61 3f 93 69 0a fe 96 f7 21 ee 6c fc 2b 01 72 cc f0 0b ed 08 e3 f0 92 3f dd f4 b3 6a 5f cb ef 7f ... 14 more bytes>,
// data: <Buffer 5c 68 a6 9e 30 e2 cb 34 ed 70 7c 92 fc 57 af f6 4f 55 a4 7e 28 d5 8a 0f 39 bd fa f4 24 ad ca f9 e1 3e cb 37 89 70 d6 2e 18 1f 8d 34 30 95 42 ac a7 a2 ... 334 more bytes>
// }

const message = await decrypt(
"grunt daisy chow barge pants",
block.salt,
block.iv,
block.headers,
block.data,
kdf
)
// <Buffer 03 56 d7 03 c3 70 73 6e 4e f9 c6 85 42 3a 73 a3 53 af 0c 7e 5f 13 85 41 b6 34 84 0d 0b 85 8d 98 8f 46 f3 95 e2 76 e7 d1 0d 13 c8 26 88 68 c5 71 02 e1 ... 974 more bytes>

const message = await decrypt(secrets[1].key, block)

console.log(message)
// <Buffer 74 68 69 73 20 69 73 20 61 20 74 65 73 74 0a 79 6f>
Expand Down
6 changes: 6 additions & 0 deletions deno.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"fmt": {
"semiColons": false,
"exclude": ["dist", "package-lock.json"]
}
}
178 changes: 178 additions & 0 deletions docs/whitepaper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# Blockcrypt: Multi-secret encryption using uniform block size

## Abstract

**Blockcrypt** is a compact symmetric encryption scheme that encodes one or more
secrets into a fixed-size ciphertext block. Designed with a focus on **uniform
ciphertext size** and **efficient encoding into constrained media (e.g., QR
codes)**, it ensures that revealing the first secret does not expose or
implicate the presence of additional secrets. Plausible deniability arises as a
collateral benefit of this design.

This whitepaper outlines the cryptographic design, threat model, implementation
decisions, and deniability guarantees, referencing the TypeScript
implementation.

---

## 1. Design goal

- **Constant block size** irrespective of the number or size of secrets
- **Compact layout**, efficient for QR code storage
- **Key separation** for independent decryption
- **Deterministic message size** with 8-byte alignment
- **Plausible deniability** as a secondary effect

---

## 2. Threat model

We assume the attacker:

- Has full access to a ciphertext block
- Knows the open-source encryption scheme
- May know one valid key/secret pair (the first secret)
- Can attempt brute-force or known-key decryption

Blockcrypt must defend against:

- Discovery of additional secrets beyond the first one
- Inference of the number or size of embedded secrets
- Correlation between different ciphertexts generated by different users

---

## 3. Implementation overview

### 3.1 Secret structure

- Each secret consists of two parts:

- A **256-bit key** (recommended to be derived from a passphrase using a
cryptographically secure KDF such as Argon2 with a unique salt)
- A **message**, which is padded and encrypted

### 3.2 Block size

- The user provides a desired `blockSize` which defines the fixed size of the
ciphertext block

### 3.3 Encryption flow

1. **Key material**

- Uses the provided 256-bit key to derive header and message keys using HKDF

2. **Key splitting via HKDF**

- Derives four values:

- `headerKey`: 256-bit key for header encryption
- `headerIv`: 12-byte IV for headers
- `messageKey`: 256-bit key for message encryption
- `messageIv`: 12-byte IV for messages

3. **Padding**

- Pads plaintext using `0x80…0x00` to reach next multiple of 8 bytes

4. **Encryption**

- Each secret becomes:

- Encrypted padded message (ChaCha20-Poly1305)
- Encrypted 24-byte header indicating message position and size within the
fixed-size ciphertext block (ChaCha20-Poly1305)
- Concatenated as `header + ciphertext`

5. **Block assembly**

- All entries are concatenated
- If result is under block size, add random noise
- If result is above block size, error is thrown

### 3.4 Decryption flow

1. Derive the same `headerKey`, `headerIv`, `messageKey`, and `messageIv`
2. Scan block in 8-byte increments
3. Try to decrypt a valid 24-byte header
4. Use header to locate and decrypt message
5. Unpad and return plaintext

---

## 4. Cryptographic properties

| Property | Guarantee |
| --------------------------- | ------------------------------------------------------- |
| **Confidentiality** | ChaCha20-Poly1305 AEAD with per-function key separation |
| **Header encryption** | Hides entry size and alignment |
| **Padding** | Obscures true message size |
| **Uniform block size** | Prevents metadata leakage |
| **Independent decryption** | No shared keys between secrets |
| **Indistinguishable noise** | Padding bytes are random if unused |

---

## 5. Deniability guarantees

### Core guarantee

> Even if the first secret and its decryption key are known, the ciphertext
> block provides no cryptographic evidence that other secrets exist.

### Design mechanisms

| Mechanism | Benefit |
| ------------------------- | -------------------------------------------------- |
| **Fixed block size** | Makes large blocks unremarkable |
| **Encrypted headers** | Hides offset/length of entries |
| **Compact noise padding** | Prevents leftover space from revealing entry count |

---

## 6. Implementation highlights

- Fully modular TypeScript
- AES-free: uses ChaCha20-Poly1305 for compatibility and speed
- Supports external KDF (Argon2 recommended)
- Exported functions include: `encrypt(secrets, blockSize)`,
`decrypt(key, block)`, and supporting utilities
- `decrypt(key, block)` reveals only the first entry per key

---

## 7. Design trade-offs

| Decision | Justification |
| ---------------------- | -------------------------------------------------------------------------------------------------------------------- |
| No decoys/fake entries | Saves space for QR code encoding |
| Fixed 8-byte alignment | Enables header scanning if key is known |
| No layout shuffling | Simplifies block assembly and parsing, but leaves predictable ordering of entries if more than one secret is present |
| Zero nonce for AEAD | Safe with per-function key separation |
| No interleaved noise | Maximizes available block space |

---

## 8. Conclusion

Blockcrypt offers an efficient, compact, and cryptographically secure encryption
scheme. It ensures that users can safely store or transmit encrypted secrets in
hostile or high-risk environments. When only the first secret and its key are
revealed to an adversary, the scheme provides no cryptographic evidence about
the number, position, or existence of additional secrets.

It is particularly suited for embedding secrets in QR codes or other
media-constrained formats. Plausible deniability follows naturally from these
design choices.

---

## 9. References

- Argon2 password hashing (RFC 9106)
- ChaCha20-Poly1305 (RFC 8439)
- HKDF (RFC 5869)
- ISO/IEC 7816-4 padding

---
Loading