-
Notifications
You must be signed in to change notification settings - Fork 0
MASC‐256 Academic Reference
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.
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:
where N is a 16-byte nonce, C is the ciphertext, and T is a 32-byte
authentication tag.
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.
- 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
| 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 |
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 | 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:
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:
Sealed message:
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.
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 |
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.
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
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"]
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
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:
Minimum valid sealed length:
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
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:
Expansion:
For an output length of 32 bytes, the implementation computes one block:
The implementation supports longer outputs using the standard chained form:
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"]
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.
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:
with:
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 |
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
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.
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 |
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:
where:
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.
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:
XORing the masks into the base constants has three practical effects:
- The v2 initial state cannot accidentally equal the v1 initial state.
- The constants encode the construction family directly in the state.
- 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"]
Implementation:
uint64_t rotl(uint64_t x, int r) {
r &= 63;
return r == 0 ? x : (x << r) | (x >> (64 - r));
}Formal definition:
with:
The implementation explicitly handles r = 0 to avoid undefined C behavior for
64-bit shifts.
Implementation:
A64(x):
x = x ^ (x >> 30)
x = x * 0xbf58476d1ce4e5b9
x = x ^ (x >> 27)
x = x * 0x94d049bb133111eb
x = x ^ (x >> 31)
return xFormal expression:
where:
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.
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:
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:
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:
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:
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}).
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"]
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:
Then:
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:
Chunk binding:
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
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:
where the product notation means sequential application.
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"]
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:
Byte extraction:
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"]
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):
for:
Ciphertext feedback word:
State transition:
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)
Decryption computes:
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}"]
Correctness:
MASC-256 v2 uses Encrypt-then-MAC.
Tag domain:
MASC-256 v2 EtM HMAC-SHA256
Tag input:
Tag:
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"]
masc256_open recomputes the tag before decryption:
It accepts only if:
Rejection rule:
if tag mismatch:
zero output buffer
return MASC256_ERR_AUTH
#define MASC256_VERSION 2
#define MASC256_NONCE_SIZE 16
#define MASC256_TAG_SIZE 32
#define MASC256_OVERHEAD (MASC256_NONCE_SIZE + MASC256_TAG_SIZE)#define MASC256_OK 0
#define MASC256_ERR_INVALID -1
#define MASC256_ERR_AUTH -2
#define MASC256_ERR_RANDOM -3
#define MASC256_ERR_BUFFER -4int 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 |
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
|
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]);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 --> [*]
| 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.
This vector is checked by tests/test_masc256.c.
key = "test master key"
aad = "test aad"
nonce = 000102030405060708090a0b0c0d0e0f
plaintext = "MASC-256 authenticated encryption test"
cca05fcfd138c65d69d319e784ed17c4831a4a75d7636337feec199e9a3f2f7657ab47bb81bd
579d833514d4149d24a1c9a69d6e0859d12b834f85bb6f795d6c67a1237dc395
000102030405060708090a0b0c0d0e0fcca05fcfd138c65d69d319e784ed17c4831a4a75d7636337feec199e9a3f2f7657ab47bb81bd579d833514d4149d24a1c9a69d6e0859d12b834f85bb6f795d6c67a1237dc395
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 |
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.
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.
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.
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.
The release validation program consists of the following engineering and cryptanalytic checks.
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
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"]
| 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 |
| 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 |
If the same keystream encrypts two plaintexts:
then:
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.
- RFC 5116: Authenticated Encryption with Associated Data
- RFC 8439: ChaCha20 and Poly1305 for IETF Protocols
- RFC 5869: HMAC-based Extract-and-Expand Key Derivation Function
- NIST FIPS 180-4: Secure Hash Standard
- NIST FIPS 197: Advanced Encryption Standard
- NIST SP 800-38D: GCM and GMAC
- NIST Random Bit Generation Documentation and Software
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.
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
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
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
- 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