Skip to content

MASC‐256 Academic Reference

!? ReLaX edited this page Jun 13, 2026 · 1 revision

MASC-256 is the Memory Accumulator Stack Cipher, 256-bit. Version 2 is an experimental authenticated stream-cipher construction that combines a custom 256-bit accumulator-based keystream generator with nonce-based HMAC-SHA256 key separation and Encrypt-then-MAC authentication.

This document is the public academic reference for the current MASC-256 design and implementation.

Important

Release status: MASC-256 v2 is a public release specification for an experimental authenticated stream-cipher construction. Its security posture is defined by this document, the reference implementation, and the published test vectors. It is not a standardized or independently certified primitive.


Abstract

MASC-256 v2 defines an authenticated encryption interface over a custom stateful stream generator. For each message, the caller supplies a secret master key and optional associated data. The implementation generates a 128-bit nonce, derives independent stream and MAC subkeys using an HMAC-SHA256 based extract-and-expand process, initializes a 256-bit accumulator state, encrypts the plaintext by XORing it with 64-bit keystream blocks, feeds ciphertext blocks back into the accumulator, and authenticates the nonce, associated data, and ciphertext using a 256-bit HMAC-SHA256 tag.

The primary public interface is masc256_seal and masc256_open, which encode messages as:

$$ sealed = N \parallel C \parallel T $$

where N is a 16-byte nonce, C is the ciphertext, and T is a 32-byte authentication tag.


Scope

This specification defines the public MASC-256 v2 construction, message format, mathematical notation, reference algorithm, public API, reference test vector, and security boundary. It is intended to be sufficient for implementation review, independent reimplementation, and cryptanalytic evaluation.

Audience Specification material
Implementers Exact API contracts, byte formats, constants, and pseudocode
Security reviewers Security goals, non-goals, threat model, and attack surfaces
Cryptanalysts State transition equations, constants, test vectors, and validation methodology

The document uses release-grade language while preserving a precise security boundary. The construction is specified as released and testable; it is not specified as externally standardized or certified.


Table of Contents

  1. Repository Mapping
  2. Parameter Summary
  3. Notation
  4. Threat Model
  5. Security Goals and Non-Goals
  6. Construction Overview
  7. Public Message Format
  8. Key Schedule and Domain Separation
  9. Accumulator State
  10. Primitive Operations
  11. State Absorption
  12. Accumulator Initialization
  13. Keystream Generation
  14. Encryption Core
  15. Authentication Core
  16. Public API
  17. Error Semantics
  18. Reference Test Vector
  19. Implementation Security Notes
  20. Validation and Analysis Program
  21. Known Limitations
  22. Public Positioning and Evaluation Scope
  23. References
  24. Release Conformance
  25. Appendix A: Complete Seal Algorithm
  26. Appendix B: Complete Open Algorithm
  27. Appendix C: Core Stream Algorithm

Release Classification

Category Current status
Public specification Released in this document
Reference implementation Present in C
Known-answer tests Present for MASC ciphertext and tag
Independent implementation Pending external artifact
Statistical keystream testing Pending external report
Third-party cryptanalysis Pending external report
Standardization status Not standardized or externally certified

Repository Mapping

The current implementation is organized as follows:

Path Role
include/acc_crypto.h Public constants and API declarations
src/acc_crypto.c SHA-256, HMAC-SHA256, key schedule, AEAD wrapper, stream core
src/memory_mix.c 64-bit rotations, stack mixing, scratch mixing, accumulator round
src/masc256_internal.h Internal accumulator type and internal primitive declarations
src/main.c Demonstration program using masc256_seal and masc256_open
tests/test_masc256.c Known-answer and tamper-detection tests
CMakeLists.txt Build and test configuration
.github/workflows/ci.yml Continuous integration checks

The public header intentionally does not expose the internal ACC state or the legacy unauthenticated encryption interface.


Parameter Summary

Parameter Symbol Value
Version MASC256_VERSION 2
Accumulator state size ` ACC
Accumulator words (a,b,c,d) 4 x 64-bit
Nonce size ` N
Authentication tag size ` T
Public overhead ` N
Stream subkey size ` K_enc
MAC subkey size ` K_mac
Keystream block size ` Z_i
Scratch words MASC256_HEAP_WORDS 64 x 64-bit
Authentication function MAC HMAC-SHA256
Authentication mode mode Encrypt-then-MAC

The public sealed length is:

$$ |sealed| = |P| + |N| + |T| = |P| + 48 $$


Notation

All 64-bit word arithmetic is performed modulo (2^{64}).

Notation Meaning
(x \oplus y) Bitwise XOR
(x + y) Addition modulo (2^{64})
(x \parallel y) Byte-string concatenation
ROTL64(x,r) 64-bit left rotation
ROTR64(x,r) 64-bit right rotation
LE64(x) 64-bit little-endian encoding
Load64LEPartial(B) Little-endian load of 1 to 8 bytes
A64(x) 64-bit avalanche/finalizer
HMAC(k,m) HMAC-SHA256
K Caller-supplied master key
N Public nonce
AAD Associated authenticated data
P Plaintext
C Ciphertext
T Authentication tag
Z_i 64-bit keystream word for block i

Accumulator state:

$$ ACC = (a,b,c,d), \quad a,b,c,d \in {0,1}^{64} $$

Sealed message:

$$ sealed = N \parallel C \parallel T $$


Threat Model

MASC-256 v2 assumes an adversary may:

  • Observe any number of sealed messages.
  • Choose plaintexts and AAD values for encryption queries.
  • Modify sealed messages before decryption.
  • Replay old sealed messages.
  • Know the full algorithm and source code.
  • Know every nonce, ciphertext, tag, and AAD value.

MASC-256 v2 assumes the adversary does not know:

  • The master key K.
  • The derived stream key K_enc.
  • The derived MAC key K_mac.
  • Internal accumulator state during execution.

Out of scope for the current implementation:

  • Key exchange.
  • Password hashing.
  • Secure key storage.
  • Replay prevention.
  • Hardware side-channel certification.
  • Fault injection resistance.
  • Nonce-misuse resistance.

Security Goals and Non-Goals

Intended Goals

With a secret key and unique nonce per message, the design intends to provide:

Goal Intended mechanism
Confidentiality Stream encryption under a per-message stream subkey
Integrity HMAC-SHA256 tag over nonce, AAD, and ciphertext
AAD binding AAD included with explicit length in the MAC input
Domain separation Separate labels for stream key, MAC key, and tag input
Tamper rejection Authentication verified before plaintext is returned
API misuse reduction masc256_seal generates and stores the nonce

Non-Goals

The current design does not claim:

  • Formal IND-CPA proof for the custom stream generator.
  • Formal IND-CCA proof for the full construction.
  • Resistance to nonce reuse.
  • Resistance to all timing, cache, power, or fault attacks.
  • Standardization-level assurance.
  • Drop-in replacement status for AES-GCM or ChaCha20-Poly1305.

Construction Overview

Full Seal Pipeline

flowchart TD
    P["Plaintext P"] --> COPY["Copy P into ciphertext buffer"]
    K["Master key K"] --> DERIVE["DeriveKeys(K, N)"]
    RNG["System RNG"] --> N["Nonce N (16 bytes)"]
    N --> DERIVE
    DERIVE --> KENC["Stream key K_enc"]
    DERIVE --> KMAC["MAC key K_mac"]
    KENC --> INIT["InitACC(K_enc, N)"]
    N --> INIT
    INIT --> STREAM["Generate MASC keystream"]
    STREAM --> XOR["XOR keystream with plaintext"]
    COPY --> XOR
    XOR --> C["Ciphertext C"]
    KMAC --> HMAC["Compute HMAC-SHA256 tag"]
    N --> HMAC
    AAD["Associated data AAD"] --> HMAC
    C --> HMAC
    HMAC --> T["Tag T (32 bytes)"]
    N --> OUT["sealed = N || C || T"]
    C --> OUT
    T --> OUT
Loading

Full Open Pipeline

flowchart TD
    SEALED["sealed input"] --> PARSE["Parse N, C, T"]
    PARSE --> N["Nonce N"]
    PARSE --> C["Ciphertext C"]
    PARSE --> T["Received tag T"]
    K["Master key K"] --> DERIVE["DeriveKeys(K, N)"]
    N --> DERIVE
    DERIVE --> KENC["Stream key K_enc"]
    DERIVE --> KMAC["MAC key K_mac"]
    KMAC --> MAC["Compute expected tag"]
    N --> MAC
    AAD["AAD"] --> MAC
    C --> MAC
    MAC --> TE["Expected tag T'"]
    T --> CMP["Constant-time comparison"]
    TE --> CMP
    CMP -->|"T == T'"| DEC["Decrypt C using K_enc,N"]
    CMP -->|"T != T'"| FAIL["Return MASC256_ERR_AUTH"]
    DEC --> P["Plaintext P"]
Loading

Data Dependency Graph

flowchart LR
    K["K"] --> PRK["PRK"]
    N["N"] --> PRK
    PRK --> ENC["K_enc"]
    PRK --> MACKEY["K_mac"]
    ENC --> ACC["ACC_0"]
    N --> ACC
    ACC --> KS["Keystream"]
    P["P"] --> C["C"]
    KS --> C
    MACKEY --> T["T"]
    N --> T
    AAD["AAD"] --> T
    C --> T
    N --> SEALED["N || C || T"]
    C --> SEALED
    T --> SEALED
Loading

Public Message Format

The sealed message layout is fixed:

offset        size              field
------------  ----------------  --------------------------
0             16 bytes          nonce N
16            |P| bytes         ciphertext C
16 + |P|      32 bytes          authentication tag T

Formally:

$$ sealed = N \parallel C \parallel T $$

$$ N = sealed[0..15] $$

$$ C = sealed[16..|sealed|-33] $$

$$ T = sealed[|sealed|-32..|sealed|-1] $$

Minimum valid sealed length:

$$ |sealed|_{\min} = |N| + |T| = 48 $$

Message Format Diagram

flowchart LR
    N["0..15<br/>Nonce N<br/>16 bytes"]
    C["16..16+|P|-1<br/>Ciphertext C<br/>variable length"]
    T["16+|P|..47+|P|<br/>Tag T<br/>32 bytes"]
    N --> C --> T
Loading

Key Schedule and Domain Separation

MASC-256 v2 derives independent subkeys from the master key and nonce.

Definitions:

I_enc = "MASC-256 v2 stream key"
I_mac = "MASC-256 v2 authentication key"

Extraction:

$$ PRK = HMAC_{SHA256}(N, K) $$

Expansion:

$$ K_{enc} = HKDF_Expand(PRK, I_{enc}, 32) $$

$$ K_{mac} = HKDF_Expand(PRK, I_{mac}, 32) $$

HKDF-Style Expansion

For an output length of 32 bytes, the implementation computes one block:

$$ T_1 = HMAC_{SHA256}(PRK, I \parallel 0x01) $$

$$ OKM = T_1[0..31] $$

The implementation supports longer outputs using the standard chained form:

$$ T_i = HMAC_{SHA256}(PRK, T_{i-1} \parallel I \parallel i) $$

Key Schedule Diagram

flowchart TD
    K["Master key K"] --> EXTRACT["PRK = HMAC-SHA256(N, K)"]
    N["Nonce N"] --> EXTRACT
    EXTRACT --> EXP1["HKDF-Expand(PRK, I_enc, 32)"]
    EXTRACT --> EXP2["HKDF-Expand(PRK, I_mac, 32)"]
    EXP1 --> KENC["K_enc"]
    EXP2 --> KMAC["K_mac"]
    KENC --> STREAM["Stream generator"]
    KMAC --> AUTH["HMAC tag"]
Loading

Rationale

The stream generator and authentication function must not share the same key material. Domain-separated subkeys prevent cross-protocol and cross-purpose key reuse inside the construction.


Accumulator State

The custom stream generator maintains a 256-bit state:

typedef struct {
    uint64_t a;
    uint64_t b;
    uint64_t c;
    uint64_t d;
} ACC;

Mathematically:

$$ ACC_i = (a_i,b_i,c_i,d_i) $$

with:

$$ a_i,b_i,c_i,d_i \in \mathbb{Z}/2^{64}\mathbb{Z} $$

Base Constants

The base state is initialized with four 64-bit constants XORed with v2 domain constants:

Word Base Domain mask Initial word
a 0x9e3779b97f4a7c15 0x4d41534332353632 base_a ^ mask_a
b 0x6a09e667f3bcc909 0x56325f4145435400 base_b ^ mask_b
c 0xbb67ae8584caa73b 0x484b44465f484d41 base_c ^ mask_c
d 0x3c6ef372fe94f82b 0x5348413235360000 base_d ^ mask_d

State Lifecycle

stateDiagram-v2
    [*] --> BaseConstants
    BaseConstants --> NonceAbsorption: absorb N
    NonceAbsorption --> KeyAbsorption: absorb K_enc
    KeyAbsorption --> Finalization: 12 absorb rounds
    Finalization --> Ready
    Ready --> Keystream: generate Z_i
    Keystream --> CipherFeedback: absorb C_i
    CipherFeedback --> Ready: next block
Loading

Constant Selection and Domain Labels

MASC-256 v2 uses fixed public constants. These constants are not secret and do not contribute hidden entropy. Their purpose is to define a reproducible, non-zero, version-separated starting state and to prevent accidental overlap between different absorption contexts.

Design Requirements for Constants

The constants used in the accumulator initialization are selected to satisfy the following engineering criteria:

Requirement Reason
Public and reproducible Prevents hidden-parameter security claims
Non-zero and high Hamming weight Avoids weak all-zero or sparse initial states
Derived from recognizable sources Provides "nothing-up-my-sleeve" style transparency
Domain-separated by ASCII labels Makes v2 state initialization distinct from earlier variants
Fixed across implementations Enables stable test vectors and cross-language ports

Base Constants

The base constants are:

Word Constant Source family Rationale
a 0x9e3779b97f4a7c15 Golden-ratio Weyl constant Widely used 64-bit additive constant with strong bit dispersion in integer mixers
b 0x6a09e667f3bcc909 SHA-2 square-root IV family Based on the SHA-512 initial-value family, with a one-bit low-end variant in the implementation
c 0xbb67ae8584caa73b SHA-2 square-root IV family Standard high-diffusion 64-bit IV-style constant
d 0x3c6ef372fe94f82b SHA-2 square-root IV family Standard high-diffusion 64-bit IV-style constant

The first constant has the well-known form:

$$ 0x9e3779b97f4a7c15 = \left\lfloor \frac{2^{64}}{\varphi} \right\rfloor $$

where:

$$ \varphi = \frac{1+\sqrt{5}}{2} $$

This value is commonly used in Weyl sequences and non-cryptographic integer mixers because repeated addition by this constant walks through 64-bit space with good equidistribution properties. In MASC-256, it is not used as a Weyl sequence directly; it is used as a transparent high-dispersion initialization word.

The SHA-2-family constants are used for the same reason SHA-2 uses irrational-derived initialization values: they provide public, reproducible 64-bit words with no secret selection process. MASC-256 does not rely on the security of SHA-512's IV; it uses the constants as neutral initial material before nonce and key absorption.

Version and Context Masks

Each base constant is XORed with a fixed 64-bit mask. These masks are readable as big-endian ASCII labels:

Word Mask ASCII interpretation Purpose
a 0x4d41534332353632 MASC2562 Binds the state to the MASC-256 v2 line
b 0x56325f4145435400 V2_AECT\\0 Marks the authenticated-encryption construction context
c 0x484b44465f484d41 HKDF_HMA Marks the HMAC/HKDF key-separation context
d 0x5348413235360000 SHA256\\0\\0 Marks the SHA-256/HMAC dependency context

The initialized state is therefore:

$$ a_0 = 0x9e3779b97f4a7c15 \oplus 0x4d41534332353632 $$

$$ b_0 = 0x6a09e667f3bcc909 \oplus 0x56325f4145435400 $$

$$ c_0 = 0xbb67ae8584caa73b \oplus 0x484b44465f484d41 $$

$$ d_0 = 0x3c6ef372fe94f82b \oplus 0x5348413235360000 $$

Why XOR Domain Masks Are Used

XORing the masks into the base constants has three practical effects:

  1. The v2 initial state cannot accidentally equal the v1 initial state.
  2. The constants encode the construction family directly in the state.
  3. Independent implementations can verify the state derivation without relying on comments or external metadata.

The masks are not secret and are not a substitute for cryptographic domain separation in the key schedule or tag input. They are structural identifiers for the accumulator initialization layer.

flowchart LR
    B1["Golden-ratio / SHA-family base constants"] --> XOR["XOR with ASCII domain masks"]
    M["MASC2562 / V2_AECT / HKDF_HMA / SHA256"] --> XOR
    XOR --> S["Version-separated base ACC state"]
    S --> N["Nonce absorption"]
    N --> K["Stream-key absorption"]
    K --> F["Finalization rounds"]
Loading

Primitive Operations

64-bit Rotation

Implementation:

uint64_t rotl(uint64_t x, int r) {
    r &= 63;
    return r == 0 ? x : (x << r) | (x >> (64 - r));
}

Formal definition:

$$ ROTL64(x,r) = ((x \ll r) \lor (x \gg (64-r))) \bmod 2^{64} $$

with:

$$ r = r \bmod 64 $$

The implementation explicitly handles r = 0 to avoid undefined C behavior for 64-bit shifts.

64-bit Avalanche Function

Implementation:

A64(x):
    x = x ^ (x >> 30)
    x = x * 0xbf58476d1ce4e5b9
    x = x ^ (x >> 27)
    x = x * 0x94d049bb133111eb
    x = x ^ (x >> 31)
    return x

Formal expression:

$$ A64(x) = ((((x \oplus (x \gg 30)) \cdot c_1) \oplus ((((x \oplus (x \gg 30)) \cdot c_1) \gg 27))) \cdot c_2) \oplus (((((x \oplus (x \gg 30)) \cdot c_1) \oplus ((((x \oplus (x \gg 30)) \cdot c_1) \gg 27))) \cdot c_2) \gg 31) $$

where:

$$ c_1 = 0xbf58476d1ce4e5b9 $$

$$ c_2 = 0x94d049bb133111eb $$

Security note:

A64 is a diffusion primitive, not a standalone cryptographic primitive. Its use in MASC-256 must be evaluated as part of the complete state transition.

StackMix

Pseudocode:

StackMix(ACC, v):
    for i = 0 to 7:
        S = ROTL64(v ^ (a + i), i + 1)
        a = a ^ S
        b = b + ROTL64(S, (i + 3) mod 64)
        c = c ^ (b + S)
        d = ROTL64(d ^ c, 7)
    return ACC

Per-iteration equations:

$$ S_i = ROTL64(v \oplus (a_i + i), i+1) $$

$$ a_{i+1} = a_i \oplus S_i $$

$$ b_{i+1} = b_i + ROTL64(S_i, (i+3) \bmod 64) $$

$$ c_{i+1} = c_i \oplus (b_{i+1} + S_i) $$

$$ d_{i+1} = ROTL64(d_i \oplus c_{i+1}, 7) $$

HeapMixScratch

The v2 implementation uses a caller-provided scratch buffer of 64 64-bit words. The name preserves the original design history, but the current function does not allocate memory inside the encryption loop.

Pseudocode:

HeapMixScratch(ACC, seed, scratch[64]):
    for i = 0 to 63:
        H = ROTL64(seed ^ (a + i), (i mod 31) + 1)
        scratch[i] = H
        a = a + H
        b = b ^ ROTL64(H, (i mod 17) + 3)
        c = c + (b ^ H)
        d = d ^ ROTL64(c, (i mod 13) + 5)
    return ACC

Equations:

$$ H_i = ROTL64(seed \oplus (a_i + i), (i \bmod 31)+1) $$

$$ a_{i+1} = a_i + H_i $$

$$ b_{i+1} = b_i \oplus ROTL64(H_i, (i \bmod 17)+3) $$

$$ c_{i+1} = c_i + (b_{i+1} \oplus H_i) $$

$$ d_{i+1} = d_i \oplus ROTL64(c_{i+1}, (i \bmod 13)+5) $$

Rotation periods:

Component Rotation expression Period
Scratch word generation (i mod 31) + 1 31
b update (i mod 17) + 3 17
d update (i mod 13) + 5 13

The joint schedule has period:

$$ \mathrm{lcm}(31,17,13) = 6851 $$

Accumulator Round

Pseudocode:

Round(ACC):
    a = ROTL64(a ^ d, 17)
    b = ROTL64(b + a, 23)
    c = ROTL64(c ^ b, 31)
    d = ROTL64(d + c, 47)
    return ACC

Sequential equations:

$$ a' = ROTL64(a \oplus d, 17) $$

$$ b' = ROTL64(b + a', 23) $$

$$ c' = ROTL64(c \oplus b', 31) $$

$$ d' = ROTL64(d + c', 47) $$

The round alternates XOR and modular addition, combining operations from two different algebraic structures: bitwise vector addition over (\mathbb{F}_2^{64}) and integer addition over (\mathbb{Z}/2^{64}\mathbb{Z}).

Primitive Relationship Diagram

flowchart TD
    W["Input word or seed"] --> DIRECT["Direct word injection"]
    DIRECT --> STACK["StackMix: 8 ARX iterations"]
    STACK --> HEAP["HeapMixScratch: 64 ARX iterations"]
    HEAP --> ROUND["Accumulator Round x4"]
    ROUND --> ACCOUT["Updated ACC"]
Loading

State Absorption

AbsorbWord

AbsorbWord is the main state update function used for initialization and ciphertext feedback.

Pseudocode:

AbsorbWord(ACC, word):
    a = a ^ (word + ROTL64(d, 17))
    b = b + ROTL64(word ^ a, 29)
    c = c ^ ROTR64(word + b, 31)
    d = d + A64(word ^ c)

    StackMix(ACC, word ^ c)
    HeapMixScratch(ACC, word ^ a ^ ROTL64(d, 13))

    for r = 0 to 3:
        Round(ACC)

    return ACC

Initial injection equations:

$$ a_0' = a_0 \oplus (w + ROTL64(d_0,17)) $$

$$ b_0' = b_0 + ROTL64(w \oplus a_0',29) $$

$$ c_0' = c_0 \oplus ROTR64(w + b_0',31) $$

$$ d_0' = d_0 + A64(w \oplus c_0') $$

Then:

$$ ACC' = Round^4(HeapMixScratch(StackMix(ACC, w \oplus c), w \oplus a \oplus ROTL64(d,13))) $$

AbsorbBytes

AbsorbBytes maps an arbitrary byte string into a sequence of 64-bit absorption words with explicit domain and length binding.

Pseudocode:

AbsorbBytes(ACC, domain, data):
    AbsorbWord(ACC, domain ^ A64(len(data)))

    block_index = 0
    offset = 0

    while offset < len(data):
        chunk = data[offset : offset + 8]
        word = Load64LEPartial(chunk)
        word = word ^ (len(chunk) << 56)
        word = word ^ A64(domain + block_index)
        AbsorbWord(ACC, word)
        offset += len(chunk)
        block_index += 1

    return ACC

Length binding:

$$ w_{len} = domain \oplus A64(|data|) $$

Chunk binding:

$$ w_i = Load64LEPartial(chunk_i) \oplus (|chunk_i| \ll 56) \oplus A64(domain + i) $$

Absorption Flow

flowchart TD
    DATA["Byte string"] --> LEN["domain ^ A64(length)"]
    LEN --> AW0["AbsorbWord"]
    DATA --> SPLIT["Split into <=8 byte chunks"]
    SPLIT --> W["Load64LEPartial + chunk length + block domain"]
    W --> AW["AbsorbWord per chunk"]
    AW0 --> STATE["Updated ACC"]
    AW --> STATE
Loading

Accumulator Initialization

Accumulator initialization absorbs the nonce and stream subkey under separate domains.

Domain constants:

Domain Hex value Meaning
nonce_domain 0x6e6f6e63655f7632 "nonce_v2" style domain
key_domain 0x6b65795f6d617363 "key_masc" style domain

Pseudocode:

InitACC(K_enc, N):
    ACC = InitBase()
    AbsorbBytes(ACC, nonce_domain, N)
    AbsorbBytes(ACC, key_domain, K_enc)

    for r = 0 to 11:
        AbsorbWord(ACC, 0xa5a5a5a5a5a5a5a5 ^ r)

    return ACC

Mathematically:

$$ ACC_0 = InitBase() $$

$$ ACC_1 = AbsorbBytes(ACC_0, D_N, N) $$

$$ ACC_2 = AbsorbBytes(ACC_1, D_K, K_{enc}) $$

$$ ACC_3 = \prod_{r=0}^{11} AbsorbWord(ACC_2, 0xa5a5a5a5a5a5a5a5 \oplus r) $$

where the product notation means sequential application.

Initialization Diagram

flowchart TD
    BASE["Base constants"] --> AN["Absorb nonce under D_N"]
    N["Nonce N"] --> AN
    AN --> AK["Absorb stream key under D_K"]
    KENC["K_enc"] --> AK
    AK --> FINAL["12 final absorption rounds"]
    FINAL --> ACC0["Initial stream ACC"]
Loading

Keystream Generation

For block index i, MASC-256 v2 produces one 64-bit keystream word:

KeystreamWord(ACC, i):
    mix = a
        ^ ROTL64(b, 17)
        ^ ROTR64(c, 23)
        ^ d
        ^ A64(i ^ 0xfeedfacedeadbeef)

    return A64(mix)

Equation:

$$ Z_i = A64(a_i \oplus ROTL64(b_i,17) \oplus ROTR64(c_i,23) \oplus d_i \oplus A64(i \oplus 0xfeedfacedeadbeef)) $$

Byte extraction:

$$ KS_{8i+j} = (Z_i \gg 8j)\ \mathrm{AND}\ 255,\quad 0 \leq j < 8 $$

Keystream Diagram

flowchart LR
    A["a"] --> MIX["xor mix"]
    B["ROTL64(b,17)"] --> MIX
    C["ROTR64(c,23)"] --> MIX
    D["d"] --> MIX
    I["A64(block_index ^ constant)"] --> MIX
    MIX --> A64["A64"]
    A64 --> Z["64-bit keystream word Z_i"]
    Z --> B0["byte 0"]
    Z --> B1["byte 1"]
    Z --> B7["byte 7"]
Loading

Encryption Core

Encryption uses XOR with the keystream. The state is then updated by absorbing the resulting ciphertext block.

For plaintext block (P_i) of length (m \leq 8):

$$ C_i[j] = P_i[j] \oplus ((Z_i \gg 8j)\ \mathrm{AND}\ 255) $$

for:

$$ 0 \leq j < m $$

Ciphertext feedback word:

$$ F_i = Load64LEPartial(C_i) \oplus (m \ll 56) \oplus A64(i \oplus 0x7365635f626c6f63) $$

State transition:

$$ ACC_{i+1} = AbsorbWord(ACC_i, F_i) $$

Stream Block Diagram

sequenceDiagram
    participant ACC as ACC_i
    participant KS as KeystreamWord
    participant XOR as XOR
    participant FB as Cipher feedback
    ACC->>KS: Z_i = KeystreamWord(ACC_i, i)
    KS->>XOR: bytes(Z_i)
    XOR->>XOR: C_i = P_i xor bytes(Z_i)
    XOR->>FB: C_i
    FB->>ACC: ACC_{i+1} = AbsorbWord(ACC_i, F_i)
Loading

Decryption Symmetry

Decryption computes:

$$ P_i[j] = C_i[j] \oplus ((Z_i \gg 8j)\ \mathrm{AND}\ 255) $$

and absorbs the original ciphertext block (C_i), not the recovered plaintext. This keeps encryption and decryption state transitions identical.

flowchart TD
    EACC["Encrypt ACC_i"] --> EKS["Z_i"]
    DACC["Decrypt ACC_i"] --> DKS["Z_i"]
    EKS --> EC["C_i = P_i xor Z_i"]
    DKS --> DP["P_i = C_i xor Z_i"]
    EC --> EFB["Absorb C_i"]
    EC --> DFB["Absorb same C_i"]
    EFB --> ENEXT["Encrypt ACC_{i+1}"]
    DFB --> DNEXT["Decrypt ACC_{i+1}"]
Loading

Correctness:

$$ P_i = C_i \oplus Z_i = (P_i \oplus Z_i) \oplus Z_i $$

$$ P_i = P_i \oplus (Z_i \oplus Z_i) = P_i $$


Authentication Core

MASC-256 v2 uses Encrypt-then-MAC.

Tag domain:

MASC-256 v2 EtM HMAC-SHA256

Tag input:

$$ M_{tag} = D_{tag} \parallel N \parallel LE64(|AAD|) \parallel AAD \parallel LE64(|C|) \parallel C $$

Tag:

$$ T = HMAC_{SHA256}(K_{mac}, M_{tag}) $$

Authentication Diagram

flowchart TD
    D["Domain string"] --> M["MAC input"]
    N["Nonce N"] --> M
    AL["LE64(|AAD|)"] --> M
    AAD["AAD"] --> M
    CL["LE64(|C|)"] --> M
    C["Ciphertext C"] --> M
    KMAC["K_mac"] --> H["HMAC-SHA256"]
    M --> H
    H --> T["Tag T"]
Loading

Verification

masc256_open recomputes the tag before decryption:

$$ T' = HMAC_{SHA256}(K_{mac}, M_{tag}) $$

It accepts only if:

$$ ConstantTimeEqual(T,T') = 1 $$

Rejection rule:

if tag mismatch:
    zero output buffer
    return MASC256_ERR_AUTH

Public API

Constants

#define MASC256_VERSION 2
#define MASC256_NONCE_SIZE 16
#define MASC256_TAG_SIZE 32
#define MASC256_OVERHEAD (MASC256_NONCE_SIZE + MASC256_TAG_SIZE)

Return Codes

#define MASC256_OK 0
#define MASC256_ERR_INVALID -1
#define MASC256_ERR_AUTH -2
#define MASC256_ERR_RANDOM -3
#define MASC256_ERR_BUFFER -4

Public Sealing API: masc256_seal

int masc256_seal(uint8_t *out,
                 size_t out_cap,
                 size_t *out_len,
                 const uint8_t *plaintext,
                 size_t plaintext_len,
                 const uint8_t *key,
                 size_t klen,
                 const uint8_t *aad,
                 size_t aad_len);

Contract:

Condition Behavior
out == NULL returns required size through out_len, then MASC256_ERR_BUFFER
out_cap < plaintext_len + 48 returns MASC256_ERR_BUFFER
invalid key or null required pointer returns MASC256_ERR_INVALID
system RNG failure returns MASC256_ERR_RANDOM
success writes `N

Public Opening API: masc256_open

int masc256_open(uint8_t *out,
                 size_t out_cap,
                 size_t *out_len,
                 const uint8_t *sealed,
                 size_t sealed_len,
                 const uint8_t *key,
                 size_t klen,
                 const uint8_t *aad,
                 size_t aad_len);

Contract:

Condition Behavior
sealed_len < 48 returns MASC256_ERR_AUTH
tag mismatch zeroes output and returns MASC256_ERR_AUTH
output too small returns MASC256_ERR_BUFFER
invalid required pointer returns MASC256_ERR_INVALID
success writes plaintext and returns MASC256_OK

Advanced API

Use only when the protocol already guarantees nonce uniqueness:

int masc256_encrypt(uint8_t *data,
                    size_t len,
                    const uint8_t *key,
                    size_t klen,
                    const uint8_t nonce[MASC256_NONCE_SIZE],
                    const uint8_t *aad,
                    size_t aad_len,
                    uint8_t tag[MASC256_TAG_SIZE]);

int masc256_decrypt(uint8_t *data,
                    size_t len,
                    const uint8_t *key,
                    size_t klen,
                    const uint8_t nonce[MASC256_NONCE_SIZE],
                    const uint8_t *aad,
                    size_t aad_len,
                    const uint8_t tag[MASC256_TAG_SIZE]);

API State Machine

stateDiagram-v2
    [*] --> SealInput
    SealInput --> SealedMessage: masc256_seal OK
    SealInput --> SealError: invalid/buffer/random error
    SealedMessage --> OpenInput
    OpenInput --> Plaintext: masc256_open OK
    OpenInput --> AuthFailure: tag mismatch
    OpenInput --> InputError: invalid/buffer error
    AuthFailure --> [*]
    Plaintext --> [*]
    SealError --> [*]
    InputError --> [*]
Loading

Error Semantics

Return code Meaning Security handling
MASC256_OK Operation succeeded Output is valid
MASC256_ERR_INVALID Invalid pointer, key length, or argument Treat as programmer error
MASC256_ERR_AUTH Authentication failed Discard output; do not reveal plaintext
MASC256_ERR_RANDOM Nonce generation failed Abort encryption
MASC256_ERR_BUFFER Output buffer too small Allocate at least reported size

Authentication failures must not be treated as recoverable parsing errors. The caller should not attempt to inspect partial plaintext.


Reference Test Vector

This vector is checked by tests/test_masc256.c.

Inputs

key       = "test master key"
aad       = "test aad"
nonce     = 000102030405060708090a0b0c0d0e0f
plaintext = "MASC-256 authenticated encryption test"

Expected Ciphertext

cca05fcfd138c65d69d319e784ed17c4831a4a75d7636337feec199e9a3f2f7657ab47bb81bd

Expected Tag

579d833514d4149d24a1c9a69d6e0859d12b834f85bb6f795d6c67a1237dc395

Expected Sealed Output

000102030405060708090a0b0c0d0e0fcca05fcfd138c65d69d319e784ed17c4831a4a75d7636337feec199e9a3f2f7657ab47bb81bd579d833514d4149d24a1c9a69d6e0859d12b834f85bb6f795d6c67a1237dc395

Test Coverage

The current test verifies:

Test Purpose
Deterministic ciphertext Detect accidental algorithm changes
Deterministic tag Detect MAC/key-schedule changes
Tampered ciphertext rejection Validate integrity
Wrong AAD rejection Validate AAD binding
Wrong nonce rejection Validate nonce binding
seal sizing behavior Validate public buffer contract
open in-place behavior Validate supported overlapping use

Implementation Security Notes

Memory Hygiene

The implementation uses secure_zero for temporary keys, tags, scratch state, and accumulator state:

static void secure_zero(void *ptr, size_t len) {
    volatile uint8_t *p = (volatile uint8_t *)ptr;
    while(len-- != 0U) {
        *p++ = 0U;
    }
}

This is a practical clearing strategy, but high-assurance environments should consider platform-provided guaranteed zeroization APIs such as memset_s, explicit_bzero, or equivalent compiler-supported intrinsics.

Random Nonce Generation

Nonce generation uses:

Platform Source
Windows BCryptGenRandom
Unix-like /dev/urandom

The nonce is public but must be unique with overwhelming probability for a given master key.

Constant-Time Tag Comparison

The tag comparison uses an accumulator-style byte loop over the fixed tag size:

diff |= left[i] ^ right[i];

This avoids early exit on the first differing byte.

Branching and Timing

The implementation branches on public lengths, error conditions, and whether an operation is encryption or decryption. It does not intentionally branch on secret key bytes. Formal constant-time verification is outside the assurance level claimed by this release specification.


Validation and Analysis Program

The release validation program consists of the following engineering and cryptanalytic checks.

Engineering Validation

flowchart TD
    SRC["Source code"] --> BUILD["GCC/Clang/MSVC build"]
    BUILD --> WARN["Warnings as errors"]
    WARN --> TEST["Known-answer tests"]
    TEST --> ASAN["AddressSanitizer"]
    TEST --> UBSAN["UndefinedBehaviorSanitizer"]
    TEST --> CPPCHECK["cppcheck"]
    TEST --> CODEQL["CodeQL"]
    ASAN --> PASS["CI pass"]
    UBSAN --> PASS
    CPPCHECK --> PASS
    CODEQL --> PASS
Loading

Cryptographic Analysis

flowchart TD
    SPEC["Reference specification"] --> TV["Independent test vectors"]
    TV --> IMPL2["Independent implementation"]
    SPEC --> DIFF["Differential analysis"]
    SPEC --> ROT["Rotational analysis"]
    SPEC --> RELKEY["Related-key review"]
    SPEC --> STREAM["Keystream statistical tests"]
    STREAM --> PRACT["PractRand"]
    STREAM --> NIST["NIST STS"]
    SPEC --> TIME["Timing analysis"]
    TIME --> DUDECT["dudect"]
Loading

Required Analysis Artifacts

Artifact Purpose
Known-answer tests for SHA-256 Validate hash implementation
Known-answer tests for HMAC-SHA256 Validate MAC implementation
Known-answer tests for HKDF expansion Validate key schedule
MASC test vectors Validate full construction
Fuzz harness for masc256_open Validate parser and auth rejection
Keystream generator tool Feed NIST STS, PractRand, TestU01
Benchmark report Measure cost and compare implementations
Cryptanalysis notes Document attacks attempted and results

Known Limitations

Limitation Impact Release treatment
Custom stream generator Unknown security margin Seek public cryptanalysis
No nonce-misuse resistance Reused key/nonce can leak plaintext relations Primary API generates nonces through masc256_seal; manual nonce protocols must enforce uniqueness
No formal proof Cannot claim standard-grade security Publish formal spec and invite review
No independent implementation Bugs may be implementation-specific Build a second implementation
No external audit Unknown design and implementation flaws Commission independent review
Sequential feedback Limited parallelism and random access Consider future counter-mode variant
HMAC-SHA256 dependency Security partly depends on HMAC/SHA-256 Intentional and standard, but document it

Nonce Reuse Failure

If the same keystream encrypts two plaintexts:

$$ C_1 = P_1 \oplus KS $$

$$ C_2 = P_2 \oplus KS $$

then:

$$ C_1 \oplus C_2 = P_1 \oplus P_2 $$

This is a standard stream-cipher failure mode. The primary sealing API generates a fresh nonce automatically to keep nonce management inside the library boundary.


References


Release Conformance

An implementation conforms to MASC-256 v2 when it satisfies all externally observable requirements below.

Requirement Conformance condition
Public constants Nonce size is 16 bytes, tag size is 32 bytes, and sealed overhead is 48 bytes
Sealed format Messages are encoded exactly as `N
Subkey derivation Stream and MAC subkeys are derived with the specified HMAC-SHA256 labels
Base state Accumulator initialization uses the specified base constants and ASCII domain masks
Absorption order Nonce absorption precedes stream-key absorption
Finalization Exactly 12 final absorption rounds are applied before keystream generation
Stream core The reference ciphertext is reproduced for the published test vector
Authentication The reference tag is reproduced for the published test vector
Verification order Authentication completes before plaintext is released
Tamper behavior Modified nonce, AAD, ciphertext, or tag is rejected
Error behavior Invalid input, authentication failure, random failure, and buffer failure are distinguishable

The reference vector in this document is the minimum interoperability test. A complete independent implementation should additionally validate SHA-256, HMAC-SHA256, and the HMAC-based expansion function against external known-answer tests.


Appendix A: Complete Seal Algorithm

Algorithm Seal(K, AAD, P):
    Input:
        K    master key
        AAD  associated data
        P    plaintext

    Output:
        sealed = N || C || T

    1. N = RandomBytes(16)
    2. (K_enc, K_mac) = DeriveKeys(K, N)
    3. ACC = InitACC(K_enc, N)
    4. C = StreamCryptEncrypt(ACC, P)
    5. T = HMAC-SHA256(K_mac,
           "MASC-256 v2 EtM HMAC-SHA256" ||
           N ||
           LE64(|AAD|) || AAD ||
           LE64(|C|) || C)
    6. return N || C || T

Appendix B: Complete Open Algorithm

Algorithm Open(K, AAD, sealed):
    Input:
        K       master key
        AAD     associated data
        sealed  N || C || T

    Output:
        plaintext P or MASC256_ERR_AUTH

    1. if |sealed| < 48:
           return MASC256_ERR_AUTH
    2. Parse:
           N = sealed[0..15]
           C = sealed[16..|sealed|-33]
           T = sealed[|sealed|-32..|sealed|-1]
    3. (K_enc, K_mac) = DeriveKeys(K, N)
    4. T' = HMAC-SHA256(K_mac,
            "MASC-256 v2 EtM HMAC-SHA256" ||
            N ||
            LE64(|AAD|) || AAD ||
            LE64(|C|) || C)
    5. if ConstantTimeEqual(T, T') == false:
           return MASC256_ERR_AUTH
    6. ACC = InitACC(K_enc, N)
    7. P = StreamCryptDecrypt(ACC, C)
    8. return P

Appendix C: Core Stream Algorithm

Algorithm StreamCrypt(ACC, data, encrypting):
    offset = 0
    block_index = 0

    while offset < len(data):
        block_len = min(8, len(data) - offset)
        Z = KeystreamWord(ACC, block_index)

        for j = 0 to block_len-1:
            input_byte = data[offset + j]
            output_byte = input_byte ^ ((Z >> (8*j)) & 0xff)

            if encrypting:
                data[offset + j] = output_byte
                cipher_block[j] = output_byte
            else:
                cipher_block[j] = input_byte
                data[offset + j] = output_byte

        F = Load64LEPartial(cipher_block)
            ^ (block_len << 56)
            ^ A64(block_index ^ 0x7365635f626c6f63)

        ACC = AbsorbWord(ACC, F)
        offset = offset + block_len
        block_index = block_index + 1

    return data

Clone this wiki locally