-
Notifications
You must be signed in to change notification settings - Fork 0
Home
Classification: Symmetric Stream Cipher · Custom Research Design
State Width: 256-bit (four 64-bit words)
Intended Use: Academic research, demonstration, security experimentation
Authors: RlxChap2 License: MIT
- Introduction
- Notation and Conventions
- Internal State: The Accumulator (ACC)
- Primitive Operations
- Key Initialization —
init_acc - Encryption Process
- Decryption Process
- Security Analysis
- Test Vectors
- Architecture Diagrams
- Comparison with AES-256
- Known Limitations and Future Work
- Compilation and Execution
- API Reference
- References
MASC-256 (Memory Accumulator Stack Cipher, 256-bit) is an experimental symmetric stream cipher whose central design thesis is that dynamic runtime memory behavior can be used as an entropy amplifier to increase diffusion without incurring the fixed-round overhead of block cipher constructions.
| Goal | Mechanism |
|---|---|
| High diffusion per byte | Accumulator round with aggressive rotations |
| Memory-dependent keystream | Stack and heap state folded into every output byte |
| Symmetric operation | Identical encrypt / decrypt logic; state synchronizes on ciphertext bytes |
| Side-channel disruption | Non-deterministic heap allocation addresses at runtime |
| Simple, auditable core | ~120 lines of portable C99 |
MASC-256 belongs to the additive stream cipher family, where a keystream is generated from a secret key and XOR'd with plaintext byte-by-byte:
Encrypt: C[i] = M[i] XOR ks[i]
Decrypt: M[i] = C[i] XOR ks[i]
Unlike conventional stream ciphers (RC4, Salsa20, ChaCha20) that evolve state purely through arithmetic, MASC-256 deliberately involves the OS memory allocator and stack frame layout in state evolution, making the keystream partially dependent on process-level memory entropy.
⚠️ Research Note: MASC-256 is a novel design that has not undergone formal peer cryptanalysis. It should not be used to protect sensitive production data without independent security audit. See Section 12.
| Symbol | Meaning |
|---|---|
XOR / ⊕
|
Bitwise XOR |
+ |
Addition modulo 2^64 |
ROTL(x, r) |
Left circular rotation of x by r bits |
ROTR(x, r) |
Right circular rotation of x by r bits |
& |
Bitwise AND |
a, b, c, d |
The four 64-bit words of accumulator state ACC |
K |
Key byte array of length L |
M[i] |
Plaintext byte at position i |
C[i] |
Ciphertext byte at position i |
ks[i] |
Keystream byte at position i |
ACC[t] |
Accumulator state at time-step t |
mod |
Modulo operation |
All word arithmetic is implicitly mod 2^64 (natural 64-bit integer overflow).
Constants are derived from fractional parts of irrational numbers, matching the SHA-2 / BLAKE2 IV strategy.
The entire cipher state is a single 256-bit structure consisting of four 64-bit words:
ACC = ( a , b , c , d )
|----64----|----64----|----64----|----64----|
256 bits total
State is seeded with magic numbers derived from fractional parts of irrational constants (same strategy as SHA-512 and BLAKE2):
| Word | Hex Value | Derivation |
|---|---|---|
a0 |
0x9E3779B97F4A7C15 |
floor( 2^64 / phi ) where phi = 1.6180… (golden ratio) |
b0 |
0x6A09E667F3BCC909 |
floor( 2^64 * frac(sqrt(2)) ) |
c0 |
0xBB67AE8584CAA73B |
floor( 2^64 * frac(sqrt(3)) ) |
d0 |
0x3C6EF372FE94F82B |
floor( 2^64 * frac(sqrt(5)) ) |
frac(x)denotes the fractional part of x. These values are well-distributed and bias-free, providing a neutral starting point for key-dependent initialization.
Every processing step — absorbing a key byte or encrypting a plaintext byte — drives a full three-stage state update:
ACC[t+1] = accumulator_round(
heap_mix(
stack_mix( ACC[t], v[t] ),
s[t] )
)
where v[t] and s[t] are step-specific seed values derived from the current input byte and the current accumulator.
ROTL(x, r) = (x << r) | (x >> (64 - r))
ROTR(x, r) = (x >> r) | (x << (64 - r))
Equivalently:
ROTL(x, r) = ROTR(x, 64 - r)
ROTR(x, r) = ROTL(x, 64 - r)
Diffusion property: A single-bit flip in x produces exactly one bit flip in ROTL(x, r), shifted to a different position. When composed with XOR and addition — which mix bits across positions — this ensures a local change propagates to all 64 bit positions across a small number of rounds.
The
accumulator_rounduses rotation offsets 17, 23, 31, 47 — all prime numbers — to avoid short internal cycles within a 64-bit word.
Purpose: Fold eight transient stack-allocated values into all four accumulator words, introducing data-dependent entropy from the processor call stack.
C Source:
void stack_mix(ACC *acc, uint64_t v) {
uint64_t stack_buf[8];
for (int i = 0; i < 8; i++) {
stack_buf[i] = rotl(v ^ (acc->a + i), i + 1);
acc->a ^= stack_buf[i];
acc->b += rotl(stack_buf[i], (i + 3) % 64);
acc->c ^= acc->b + stack_buf[i];
acc->d = rotl(acc->d ^ acc->c, 7);
}
}Per-iteration update formulas — for each i from 0 to 7:
S[i] = ROTL( v XOR (a + i), i + 1 )
a <-- a XOR S[i]
b <-- b + ROTL( S[i], (i + 3) mod 64 )
c <-- c XOR (b + S[i])
d <-- ROTL( d XOR c, 7 )
Intra-iteration dependency chain:
v, a --> S[i] --> a'
|--> b' --> c' --> d'
After all 8 iterations, every output word (a', b', c', d') is a non-linear function of all four input words and the seed v. Full intra-state dependency is guaranteed by iteration 8.
Stack entropy note: stack_buf is allocated at a runtime-determined stack address influenced by ASLR, calling conventions, and current stack depth. The computed values inside the buffer are deterministic given (v, ACC), but their memory addresses introduce microarchitectural variation (cache-line placement, prefetcher behavior) that complicates physical side-channel modelling.
Purpose: Allocate a 512-byte heap buffer at an OS-determined (non-deterministic) address, apply 64 rounds of ARX mixing into the accumulator, then release the buffer.
C Source:
void heap_mix(ACC *acc, uint64_t seed) {
uint64_t *heap = malloc(64 * sizeof(uint64_t)); // 512 bytes
for (int i = 0; i < 64; i++) {
heap[i] = rotl(seed ^ (acc->a + i), (i % 31) + 1);
acc->a += heap[i];
acc->b ^= rotl(heap[i], (i % 17) + 3);
acc->c += acc->b ^ heap[i];
acc->d ^= rotl(acc->c, (i % 13) + 5);
}
free(heap);
}Per-iteration update formulas — for each i from 0 to 63:
H[i] = ROTL( seed XOR (a + i), (i mod 31) + 1 )
a <-- a + H[i]
b <-- b XOR ROTL( H[i], (i mod 17) + 3 )
c <-- c + (b XOR H[i])
d <-- d XOR ROTL( c, (i mod 13) + 5 )
Rotation schedule — three independent moduli:
| Update target | Rotation formula | Period | Range of values |
|---|---|---|---|
H[i] seed rotation |
(i mod 31) + 1 |
31 | 1 to 31 |
b update |
(i mod 17) + 3 |
17 | 3 to 19 |
d update |
(i mod 13) + 5 |
13 | 5 to 17 |
The three moduli — 31, 17, 13 — are pairwise coprime, so the joint rotation triple (rH, rb, rd) repeats with period:
lcm(31, 17, 13) = 6851 >> 64 iterations
No two iterations within a single heap_mix call ever use the same rotation triple.
Memory-dependent entropy note: The pointer returned by malloc varies each call due to ASLR and allocator free-list state. The buffer occupies different cache lines on each invocation, making cache-timing-based side-channel attacks significantly harder to mount than classical S-box cache attacks on AES.
Purpose: Apply a final non-linear ARX (Add-Rotate-XOR) permutation across all four 64-bit state words. Structurally analogous to the permutation step in sponge constructions such as Keccak.
C Source:
void accumulator_round(ACC *acc) {
acc->a = rotl(acc->a ^ acc->d, 17);
acc->b = rotl(acc->b + acc->a, 23);
acc->c = rotl(acc->c ^ acc->b, 31);
acc->d = rotl(acc->d + acc->c, 47);
}Update formulas — applied sequentially (each line uses the updated value from the line above):
a <-- ROTL( a XOR d, 17 )
b <-- ROTL( b + a, 23 )
c <-- ROTL( c XOR b, 31 )
d <-- ROTL( d + c, 47 )
Why these rotation constants?
| Constant | Property |
|---|---|
| 17 | Prime |
| 23 | Prime |
| 31 | Prime (Mersenne prime: 2^5 - 1) |
| 47 | Prime |
All four constants are prime, avoiding short internal word cycles. The pattern alternates XOR and addition — operations from two algebraically incompatible groups (GF(2)^64 and Z/2^64 Z) — making linear approximations over either group degrade rapidly.
Single-pass full diffusion trace — a 1-bit difference in a:
Step 1: ROTL(a XOR d, 17) --> 1-bit change enters a'
Step 2: ROTL(b + a', 23) --> addition carry propagates change into b'
Step 3: ROTL(c XOR b', 31) --> change reaches c'
Step 4: ROTL(d + c', 47) --> change reaches d'
All 4 words affected after 1 call.
Key initialization maps a variable-length byte key K[0..L-1] to a unique 256-bit accumulator state. Each key byte drives one complete three-stage state update cycle.
C Source:
void init_acc(ACC *acc, const uint8_t *key, size_t len) {
acc->a = 0x9E3779B97F4A7C15ULL;
acc->b = 0x6A09E667F3BCC909ULL;
acc->c = 0xBB67AE8584CAA73BULL;
acc->d = 0x3C6EF372FE94F82BULL;
for (size_t i = 0; i < len; i++) {
acc->a ^= key[i] + rotl(acc->d, i % 63); // Key absorption
stack_mix(acc, key[i]); // Stack entropy
heap_mix(acc, key[i] ^ acc->a); // Heap entropy
accumulator_round(acc); // Non-linear round
}
}For key byte index i with value K[i]:
Step 1 — Key Absorption into word a:
a <-- a XOR ( K[i] + ROTL(d, i mod 63) )
Step 2 — Stack Mixing (8 inner iterations):
ACC <-- stack_mix( ACC, K[i] )
Step 3 — Heap Mixing with seed = K[i] XOR a (64 inner iterations):
ACC <-- heap_mix( ACC, K[i] XOR a )
Step 4 — Non-linear ARX Round:
ACC <-- accumulator_round( ACC )
( a0, b0, c0, d0 ) <-- magic constants
|
| repeat for i = 0, 1, ..., L-1
v
+----------------------------------------------+
| STEP 1 — Key Absorption |
| a <-- a XOR (K[i] + ROTL(d, i mod 63)) |
+----------------------+-----------------------+
|
v
+----------------------------------------------+
| STEP 2 — stack_mix(ACC, K[i]) |
| 8 iterations of ARX on stack buffer |
+----------------------+-----------------------+
|
v
+----------------------------------------------+
| STEP 3 — heap_mix(ACC, K[i] XOR a) |
| 64 iterations of ARX on heap buffer |
+----------------------+-----------------------+
|
v
+----------------------------------------------+
| STEP 4 — accumulator_round(ACC) |
| a <-- ROTL(a XOR d, 17) |
| b <-- ROTL(b + a, 23) |
| c <-- ROTL(c XOR b, 31) |
| d <-- ROTL(d + c, 47) |
+----------------------+-----------------------+
|
v (next key byte, or done)
Final ACC[0]
| Key length | Inner loop iterations | ARX rounds |
|---|---|---|
| 16 bytes | 16 × (8 + 64) = 1,152 | 16 |
| 17 bytes | 17 × 72 = 1,224 | 17 |
| 32 bytes | 32 × 72 = 2,304 | 32 |
| N bytes | N × 72 | N |
Key initialization cost grows linearly with key length. There is no enforced maximum — arbitrarily long keys are absorbed byte-by-byte.
void encrypt(uint8_t *data, size_t len, const uint8_t *key, size_t klen) {
ACC acc;
init_acc(&acc, key, klen);
for (size_t i = 0; i < len; i++) {
uint64_t mix = acc.a ^ acc.b ^ acc.c ^ acc.d; // 64-bit keystream word
data[i] ^= (uint8_t)(mix & 0xFF); // XOR lowest byte
stack_mix(&acc, data[i]); // State update with C[i]
heap_mix(&acc, mix ^ data[i]); // State update with mix XOR C[i]
accumulator_round(&acc);
}
}Keystream generation:
mix[i] = a XOR b XOR c XOR d (full 64-bit word)
ks[i] = mix[i] & 0xFF (lowest 8 bits)
Encryption:
C[i] = M[i] XOR ks[i]
Accumulator update — using the just-produced ciphertext byte C[i]:
ACC <-- stack_mix( ACC, C[i] )
ACC <-- heap_mix( ACC, mix[i] XOR C[i] )
ACC <-- accumulator_round( ACC )
Key --> init_acc --> ACC[0]
|
+----------------+ for i = 0, 1, ..., N-1
| |
| mix[i] = a XOR b XOR c XOR d
| ks[i] = mix[i] & 0xFF
| |
| C[i] = M[i] XOR ks[i] <-- encrypt byte
| |
| stack_mix(ACC, C[i])
| heap_mix(ACC, mix[i] XOR C[i])
| accumulator_round(ACC)
| |
+----------------+ ACC[i+1] ready for next byte
|
Output: C[0] C[1] ... C[N-1]
Printed: %02X %02X ... %02X
Each ciphertext byte is printed as a two-character uppercase hex pair to safely represent all 256 possible values:
printf("%02X", data[i]); // Example: 0xAB --> "AB"void decrypt(uint8_t *data, size_t len, const uint8_t *key, size_t klen) {
ACC acc;
init_acc(&acc, key, klen);
for (size_t i = 0; i < len; i++) {
uint64_t mix = acc.a ^ acc.b ^ acc.c ^ acc.d;
uint8_t cipher = data[i]; // Save raw ciphertext byte C[i]
data[i] ^= (uint8_t)(mix & 0xFF); // Recover plaintext M[i]
stack_mix(&acc, cipher); // State update with C[i] (same as encrypt)
heap_mix(&acc, mix ^ cipher);
accumulator_round(&acc);
}
}Keystream generation (identical to the encryption path):
mix[i] = a XOR b XOR c XOR d
ks[i] = mix[i] & 0xFF
Decryption:
M[i] = C[i] XOR ks[i]
Accumulator update — using the saved ciphertext byte C[i]:
ACC <-- stack_mix( ACC, C[i] )
ACC <-- heap_mix( ACC, mix[i] XOR C[i] )
ACC <-- accumulator_round( ACC )
The cipher is symmetric because the state update in both directions consumes the ciphertext byte C[i], not the plaintext. This guarantees that ACC[i+1] is identical on both paths given the same key.
Claim: ACC_encrypt[i+1] = ACC_decrypt[i+1] for all i
Proof (by induction):
Base case: ACC[0] is identical — same key, same init_acc call.
Inductive step:
Encrypt feeds C[i] = M[i] XOR ks[i] into the state update.
Decrypt saves C[i] before XOR, feeds the same C[i] into state update.
Therefore the state transitions are identical --> ACC[i+1] matches. QED
Correctness of decryption:
M[i] = C[i] XOR ks[i]
= (M[i] XOR ks[i]) XOR ks[i]
= M[i] XOR (ks[i] XOR ks[i])
= M[i] XOR 0
= M[i] (correct)
⚠️ Disclaimer: The analysis below describes design-intent properties based on structural inspection. None of these properties have been formally proven or validated by independent cryptanalysis. MASC-256 is a research cipher; treat all security claims as theoretical until external review confirms them.
The keystream word at step i is:
mix[i] = a[i] XOR b[i] XOR c[i] XOR d[i]
ks[i] = mix[i] & 0xFF
If the four 64-bit words were independently and uniformly distributed, their XOR would also be uniform:
H(ks[i]) = 8 bits (maximum entropy for a single byte)
The four words are not strictly independent — they share a common update path — but the combination of XOR over GF(2)^64 and addition over Z/2^64 Z makes linear correlation between consecutive keystream bytes computationally hard to exploit.
A 1-bit change in a key byte or plaintext byte should, after one full state update cycle, produce approximately 50% differing bits in the 256-bit state:
Expected Hamming distance from 1-bit input change:
~128 bits out of 256 (approx. 50%)
Propagation trace through accumulator_round alone:
a' = ROTL(a XOR d, 17) --> 1-bit change in a exits as 1-bit change in a'
b' = ROTL(b + a', 23) --> addition carry chain propagates change into b'
c' = ROTL(c XOR b', 31) --> change reaches c'
d' = ROTL(d + c', 47) --> change reaches d'
Result: all 4 words affected after 1 accumulator_round call.
After the preceding 64 iterations of heap_mix, the change has already spread extensively before accumulator_round executes.
Linear cryptanalysis searches for affine approximations of the form:
L(P) XOR L(C) = 0 with probability p != 1/2
Structural mitigations in MASC-256:
-
Mixed algebraic groups: XOR operates over GF(2)^64; addition operates over Z/2^64 Z. These groups are algebraically incompatible — a linear approximation that holds over one group breaks down in the other. Every
heap_mixiteration combines both operations. -
64 compounding iterations: A linear bias
eper iteration compounds to approximatelye^64after 64 rounds — negligibly small for any realistice. -
State feedback: The keystream byte at position
i+1depends on the full state after processing bytei, including the ciphertext byteC[i]. A linear distinguisher must account for the full feedback chain, increasing required data exponentially.
Differential cryptanalysis tracks how a plaintext difference deltaM propagates to a ciphertext difference deltaC.
Key structural property: The accumulator is updated with the ciphertext byte C[i], not the plaintext M[i].
For two plaintexts M and M' differing at position i:
C[i] = M[i] XOR ks[i]
C'[i] = M'[i] XOR ks[i] (same ks because ACC[i] is the same)
deltaC[i] = C[i] XOR C'[i] = M[i] XOR M'[i] = deltaM[i]
Both ciphertexts differ at C[i], and these differing bytes drive divergent state updates, meaning ACC[i+1] != ACC'[i+1]. A differential characteristic valid at step i does not extend to step i+1 with high probability, defeating multi-step differential trails.
The malloc call inside heap_mix introduces runtime variability:
| Factor | Effect on side-channel attack surface |
|---|---|
| ASLR randomizes heap base | Heap buffer occupies unpredictable cache lines |
| Allocator free-list state |
malloc timing varies across calls |
| 512-byte buffer access pattern | Cache-line coverage changes per call |
A timing side-channel would require an adversary to measure malloc execution time, correlate that to the returned heap address, and infer seed or acc->a. In practice, ASLR and allocator non-determinism make this significantly harder than classical AES S-box cache attacks.
Caveat: No formal cache-timing or power analysis has been performed. These are claimed structural properties only.
For a key of length L bytes drawn from the full 256-symbol alphabet:
|K| = 256^L = 2^(8 * L)
| Key length | Key space cardinality |
|---|---|
| 16 bytes | 2^128 |
| 32 bytes | 2^256 (equal cardinality to AES-256) |
| 64 bytes | 2^512 |
To recover the 256-bit state from observed keystream bytes, an adversary must:
- Collect keystream bytes
ks[0], ks[1], ..., ks[n]. - Invert
ncoupled ARX state transitions (each involving 64 heap rounds + 8 stack rounds + 1 ARX round). - Search a space of
2^256candidate initial states.
The heap_mix function frees its buffer after each call — the heap addresses used during computation are irrecoverable post-execution. Direct algebraic inversion is infeasible; brute-force complexity remains O(2^256).
Key (ASCII): UltraEncryptedKey
Key (hex): 55 6C 74 72 61 45 6E 63 72 79 70 74 65 64 4B 65 79
Key length: 17 bytes
Plaintext (ASCII): Advanced Memory Encryption System
Plaintext (hex): 41 64 76 61 6E 63 65 64 20 4D 65 6D 6F 72 79 20
45 6E 63 72 79 70 74 69 6F 6E 20 53 79 73 74 65 6D
Plaintext length: 33 bytes
$ ./build/Debug/acc_crypto
Encrypted: <66 uppercase hex characters — 33 ciphertext bytes>
Decrypted: Advanced Memory Encryption System
Compile and run
main.cto generate the live encrypted hex value. The ciphertext bytes are deterministic within a single process (heap values are computed, not random), but may differ across platforms if compiler optimization changes stack layout. Decryption must always recover the original plaintext.
Run the binary twice and compare outputs:
./build/Debug/acc_crypto > run1.txt
./build/Debug/acc_crypto > run2.txt
diff run1.txt run2.txt # Must show no differencesA 1-character change to the key must produce a completely different ciphertext. The expected Hamming weight of A XOR B for a 33-byte message:
Expected Hamming distance: ~132 bits out of 264 bits (~50%)
| Key | Expected result |
|---|---|
UltraEncryptedKey |
Ciphertext A |
UltraEncryptedKex |
Completely different ciphertext B |
ultraEncryptedKey |
Completely different ciphertext C |
+------------------------------------------------------------+
| ACCUMULATOR (256 bits) |
| +----------+ +----------+ +----------+ +----------+ |
| | a (64b) | | b (64b) | | c (64b) | | d (64b) | |
| +----+-----+ +----+-----+ +----+-----+ +----+-----+ |
+-------+-------------+-------------+-------------+---------+
| | | |
+-------------+-------------+-------------+
|
a XOR b XOR c XOR d
|
& 0xFF = ks[i]
|
M[i] --XOR--> C[i] (encrypt)
C[i] --XOR--> M[i] (decrypt)
|
+-----------------------+------------------------+
| stack_mix(ACC, v) |
| for i in 0..7: |
| S[i] = ROTL(v XOR (a+i), i+1) |
| a XOR= S[i] |
| b += ROTL(S[i], (i+3) mod 64) |
| c XOR= (b + S[i]) |
| d = ROTL(d XOR c, 7) |
+-----------------------+------------------------+
|
+-----------------------+------------------------+
| heap_mix(ACC, seed) |
| heap = malloc(64 x 8 bytes) |
| for i in 0..63: |
| H[i] = ROTL(seed XOR (a+i), (i%31)+1) |
| a += H[i] |
| b XOR= ROTL(H[i], (i%17)+3) |
| c += b XOR H[i] |
| d XOR= ROTL(c, (i%13)+5) |
| free(heap) |
+-----------------------+------------------------+
|
+-----------------------+------------------------+
| accumulator_round(ACC) |
| a <-- ROTL(a XOR d, 17) |
| b <-- ROTL(b + a, 23) |
| c <-- ROTL(c XOR b, 31) |
| d <-- ROTL(d + c, 47) |
+-----------------------+------------------------+
|
ACC[i+1] --> next byte
ENCRYPT PATH DECRYPT PATH
-------------------- --------------------
ACC[i] (same key) ACC[i] (same key)
| |
v v
mix = a XOR b XOR c XOR d mix = a XOR b XOR c XOR d
ks = mix & 0xFF ks = mix & 0xFF
| |
v v
C[i] = M[i] XOR ks save C[i]; M[i] = C[i] XOR ks
| |
v v
stack_mix(ACC, C[i]) <-same-> stack_mix(ACC, C[i])
heap_mix(ACC, mix XOR C[i]) heap_mix(ACC, mix XOR C[i])
accumulator_round(ACC) accumulator_round(ACC)
| |
v v
ACC[i+1] = ACC[i+1] <-- guaranteed identical
malloc(64 x 8 = 512 bytes)
+--------+--------+--------+-----+----------+-----+----------+
| heap[0]| heap[1]| heap[2]| ... | heap[31] | ... | heap[63] |
| H(0) | H(1) | H(2) | | H(31) | | H(63) |
+--------+--------+--------+-----+----------+-----+----------+
| |
+------------------+-------------------------------+
| for each H(i):
| a += H(i)
| b XOR= ROTL(H(i), (i%17)+3)
| c += b XOR H(i)
| d XOR= ROTL(c, (i%13)+5)
v
free(heap) --> address discarded
| Property | AES-256 | MASC-256 |
|---|---|---|
| Cipher type | Block cipher (128-bit block) | Stream cipher (1-byte per step) |
| Key length | Fixed 256 bits | Variable (any length) |
| Internal state | 128 bits (4x4 byte matrix) | 256 bits (4x64-bit words) |
| Rounds per block/byte | 14 fixed rounds | 1 stack (8 iter) + 1 heap (64 iter) + 1 ARX |
| Inner loop work per byte | ~14 x AES round operations | 73 inner iterations + 1 ARX round |
| Parallelizability | CTR / GCM modes | Inherently sequential |
| Formal security proof | Yes (standard model) | None |
| NIST standardization | FIPS 197 | None |
| Side-channel hardening | AES-NI constant-time | Memory-address variation (partial) |
| Hardware acceleration | AES-NI instruction set | None |
| Public cryptanalysis | 25+ years, no key recovery found | None |
| Memory allocations | Zero | 1 x 512 bytes per plaintext byte |
| Authentication (AEAD) | GCM mode available | Not implemented |
AES-256 holds its security reputation because of:
- 25+ years of open public cryptanalysis with no successful key-recovery attack published.
- Hardware acceleration (AES-NI) making it among the fastest ciphers on modern CPUs.
- Formal security reductions and proofs under standard cryptographic assumptions.
- NIST standardization with audited implementation requirements.
MASC-256's design properties — 256-bit state, memory-dependent entropy, mixed ARX structure, variable-length key absorption — are sound structural choices. However, structural soundness is not the same as proven security. A cipher is not stronger than AES-256 until it survives equivalent public scrutiny over time.
Recommended validation roadmap:
- Publish openly (this document is step one).
- Submit to the IACR ePrint Archive for community review.
- Run the full NIST SP 800-22 statistical test suite on the raw keystream.
- Commission a formal cryptanalysis engagement from independent researchers.
- Pursue a standardization track if cryptanalysis results are favorable.
| Limitation | Description | Impact |
|---|---|---|
| No IV / Nonce | Same key always produces the same keystream | Nonce-reuse: XOR two ciphertexts reveals M1 XOR M2
|
| Sequential only | Each byte's state depends on all prior bytes | Cannot parallelize; no random access within ciphertext |
| malloc per byte | 512-byte allocation per plaintext byte | High allocator overhead — far slower than AES on bulk data |
| No authentication | Pure encryption with no integrity check | Ciphertext can be tampered undetected |
| No formal analysis | All security properties are unverified claims | True security margin is unknown |
| Deterministic heap values | Buffer contents are computable; only addresses vary | Side-channel resistance weaker than implied |
-
Add Nonce / IV — Pass a 128-bit random nonce into
init_accand mix it in before key absorption. Each message then produces a unique keystream even under key reuse. -
Implement AEAD mode — Append a 128-bit authentication tag (Poly1305 or GHASH) to detect tampering before decryption.
-
Batch heap allocation — Allocate the heap buffer once per
encrypt/decryptcall and pass it intoheap_mix, eliminating per-bytemallocoverhead. -
Expand keystream extraction — Use all 64 bits of
mixto encrypt 8 bytes per state update, increasing throughput 8x without extra state computation. -
Statistical testing — Run NIST SP 800-22 and the TestU01 Crush battery on a large keystream sample to verify pseudorandomness properties.
-
Constant-time implementation — Audit rotation, XOR, and addition paths for data-dependent timing; add mitigations where found.
- Compiler: GCC >= 4.8, Clang >= 3.5, or MSVC 2019+ (C99 required)
- Build system: CMake >= 3.10
-
Standard library:
stdlib.h,stdint.h,stddef.h(all standard C)
# Configure the build
cmake -B build -S .
# Compile
cmake --build build
# Execute
./build/Debug/acc_cryptomake all # Configure + compile
make run # Compile + executeEncrypted: <66 uppercase hex characters — 33 bytes>
Decrypted: Advanced Memory Encryption System
Include acc_crypto.h, compile and link acc_crypto.c and memory_mix.c:
#include "acc_crypto.h"
uint8_t key[] = "MySecretKey";
uint8_t data[] = "Hello, World!";
size_t dlen = sizeof(data) - 1;
size_t klen = sizeof(key) - 1;
// Encrypt in-place
encrypt(data, dlen, key, klen);
// data[] now contains ciphertext bytes
// Decrypt in-place (same key)
decrypt(data, dlen, key, klen);
// data[] restored to "Hello, World!"void init_acc(ACC *acc, const uint8_t *key, size_t len);Initializes the 256-bit accumulator acc from key key of byte-length len.
Must be called before any encrypt or decrypt operation.
void encrypt(uint8_t *data, size_t len, const uint8_t *key, size_t klen);Encrypts len bytes in data in-place using key.
Internally calls init_acc, then processes each byte with stack_mix → heap_mix → accumulator_round.
void decrypt(uint8_t *data, size_t len, const uint8_t *key, size_t klen);Decrypts len bytes in data in-place using key.
Identical structure to encrypt; saves each ciphertext byte before overwriting to maintain synchronized state.
void stack_mix(ACC *acc, uint64_t v);Applies 8 rounds of stack-entropy ARX mixing to acc using seed value v.
void heap_mix(ACC *acc, uint64_t seed);Allocates a 512-byte heap buffer, applies 64 rounds of ARX mixing to acc using seed, then frees the buffer.
void accumulator_round(ACC *acc);Applies one 4-word ARX permutation round to acc using rotation constants 17, 23, 31, 47.
uint64_t rotl(uint64_t x, int r);
uint64_t rotr(uint64_t x, int r);64-bit circular left / right rotation of x by r bit positions.
- Repository Mapping
- Parameter Summary
- Notation
- Threat Model
- Security Goals and Non-Goals
- Construction Overview
- Public Message Format
- Key Schedule and Domain Separation
- Accumulator State
- Primitive Operations
- State Absorption
- Accumulator Initialization
- Keystream Generation
- Encryption Core
- Authentication Core
- Public API
- Error Semantics
- Reference Test Vector
- Implementation Security Notes
- Validation and Analysis Program
- Known Limitations
- Public Positioning and Evaluation Scope
- References
- Release Conformance
- Appendix A: Complete Seal Algorithm
- Appendix B: Complete Open Algorithm
- Appendix C: Core Stream Algorithm