Version 1.0 — February 2026
Category: Symmetric Block Cipher Specification
This document specifies the Rubik's Cube Encryption Algorithm (RCEA), a symmetric block cipher that uses permutations derived from an N×N×N Rubik's Cube as its primary diffusion mechanism. RCEA operates on blocks of 6N² bytes and supports configurable cube sizes (N = 2..6) and round counts (R = 4..20). The cipher combines four operations per round — byte substitution, cube rotation, face mixing, and round key addition — to achieve confusion and diffusion.
RCEA is designed as an educational and experimental cipher to demonstrate how geometric transformations can serve as cryptographic primitives. It uses the AES S-Box for substitution, SHA-256 for key derivation, and HMAC-SHA-256 for message authentication.
This specification defines the complete RCEA algorithm including: the cube geometry model, key derivation procedure, four round operations, block encryption/decryption, CBC mode with Encrypt-then-MAC authentication, and the binary file format for encrypted files.
This specification is intended for implementors who wish to create compatible RCEA implementations in any programming language, as well as for cryptographic study and analysis of cube-based diffusion mechanisms.
| Symbol | Definition |
|---|---|
| N | Cube size parameter. An integer in [2, 6]. |
| R | Number of encryption rounds. An integer in [4, 20]. |
| BS | Block size in bytes: BS = 6 × N². |
| M | Total number of available cube moves: M = 6 × N. |
| RPR | Rotations per round: RPR = N + 2. |
| ⊕ | Bitwise exclusive OR (XOR). |
| || | Byte string concatenation. |
| S[x] | S-Box lookup: substitution of byte x. |
| state[i] | The i-th byte of the cipher state, where 0 ≤ i < BS. |
| π | A permutation array: π[i] = j means position i maps to position j. |
| π⁻¹ | The inverse permutation of π. |
| π₁ ∘ π₂ | Composition of permutations: (π₁ ∘ π₂)[i] = π₂[π₁[i]]. |
The following table shows derived values for each supported cube size:
| N | Block Size (bytes) | Moves (M) | Rotations/Round (RPR) |
|---|---|---|---|
| 2 | 24 | 12 | 4 |
| 3 | 54 | 18 | 5 |
| 4 | 96 | 24 | 6 |
| 5 | 150 | 30 | 7 |
| 6 | 216 | 36 | 8 |
The virtual cube has six faces, each assigned a fixed integer identifier:
| Face Name | Index |
|---|---|
| FRONT | 0 |
| BACK | 1 |
| LEFT | 2 |
| RIGHT | 3 |
| UP | 4 |
| DOWN | 5 |
Each face contains N×N cells arranged in a row-major grid with rows indexed top-to-bottom and columns indexed left-to-right.
The cipher state is a flat array of BS = 6N² bytes. The mapping from a (face, row, col) triple to a linear index is:
cellIndex(face, row, col, N) = face × N² + row × N + colwhere face ∈ [0, 5], row ∈ [0, N−1], col ∈ [0, N−1].
A move is a pair of permutations (forward, inverse) that together define a single clockwise/counter-clockwise rotation. RCEA constructs M = 6N moves total, organized in three axes with two directions each.
A clockwise face rotation of face f maps each cell as:
faceRotateCW(f, N):
For each (row, col) on face f:
π[cellIndex(f, row, col, N)] = cellIndex(f, col, N−1−row, N)
All other positions map to themselves.Counter-clockwise rotation is the inverse: faceRotateCCW = (faceRotateCW)⁻¹.
For each axis, there are N layers (indexed k = 0..N−1). Each layer defines a ring permutation that cycles cells across four adjacent faces.
Z-Axis Ring (layer k):
For each column c in [0, N):
UP[N−1−k, c] → RIGHT[c, k]
RIGHT[c, k] → DOWN[k, N−1−c]
DOWN[k, c] → LEFT[c, N−1−k]
LEFT[c, N−1−k] → UP[N−1−k, N−1−c]X-Axis Ring (layer k):
For each row r in [0, N):
UP[r, k] → FRONT[r, k]
FRONT[r, k] → DOWN[r, k]
DOWN[r, k] → BACK[N−1−r, N−1−k]
BACK[N−1−r, N−1−k] → UP[r, k]Y-Axis Ring (layer k):
For each column c in [0, N):
FRONT[N−1−k, c] → RIGHT[N−1−k, c]
RIGHT[N−1−k, c] → BACK[N−1−k, c]
BACK[N−1−k, c] → LEFT[N−1−k, c]
LEFT[N−1−k, c] → FRONT[N−1−k, c]For each axis (Z, X, Y) and each layer k in [0, N):
ring = axisRing(k, N)
If k = 0:
moveCW = ring ∘ faceRotateCW(boundaryFace, N)
Else if k = N−1:
moveCW = ring ∘ faceRotateCW(oppositeFace, N) // or CCW depending on axis
Else:
moveCW = ring
moveCCW = (moveCW)⁻¹
Emit two moves: (moveCW, moveCCW) and (moveCCW, moveCW)Boundary face assignments per axis:
| Axis | k = 0 face (CW rotation) | k = N−1 face |
|---|---|---|
| Z | FRONT (CW) | BACK (CCW) |
| X | LEFT (CCW) | RIGHT (CW) |
| Y | DOWN (CCW) | UP (CW) |
The total number of moves is 3 axes × N layers × 2 directions = 6N.
RCEA uses the AES (Rijndael) S-Box as defined in FIPS 197, Section 5.1.1. The S-Box is a fixed 256-byte lookup table providing non-linear byte substitution. The inverse S-Box is computed such that INV_SBOX[SBOX[x]] = x for all x ∈ [0, 255].
The S-Box is presented here in hexadecimal. Row index = high nibble, column index = low nibble:
| .0 | .1 | .2 | .3 | .4 | .5 | .6 | .7 | .8 | .9 | .a | .b | .c | .d | .e | .f | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0. | 63 | 7c | 77 | 7b | f2 | 6b | 6f | c5 | 30 | 01 | 67 | 2b | fe | d7 | ab | 76 |
| 0. | ca | 82 | c9 | 7d | fa | 59 | 47 | f0 | ad | d4 | a2 | af | 9c | a4 | 72 | c0 |
| 0. | b7 | fd | 93 | 26 | 36 | 3f | f7 | cc | 34 | a5 | e5 | f1 | 71 | d8 | 31 | 15 |
| 0. | 04 | c7 | 23 | c3 | 18 | 96 | 05 | 9a | 07 | 12 | 80 | e2 | eb | 27 | b2 | 75 |
| 0. | 09 | 83 | 2c | 1a | 1b | 6e | 5a | a0 | 52 | 3b | d6 | b3 | 29 | e3 | 2f | 84 |
| 0. | 53 | d1 | 00 | ed | 20 | fc | b1 | 5b | 6a | cb | be | 39 | 4a | 4c | 58 | cf |
| 0. | d0 | ef | aa | fb | 43 | 4d | 33 | 85 | 45 | f9 | 02 | 7f | 50 | 3c | 9f | a8 |
| 0. | 51 | a3 | 40 | 8f | 92 | 9d | 38 | f5 | bc | b6 | da | 21 | 10 | ff | f3 | d2 |
| 0. | cd | 0c | 13 | ec | 5f | 97 | 44 | 17 | c4 | a7 | 7e | 3d | 64 | 5d | 19 | 73 |
| 0. | 60 | 81 | 4f | dc | 22 | 2a | 90 | 88 | 46 | ee | b8 | 14 | de | 5e | 0b | db |
| 0. | e0 | 32 | 3a | 0a | 49 | 06 | 24 | 5c | c2 | d3 | ac | 62 | 91 | 95 | e4 | 79 |
| 0. | e7 | c8 | 37 | 6d | 8d | d5 | 4e | a9 | 6c | 56 | f4 | ea | 65 | 7a | ae | 08 |
| 0. | ba | 78 | 25 | 2e | 1c | a6 | b4 | c6 | e8 | dd | 74 | 1f | 4b | bd | 8b | 8a |
| 0. | 70 | 3e | b5 | 66 | 48 | 03 | f6 | 0e | 61 | 35 | 57 | b9 | 86 | c1 | 1d | 9e |
| 0. | e1 | f8 | 98 | 11 | 69 | d9 | 8e | 94 | 9b | 1e | 87 | e9 | ce | 55 | 28 | df |
| 0. | 8c | a1 | 89 | 0d | bf | e6 | 42 | 68 | 41 | 99 | 2d | 0f | b0 | 54 | bb | 16 |
RCEA derives all key material from a variable-length user key using a counter-mode construction based on SHA-256:
KDF(key, label, length):
result = empty byte string
counter = 0
While len(result) < length:
counterBytes = counter as 4-byte big-endian uint32
input = key || encode_utf8(label) || counterBytes
hash = SHA-256(input)
result = result || hash
counter = counter + 1
Return first 'length' bytes of resultEach derived quantity uses a unique label string to ensure domain separation. The labels used are specified in Section 7.
The key schedule transforms the user-provided key into all material required for encryption: a keyed cube state, per-round keys, per-round rotation sequences, and an HMAC key.
The keyed cube state is a BS-byte array used as the initial whitening key.
// 1. Derive initial state bytes
state = KDF(key, "rcea_phase1_columns", BS)
// 2. Derive rotation material
rotMaterial = KDF(key, "rcea_phase1_rotations", RPR)
// 3. Apply RPR cube rotations to the state
For i = 0 to RPR−1:
moveIndex = rotMaterial[i] mod M
state = applyPermutation(state, allMoves[moveIndex].forward)
keyedState = stateFor each round r ∈ [0, R):
// Round key (BS bytes)
roundKeys[r] = KDF(key, "rcea_round_key_" || str(r), BS)
// Rotation sequence (RPR indices)
rotBytes = KDF(key, "rcea_round_rot_" || str(r), RPR)
roundRotations[r][i] = rotBytes[i] mod M for i in [0, RPR)The string representation of r is its decimal ASCII encoding (e.g., round 0 uses label "rcea_round_key_0", round 12 uses "rcea_round_key_12").
hmacKey = KDF(key, "rcea_hmac_key", 32)The HMAC key is always 32 bytes regardless of block size.
Each encryption round applies four operations in sequence. This section defines each operation precisely.
Non-linear byte substitution using the AES S-Box (Section 5):
SubBytes(state):
For i = 0 to BS−1:
state'[i] = SBOX[state[i]]
Return state'This operation provides confusion — it breaks the linear relationship between key and ciphertext. Each byte is substituted independently.
Positional permutation of the state by applying a sequence of cube rotations:
CubeRotate(state, moveIndices, allMoves):
result = state
For each idx in moveIndices: // RPR indices
result = applyPermutation(result, allMoves[idx].forward)
Return resultWhere applyPermutation is defined as:
applyPermutation(state, perm):
newState = new array of BS bytes
For i = 0 to BS−1:
newState[perm[i]] = state[i]
Return newStateThis operation provides diffusion — it spatially rearranges bytes across cube faces, ensuring that a single byte change propagates across the entire state.
Intra-face byte mixing using two sequential XOR scans with S-Box feedback:
FaceMix(state, N):
result = copy of state
For each face f in [0, 5]:
// Pass 1: Row scan (left to right)
For row = 0 to N−1:
For col = 1 to N−1:
idx = cellIndex(f, row, col, N)
prevIdx = cellIndex(f, row, col−1, N)
result[idx] = result[idx] ⊕ SBOX[result[prevIdx]]
// Pass 2: Column scan (top to bottom)
For col = 0 to N−1:
For row = 1 to N−1:
idx = cellIndex(f, row, col, N)
aboveIdx = cellIndex(f, row−1, col, N)
result[idx] = result[idx] ⊕ SBOX[result[aboveIdx]]
Return resultFaceMix provides local diffusion within each face. Each byte depends on its left and upper neighbors through the S-Box, creating a cascade effect where changing one byte on a face affects all bytes below and to the right of it.
XOR the state with the round key:
AddRoundKey(state, roundKey):
For i = 0 to BS−1:
state'[i] = state[i] ⊕ roundKey[i]
Return state'Given a BS-byte plaintext block, the encryption procedure is:
EncryptBlock(block, keyedState, roundKeys, roundRotations, allMoves, N, R):
// Initial whitening
state = block ⊕ keyedState
// R rounds
For r = 0 to R−1:
state = SubBytes(state)
state = CubeRotate(state, roundRotations[r], allMoves)
state = FaceMix(state, N)
state = AddRoundKey(state, roundKeys[r])
Return statekeyedState ensures that even before the first round, the plaintext is mixed with key-dependent material. This is analogous to AES's initial AddRoundKey step.Decryption applies the inverse of each operation in reverse round order:
DecryptBlock(block, keyedState, roundKeys, roundRotations, allMoves, N, R):
state = copy of block
// Reverse R rounds
For r = R−1 down to 0:
state = state ⊕ roundKeys[r] // Inverse AddRoundKey
state = InvFaceMix(state, N) // Inverse FaceMix
state = InvCubeRotate(state, roundRotations[r], allMoves)
state = InvSubBytes(state) // Inverse SubBytes
// Undo initial whitening
state = state ⊕ keyedState
Return stateInvSubBytes: Uses the inverse S-Box table.
InvSubBytes(state):
For i = 0 to BS−1:
state'[i] = INV_SBOX[state[i]]InvCubeRotate: Applies inverse permutations in reverse order.
InvCubeRotate(state, moveIndices, allMoves):
result = state
For i = len(moveIndices)−1 down to 0:
result = applyPermutation(result, allMoves[moveIndices[i]].inverse)
Return resultInvFaceMix: Undoes the two passes in reverse order.
InvFaceMix(state, N):
result = copy of state
For each face f in [0, 5]:
// Undo Pass 2: Column scan (bottom to top)
For col = 0 to N−1:
For row = N−2 down to 0:
idx = cellIndex(f, row+1, col, N)
aboveIdx = cellIndex(f, row, col, N)
result[idx] = result[idx] ⊕ SBOX[result[aboveIdx]]
// Undo Pass 1: Row scan (right to left)
For row = 0 to N−1:
For col = N−2 down to 0:
idx = cellIndex(f, row, col+1, N)
prevIdx = cellIndex(f, row, col, N)
result[idx] = result[idx] ⊕ SBOX[result[prevIdx]]
Return resultRCEA uses Cipher Block Chaining (CBC) mode with PKCS#7 padding and an Encrypt-then-MAC construction using HMAC-SHA-256.
PKCS7Pad(data, BS):
padLen = BS − (len(data) mod BS)
// padLen is in [1, BS]; if data is already aligned, a full block of padding is added
Return data || repeat(padLen, padLen)
PKCS7Unpad(data, BS):
padLen = data[len(data) − 1]
If padLen < 1 or padLen > BS: ERROR "invalid padding"
For i = len(data) − padLen to len(data) − 1:
If data[i] ≠ padLen: ERROR "invalid padding"
Return data[0 .. len(data) − padLen]CBC-Encrypt(plaintext, key, N, R):
Derive all key material via KeySchedule(key, N, R)
padded = PKCS7Pad(plaintext, BS)
IV = random(BS) // cryptographically random
numBlocks = len(padded) / BS
C₀ = IV
For i = 1 to numBlocks:
Pᵢ = padded[(i−1)×BS .. i×BS]
Cᵢ = EncryptBlock(Pᵢ ⊕ Cᵢ₋₁, ...)
body = IV || C₁ || C₂ || ... || Cₙ
tag = HMAC-SHA-256(hmacKey, body)
Return body || tagCBC-Decrypt(ciphertext, key, N, R):
Derive all key material via KeySchedule(key, N, R)
If len(ciphertext) < BS + BS + 32: ERROR "ciphertext too short"
tag = ciphertext[−32 ..]
body = ciphertext[.. −32]
// Verify authentication BEFORE decryption
expectedTag = HMAC-SHA-256(hmacKey, body)
If NOT constantTimeCompare(tag, expectedTag):
ERROR "authentication failed"
IV = body[0 .. BS]
encData = body[BS ..]
numBlocks = len(encData) / BS
C₀ = IV
For i = 1 to numBlocks:
Cᵢ = encData[(i−1)×BS .. i×BS]
Pᵢ = DecryptBlock(Cᵢ, ...) ⊕ Cᵢ₋₁
Return PKCS7Unpad(P₁ || P₂ || ... || Pₙ, BS)RCEA uses the Encrypt-then-MAC paradigm. The HMAC tag is computed over the IV and all ciphertext blocks (but not the tag itself). Authentication is verified before any decryption is performed. This prevents chosen-ciphertext attacks and ensures integrity.
The HMAC key is 32 bytes derived via KDF with label "rcea_hmac_key". The MAC function is HMAC-SHA-256 as defined in RFC 2104 and FIPS 198-1.
Comparison of the received tag and expected tag MUST be performed in constant time to prevent timing side-channel attacks.
The RCEA binary file format encapsulates cipher parameters, the original filename, and the CBC-encrypted payload:
Offset Size Field
────── ─────── ──────────────────────────
0 4 Magic bytes: 0x52 0x43 0x45 0x41 ("RCEA")
4 1 Cube size N (uint8)
5 1 Rounds R (uint8)
6 2 Filename length L (uint16, big-endian)
8 L Original filename (UTF-8 encoded)
8+L ... CBC payload: IV || C₁ || ... || Cₙ || HMAC tagThe CBC payload follows the format described in Section 11.2: the first BS bytes are the IV, followed by ciphertext blocks, followed by a 32-byte HMAC-SHA-256 tag.
| Parameter | Range | Recommended | Notes |
|---|---|---|---|
| Cube Size (N) | 2 – 6 | 3 | N=3 gives 54-byte blocks; larger cubes increase block size quadratically |
| Rounds (R) | 4 – 20 | 10 | More rounds increase security margin but slow encryption linearly |
| Key | ≥ 1 byte | ≥ 16 bytes | Key is hashed via SHA-256 in KDF; longer keys do not exceed 256-bit security |
The block size grows as 6N². A 2×2 cube produces 24-byte blocks, while a 6×6 cube produces 216-byte blocks. Larger blocks mean:
The following test vectors can be used to verify a correct RCEA implementation. All values are in hexadecimal.
SBOX[0x00] = 0x63
SBOX[0x01] = 0x7C
SBOX[0x11] = 0x82
SBOX[0xFF] = 0x16
INV_SBOX[0x63] = 0x00
INV_SBOX[0x7C] = 0x01
INV_SBOX[0x16] = 0xFFblockSize(2) = 24
blockSize(3) = 54
blockSize(4) = 96
blockSize(5) = 150
blockSize(6) = 216numMoves(2) = 12
numMoves(3) = 18
numMoves(4) = 24
numMoves(5) = 30
numMoves(6) = 36
rotsPerRound(2) = 4
rotsPerRound(3) = 5
rotsPerRound(4) = 6
rotsPerRound(5) = 7
rotsPerRound(6) = 8Given key = encode_utf8("test") and label = "rcea_phase1_columns":
KDF input for counter=0:
SHA-256( 0x74 0x65 0x73 0x74 || // "test"
"rcea_phase1_columns" || // label
0x00 0x00 0x00 0x00 ) // counter = 0
The output depends on the SHA-256 implementation.
Implementors should verify: KDF("test", "label", 32) produces
a deterministic 32-byte output matching their SHA-256.For any key K, plaintext P, and parameters (N, R):
DecryptBlock(EncryptBlock(P, ...), ...) = P
CBC-Decrypt(CBC-Encrypt(P, K, N, R), K, N, R) = PAn implementation MUST verify round-trip correctness for all supported parameter combinations during testing.
RCEA is designed as an experimental and educational cipher. It has not been subjected to formal cryptanalysis by the academic community. It SHOULD NOT be used for protecting sensitive data where established algorithms (AES, ChaCha20) are available.
RCEA achieves confusion through the AES S-Box (SubBytes) and S-Box feedback in FaceMix. It achieves diffusion through CubeRotate (global byte position permutation) and FaceMix (local intra-face mixing). The combination of global and local diffusion mechanisms is intended to ensure full avalanche within a few rounds.
The key schedule uses SHA-256 based KDF with domain-separated labels, providing:
The Encrypt-then-MAC construction with HMAC-SHA-256 provides:
The constant-time comparison prevents timing oracle attacks on the MAC verification.