Cryptosystems Journal animated header graphic

Introducing the Hill-GF-4D Cryptosystem!
Four-dimensional Matrix-within-Matrix Cryptosystem
inspired by the last paragraph of Lester Hill's 1931 paper!

Is the below image inspiring to you?
It shows a four-dimensional array structure where there is are inner matrices (in green) inside outer matrices (in blue).
All math is performed in Galois Field GF(997727) as I explain in my paper
“An interesting example at the intersection of Matrix Mathematics and Cryptography (and how Artificial Intelligence can write programs)".
The matrix on the left is the Encryption Key, the matrix in the center is the Decryption Key.
The reason the math, and these matrices, work for encryption, is because they are inverse matrices.
When multiplied together they yield the Identity Matrix (technically in this instance, the Block Identity Matrix).

Matrix-within-Matrix image with green and blue
 
matrix times plaintext vector equals ciphertext vector, using matrix-within-matrix arithmetic
 
matrix times ciphertext vector equals plaintext vector, using matrix-within-matrix arithmetic
You are likely familiar with the Hill Cipher which was the first functional polygraphic substitution cipher based on formal mathematics. Indeed, Lester Hill’s 7-page 1929 paper (approaching its 100th Anniversary!) "Cryptography in an Algebraic Alphabet" is well known.
 
Less well known is Hill's second, and longer (at 21 pages) paper, written in 1931 “Concerning Certain Linear Transformation Apparatus of Cryptography. That paper is online at https://ubicua.cua.uam.mx/pluginfile.php/52449/mod_resource/content/1/Hill2.pdf and I want to bring your attention to his last paragraph, where he writes “… it is easy to set up an algebra for ranges of matrices whose elements are in turn matrices …” and I decided to run with that idea!
 
When you embed an (inner) two-dimensional matrix INSIDE every element of an (outer) two-dimensional matrix, you are creating a 4D (four-dimensional) memory structure, which a simple example looks like the image below. And inside the program, you see array references with FOUR subscripts, for example: encr_data[i][j][r][c] = rng.gen_range(0..MOD);
 
Expanding on my 2024/2025 paper “An interesting example at the intersection of Matrix Mathematics and Cryptography (and how Artificial Intelligence can write programs)” at https://cryptosystemsjournal.com/an-interesting-example-at-the-intersection-of-matrix-mathematics-and-cryptography-version-2.pdf which has full source code in Rust, “C”, Python, Java, and Go (Golang), I asked Google Gemini to help me write a Rust program which would implement this matrix-within-matrix cryptosystem in Galois Field GF(997727). It took only about one hour, working with Google Gemini, to get the first functional version of this program working!
 
While I started this post by talking about Lester Hill’s original two articles, this approach is really based on Rodney Cooper’s "Linear Transformations in Galois Fields and their Application to Cryptography" which appeared in Cryptologia Volume 4 (issue 3): pages 184-188 (1980) and extends the Hill Cipher to Galois Fields (of either integers or polynomials, my paper and programs do both).
 
It shocks me that the program below is only 186 lines of Rust source code! (not counting blank lines). While this program is complete, it is a demonstration, it would need to have an Affine Transformation added (an additional vector), and also the insertion of three random bits into each element of the plaintext vector, both of which I describe in my paper above.
 
What makes matrices so "super awesome"? They can be of ANY dimension (size)! And remember that the matrices in this cryptosystem are 4D (four dimensional) with four subscripts. So, this cryptosystem supports tiny 2x2x2x2 (=2^4) matrices (which contain 320 key bits), all the way up to 64x64x64x64 (=64^4) matrices (which contain 335 MILLION key bits), so this cryptosystem supports key sizes over SIX ORDERS OF MAGNITUDE! And this is why these matrix-based cryptosystems are "asymptotic to a One-Time-Pad" (as I fully explain in my paper).
 

There are TWO SIDE-BY-SIDE VERSIONS of this program:
== Single-Threaded: on the left
== Parallel/Multi-Threaded: on the right
Note the Yellow Highlights differences in the source code.

Standard Engine (Single-Threaded)
48x48x48x48 inversion = 164.703 seconds
Parallel Engine (Rayon)
48x48x48x48 inversion = 7.125 seconds (23.12x speedup!)
use rand::Rng; use std::time::Instant; use std::fs::File; use std::io::{Write, BufWriter, Result}; const MOD: u64 = 997727; const DIM: usize = 48; const SUB_DIM: usize = 48; type Matrix = Vec<Vec<u64>>; struct BlockMatrix { data: Vec<Vec<Matrix>>, } fn get_identity_sub() -> Matrix { let mut m = vec![vec![0; SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { m[i][i] = 1; } m } fn get_zero_sub() -> Matrix { vec![vec![0; SUB_DIM]; SUB_DIM] } fn mod_inv(a: u64, m: u64) -> u64 { let mut mn = (m as i64, a as i64); let mut xy = (0, 1); while mn.1 != 0 { xy = (xy.1, xy.0 - (mn.0 / mn.1) * xy.1); mn = (mn.1, mn.0 % mn.1); } let mut res = xy.0; if res < 0 { res += m as i64; } res as u64 } fn sub_mat_mul(a: &Matrix, b: &Matrix) -> Matrix { let mut res = vec![vec![0; SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { for j in 0..SUB_DIM { let mut sum = 0; for k in 0..SUB_DIM { sum = (sum + (a[i][k] * b[k][j])) % MOD; } res[i][j] = sum; } } res } fn sub_mat_inv(a: &Matrix) -> Option<Matrix> { let mut aug = vec![vec![0; 2 * SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { for j in 0..SUB_DIM { aug[i][j] = a[i][j]; } aug[i][SUB_DIM + i] = 1; } for i in 0..SUB_DIM { let mut pivot = i; while pivot < SUB_DIM && aug[pivot][i] == 0 { pivot += 1; } if pivot == SUB_DIM { return None; } aug.swap(i, pivot); let inv = mod_inv(aug[i][i], MOD); for j in 0..2 * SUB_DIM { aug[i][j] = (aug[i][j] * inv) % MOD; } for k in 0..SUB_DIM { if k != i { let factor = aug[k][i]; for j in 0..2 * SUB_DIM { let sub = (factor * aug[i][j]) % MOD; aug[k][j] = (aug[k][j] + MOD - sub) % MOD; } } } } let mut res = vec![vec![0; SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { for j in 0..SUB_DIM { res[i][j] = aug[i][SUB_DIM + j]; } } Some(res) } fn block_invert(bm: &BlockMatrix) -> BlockMatrix { let mut aug = vec![vec![vec![vec![0; SUB_DIM]; SUB_DIM]; 2 * DIM]; DIM]; for i in 0..DIM { for j in 0..DIM { aug[i][j] = bm.data[i][j].clone(); } for j in DIM..2 * DIM { aug[i][j] = if j - DIM == i { get_identity_sub() } else { get_zero_sub() }; } } for i in 0..DIM { let p_inv = sub_mat_inv(&aug[i][i]) .expect("Singular Block Pivot"); for j in 0..2 * DIM { aug[i][j] = sub_mat_mul(&p_inv, &aug[i][j]); } let pivot_row = aug[i].clone(); for k in 0..DIM { if k != i { let factor = aug[k][i].clone(); for j in 0..2 * DIM { let prod = sub_mat_mul(&factor, &pivot_row[j]); for r in 0..SUB_DIM { for c in 0..SUB_DIM { aug[k][j][r][c] = (aug[k][j][r][c] + MOD - prod[r][c]) % MOD; } } } } } } let mut inv_data = vec![vec![get_zero_sub(); DIM]; DIM]; for i in 0..DIM { for j in 0..DIM { inv_data[i][j] = aug[i][DIM + j].clone(); } } BlockMatrix { data: inv_data } } fn main() -> Result<()> { let mut rng = rand::thread_rng(); let mut encr_data = vec![vec![vec![vec![0; SUB_DIM]; SUB_DIM]; DIM]; DIM]; for i in 0..DIM { for j in 0..DIM { for r in 0..SUB_DIM { for c in 0..SUB_DIM { encr_data[i][j][r][c] = rng.gen_range(0..MOD); } } } } let encr = BlockMatrix { data: encr_data }; save_key_to_file(&encr, "secret.key")?; let start_inv = Instant::now(); let decr = block_invert(&encr); println!("Inversion time: {:?}", start_inv.elapsed()); for i in 0..DIM { for j in 0..DIM { let mut res_block = get_zero_sub(); for k in 0..DIM { let prod = sub_mat_mul(&encr.data[i][k], &decr.data[k][j]); for r in 0..SUB_DIM { for c in 0..SUB_DIM { res_block[r][c] = (res_block[r][c] + prod[r][c]) % MOD; } } } let target = if i == j { get_identity_sub() } else { get_zero_sub() }; if res_block != target { println!("Verification failed!"); break; } } } println!("✅ VERIFIED"); Ok(()) } fn save_key_to_file(bm: &BlockMatrix, filename: &str) -> Result<()> { let file = File::create(filename)?; let mut writer = BufWriter::new(file); for i in 0..DIM { for j in 0..DIM { for r in 0..SUB_DIM { for c in 0..SUB_DIM { write!(writer, "{} ", bm.data[i][j][r][c])?; } writeln!(writer)?; } } } Ok(()) }
use rand::Rng; use std::time::Instant; use rayon::prelude::*; use std::fs::File; use std::io::{Write, BufWriter, Result}; const MOD: u64 = 997727; const DIM: usize = 48; const SUB_DIM: usize = 48; type Matrix = Vec<Vec<u64>>; struct BlockMatrix { data: Vec<Vec<Matrix>>, } fn get_identity_sub() -> Matrix { let mut m = vec![vec![0; SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { m[i][i] = 1; } m } fn get_zero_sub() -> Matrix { vec![vec![0; SUB_DIM]; SUB_DIM] } fn mod_inv(a: u64, m: u64) -> u64 { let mut mn = (m as i64, a as i64); let mut xy = (0, 1); while mn.1 != 0 { xy = (xy.1, xy.0 - (mn.0 / mn.1) * xy.1); mn = (mn.1, mn.0 % mn.1); } let mut res = xy.0; if res < 0 { res += m as i64; } res as u64 } fn sub_mat_mul(a: &Matrix, b: &Matrix) -> Matrix { let mut res = vec![vec![0; SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { for j in 0..SUB_DIM { let mut sum = 0; for k in 0..SUB_DIM { sum = (sum + (a[i][k] * b[k][j])) % MOD; } res[i][j] = sum; } } res } fn sub_mat_inv(a: &Matrix) -> Option<Matrix> { let mut aug = vec![vec![0; 2 * SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { for j in 0..SUB_DIM { aug[i][j] = a[i][j]; } aug[i][SUB_DIM + i] = 1; } for i in 0..SUB_DIM { let mut pivot = i; while pivot < SUB_DIM && aug[pivot][i] == 0 { pivot += 1; } if pivot == SUB_DIM { return None; } aug.swap(i, pivot); let inv = mod_inv(aug[i][i], MOD); for j in 0..2 * SUB_DIM { aug[i][j] = (aug[i][j] * inv) % MOD; } for k in 0..SUB_DIM { if k != i { let factor = aug[k][i]; for j in 0..2 * SUB_DIM { let sub = (factor * aug[i][j]) % MOD; aug[k][j] = (aug[k][j] + MOD - sub) % MOD; } } } } let mut res = vec![vec![0; SUB_DIM]; SUB_DIM]; for i in 0..SUB_DIM { for j in 0..SUB_DIM { res[i][j] = aug[i][SUB_DIM + j]; } } Some(res) } fn block_invert(bm: &BlockMatrix) -> BlockMatrix { let mut aug = vec![vec![vec![vec![0; SUB_DIM]; SUB_DIM]; 2 * DIM]; DIM]; for i in 0..DIM { for j in 0..DIM { aug[i][j] = bm.data[i][j].clone(); } for j in DIM..2 * DIM { aug[i][j] = if j - DIM == i { get_identity_sub() } else { get_zero_sub() }; } } for i in 0..DIM { let p_inv = sub_mat_inv(&aug[i][i]) .expect("Singular Block Pivot"); aug[i].par_iter_mut().for_each(|block| { *block = sub_mat_mul(&p_inv, block); }); let pivot_row = aug[i].clone(); aug.par_iter_mut().enumerate().for_each(|(k, row)| { if k != i { let factor = row[i].clone(); for j in 0..2 * DIM { let prod = sub_mat_mul(&factor, &pivot_row[j]); for r in 0..SUB_DIM { for c in 0..SUB_DIM { row[j][r][c] = (row[j][r][c] + MOD - prod[r][c]) % MOD; } } } } }); } let mut inv_data = vec![vec![get_zero_sub(); DIM]; DIM]; for i in 0..DIM { for j in 0..DIM { inv_data[i][j] = aug[i][DIM + j].clone(); } } BlockMatrix { data: inv_data } } fn main() -> Result<()> { let mut rng = rand::thread_rng(); let mut encr_data = vec![vec![vec![vec![0; SUB_DIM]; SUB_DIM]; DIM]; DIM]; for i in 0..DIM { for j in 0..DIM { for r in 0..SUB_DIM { for c in 0..SUB_DIM { encr_data[i][j][r][c] = rng.gen_range(0..MOD); } } } } let encr = BlockMatrix { data: encr_data }; save_key_to_file(&encr, "secret.key")?; let start_inv = Instant::now(); let decr = block_invert(&encr); println!("Inversion time: {:?}", start_inv.elapsed()); (0..DIM).into_par_iter().for_each(|i| { for j in 0..DIM { let mut res_block = get_zero_sub(); for k in 0..DIM { let prod = sub_mat_mul(&encr.data[i][k], &decr.data[k][j]); for r in 0..SUB_DIM { for c in 0..SUB_DIM { res_block[r][c] = (res_block[r][c] + prod[r][c]) % MOD; } } } let target = if i == j { get_identity_sub() } else { get_zero_sub() }; if res_block != target { println!("Verification failed!"); } } }); println!("✅ VERIFIED"); Ok(()) } fn save_key_to_file(bm: &BlockMatrix, filename: &str) -> Result<()> { let file = File::create(filename)?; let mut writer = BufWriter::new(file); for i in 0..DIM { for j in 0..DIM { for r in 0..SUB_DIM { for c in 0..SUB_DIM { write!(writer, "{} ", bm.data[i][j][r][c])?; } writeln!(writer)?; } } } Ok(()) }

Skeptical about that parallel speedup by 23 times?!?!

Don't be, I ran the Linux "top" command while running the parallel.rs program, and do you know what it shows?
The program is using 2213% to 2395% CPU! That means using 22 to (all) 24 threads simultaneously!
My Dell PowerEdge has dual (two) Intel Xeon E5-2620 v3 microarchitecture, each has 6 cores, 12 threads,
so 12 cores, 24 threads total, and this program put them all to work!
 
linux top command shows 2213% CPU utilization, so 22 threads
 
linux top command shows 2213% CPU utilization, so 22 threads
Observations on the E5-2620 v3 specifically:
  • L3 Cache (30 MiB total in both CPU's): This is huge. It's likely why your matrix inversion is running so well; the Xeon can keep large chunks of your BlockMatrix close to the silicon instead of going out to the slower RAM.
  • AVX2 Support: Your Rust code is likely using "SIMD" (Single Instruction, Multiple Data) instructions automatically. This means that inside each of those 24 threads, the CPU is processing multiple numbers in a single clock cycle.

The Rayon Parallelism Engine

To achieve maximum throughput on the E5-2620 v3 processors, this program utilizes Rayon, a data-parallelism library for Rust. Rayon's core strength is its "work-stealing" scheduler. Unlike traditional threading where you manually manage a pool, Rayon dynamically balances the workload across all 24 logical cores. When one core finishes its set of matrix sub-blocks, it "steals" pending work from other cores, ensuring that no single thread sits idle while others are bottlenecked by heavy calculations.
 
The transition from serial to parallel is mathematically seamless because of Rust's Send and Sync traits. Because the BlockMatrix is composed of thread-safe types, Rayon can guarantee that memory access across the NUMA boundary (between your two physical CPU sockets) remains data-race free. By simply replacing standard iterators with par_iter(), the engine transforms a linear O(n^3) block inversion into a distributed task, resulting in the near-total CPU saturation observed in the system telemetry (the Linux "top" command, shown in the print-screen above).

 
Please note: in a Galois Field, subtraction is equivalent to adding the additive inverse.
 
By adding the prime MOD before subtracting, the program creates a positive buffer that prevents underflow, ensuring that the result of every operation remains a valid, positive element of the finite field. The program uses the u64 (unsigned 64-bit integer) data type for all internal calculations to maximize hardware throughput and maintain strict positive alignment with the Galois Field. To perform subtraction within this unsigned space, the program uses the Value + MOD - Subtrahend pattern. By adding the prime modulus before subtracting, the system creates a positive 'buffer' that prevents integer underflow while ensuring the final result remains mathematically equivalent within GF(997727).
 
Please see the two green highlights which I added into the source code above, to show exactly where this occurs.

 
What does a (tiny example) 2x2x3x3 matrix-within-matrix key file look like?
A lot of integers which are all in the range 0..997726 because we are using math in GF(997727).
 
--- ENCRYPTION KEY (E) ---
[[[[296694, 390531, 243675], [320449, 970431, 98244], [627164, 953395, 916306]], 
  [[430087, 647583, 421775], [475138, 345706, 880216], [733525, 368995, 692281]]], 
 [[[637979, 335126, 854517], [71159, 285713, 495290], [214226, 716943, 923249]], 
  [[137641, 656211, 147803], [735157, 506297, 348918], [455121, 649579, 248797]]]]

--- DECRYPTION KEY (D) ---
[[[[813163, 768189, 61393], [826065, 60489, 275868], [261426, 297233, 455745]], 
  [[249617, 528552, 440182], [520685, 959134, 550012], [804146, 877024, 121624]]], 
 [[[361085, 589785, 121899], [113114, 125523, 957137], [150194, 96425, 201282]], 
  [[46970, 404510, 885054], [678588, 155444, 608800], [25970, 885063, 979838]]]]

 
matrix-within-matrix Excel spreadsheet showing matrix key sizes, plaintext vector sizes, and timings
 

Technical Whitepaper: The Matrix-with-Matrix Cryptosystem


1. Executive Summary

The Matrix-within-Matrix Cryptosystem is an advanced evolution of classical polyalphabetic ciphers. By treating every element of the encryption process as a sub-matrix rather than a single integer, the system creates a massive computational barrier. It operates within a non-commutative ring of matrices over a finite field, ensuring that every byte of data is subjected to quadratic diffusion.

2. Algebraic Glossary

3. Mathematical Work Multiplier

The "Strength" of the system is measured by the increase in operations compared to a standard Hill cipher. While a baseline cipher uses O(N) operations per element, the Matrix-within-Matrix system scales at O(SUB_DIM²) per element.

In a configuration where SUB_DIM is 20, each byte of plaintext is processed by 400 times more mathematical operations than a standard matrix cipher. At SUB_DIM 48, this multiplier increases to 2,304.

4. Performance Benchmarking

Empirical testing demonstrates that the system scales predictably without hitting a "performance wall," even when key sizes exceed typical L3 cache limits.

DIM
"M"
SUB_DIM
"N"
Key Elements
M * M * N * N
Inversion Time
(seconds)
Plaintext Bits
per Block (Vector)
M * N * N * 20
10 20 40,000 0.019 (parallel) 80,000
20 40 640,000 0.370 (parallel) 640,000
20 50 1,000,000 0.702 (parallel) 1,000,000
48 48 5,308,416 7.125 (parallel) 2,211,840

Data derived from hardware testing on 2026-02-22 and 2026-02-23.

5. Security & Post-Quantum (PQ) Assessment

The system provides a "Lattice-Like" defense. Unlike RSA or ECC, it does not rely on factoring or discrete logarithms, making it resistant to Shor’s Algorithm. To remain secure against Grover’s Algorithm (quantum brute force), the total keyspace is expanded through the high DIM and SUB_DIM parameters.

6. Resilience to Known-Plaintext Attacks (KPA)

The system mitigates KPA through Dimensionality Explosion. At a 48x48 configuration, an attacker must solve for over 5.3 million unknown variables. The non-commutative nature of the ring ensures that standard linear solver shortcuts are inapplicable.


 

Deep Dive: The Quadratic Work Multiplier & Matrix Ring Security

1. The Geometry of Diffusion

In a standard Hill cipher, a single byte of plaintext is multiplied by a single scalar in the key matrix. In the Matrix-within-Matrix Matrix Cryptosystem, a single byte of plaintext is conceptually expanded into a coordinate within a sub-matrix.

Because every "element" in our DIM × DIM grid is actually a SUB_DIM × SUB_DIM matrix, the system achieves Horizontal and Vertical Diffusion simultaneously:

  • Horizontal Diffusion: The byte interacts with an entire row of the inner sub-matrix.
  • Vertical Diffusion: The resulting product is then distributed across the entire outer block structure.

2. Understanding the "Math Multiplier"

The primary benefit of this architecture is the Quadratic Work Multiplier. We calculate the cost of processing a single byte of data as follows:

In our system, for every block-level interaction, we perform a matrix-matrix multiplication. For an inner matrix of size N (where N = SUB_DIM), this requires multiplications and additions. However, when we average this work across the total elements in the block, the cost per byte scales at O(SUB_DIM²).

System Type Operations per Element At SUB_DIM = 20 At SUB_DIM = 48
Standard Hill Cipher 1 (Linear) 1x Math 1x Math
Matrix-within-Matrix Matrix SUB_DIM² (Quadratic) 400x Math 2,304x Math

Note: This multiplier represents the "computational tax" an attacker must pay for every single byte they attempt to analyze or brute-force.

3. The Non-Commutative Advantage

One of the most significant benefits is operating in a Non-Commutative Ring. In standard modular arithmetic, A × B = B × A. This allows attackers to use simple division to isolate keys.

In our matrix ring, the order of operations is immutable (A × B ≠ B × A). This renders standard algebraic "shortcut" attacks useless, as the attacker cannot simply "divide out" the plaintext from the ciphertext without knowing the exact orientation and sequence of the sub-matrix multiplications.

4. Performance Efficiency at Scale

Despite the massive increase in mathematical density, the system remains efficient due to Block Gauss-Jordan Elimination. Our benchmarking shows that even when the complexity increases 1,630x, the hardware handles the operations predictably:

  • Predictable Memory Access: The algorithm processes sub-matrices in contiguous memory blocks, which is highly compatible with CPU pre-fetching.
  • Cache Resilience: By working on SUB_DIM blocks, the "hot" data often fits within the L1/L2 cache even when the total Key File is as large as 84MB.

5. Security Conclusion

The "Quadratic Nature" of the system means that for every small increase in user effort (parameter size), the attacker's burden grows exponentially. This makes the Matrix-within-Matrix Matrix Cryptosystem an ideal candidate for high-security environments where mathematical "weight" is a priority.


 

The Engineering Behind Hill-GF-4D: Why Rust?

1. Memory Management: Stack vs. Heap

In the Hill-GF-4D system, performance is driven by how Rust manages data storage. Small, fixed-size variables like the modulus (MOD) or loop counters are stored on the Stack, which is incredibly fast but limited in size. However, the massive 4D matrix structures—which can reach 84MB in size—are allocated on the Heap.

By using the Vec<Vec<u64>> type, the program requests large contiguous blocks of memory from the system's RAM. This allows the Hill-GF-4D engine to handle millions of elements without crashing, as the Heap can expand to use all available system memory while the Stack stays light and responsive.

2. Zero-Cost Abstractions and Ownership

One of the primary reasons for the system's speed (such as the 0.101s inversion for 10x20 keys) is Rust's Ownership model. In many languages, a "Garbage Collector" must periodically stop the program to clean up memory, causing "stutters" during heavy math operations. Rust eliminates this.

When the block_invert function finishes, Rust automatically deallocates the temporary "Augmented" matrices. This "deterministic destruction" ensures that the program's memory footprint doesn't grow uncontrollably.

3. Explicit Cloning and Data Integrity

In the source code, you will notice the .clone() command frequently used before matrix multiplications. Because matrices live on the Heap, Rust's safety rules prevent multiple parts of the program from changing the same data at the same time.

By explicitly cloning the factor matrix (line 97), the program creates a dedicated "workspace" for that specific row-reduction step. This ensures that the original Encryption Key remains untouched and corruption-free while the Decryption Key is being mathematically derived.

4. Optimized Mathematical Throughput

By leveraging unsigned 64-bit integers (u64), the program bypasses the complexity of signed-bit logic. Modern CPUs can process u64 operations in parallel more efficiently than signed counterparts. This architectural choice is why the relationship between key size and timing remains remarkably consistent, allowing the system to scale to 5.3 million elements without hitting a performance wall.

5. Cache-Optimized Design (The "Conveyor Belt" Effect)

Modern CPUs have a tiny, lightning-fast memory called the L3 Cache. The Hill-GF-4D system is highly efficient because it stores data in "contiguous blocks"—meaning all the numbers for a sub-matrix are lined up right next to each other in RAM.

This allows the CPU to treat the data like a conveyor belt: it predicts what the program needs next and pre-loads it into the L3 cache before the math even starts. Even though your largest key files (84MB) are bigger than most caches, the program stays fast (0.101s to 164s) because it only ever "looks" at one small, perfectly-sized block at a time, never forcing the CPU to wait for slow system RAM.

6. Parallel Potential (The "Factory Floor")

The math in Hill-GF-4D is "embarrassingly parallel". In the main.rs code, when we multiply the outer DIM × DIM blocks, each of those smaller SUB_DIM multiplications is completely independent of the others.

While the current version of Hill-GF-4D runs on a single high-speed thread to ensure maximum stability and memory safety, its 4D-block architecture is natively designed for parallel scaling. The independent nature of the matrix-ring calculations means the system is 'multi-core ready' for future high-throughput enterprise updates.


 

Would you like to move from 4D (four dimensions) to 5D (five dimensions)?
We will replace the integers inside the matrices with POLYNOMIALS!

We have seen how our architecture uses matrices nested within matrices to organize and manipulate large blocks of data. While these structures initially contained simple integers within the prime field GF(997727), we can replace those integers with polynomials of degree n. This shift moves our operations from the base field GF(997727) into the extension field GF(997727^n). In this higher-dimensional space, an 'element' is no longer a single value, but a vector of coefficients that follows the strict laws of polynomial arithmetic—addition, subtraction, and most importantly, inversion via an irreducible polynomial.
 
The cryptographic strength gained from this transition is substantial. By moving to an extension field, we exponentially increase the size of the keyspace without requiring a larger modulus. While a standard Hill Cipher is vulnerable to known-plaintext attacks through linear algebra, the introduction of polynomial multiplication and modular reduction creates a much more complex set of equations. The 'hidden' layers of the extension field mean that an attacker cannot simply solve for individual integers; they must contend with the interaction of all coefficients within the polynomial structure simultaneously. This effectively masks the underlying linear relationships, providing a robust defense against frequency analysis and modern algebraic attacks.
 
Key Points:
  • Keyspace Expansion: Moving to GF(p^n) means the number of possible "elements" in the matrix grows from p to p^n. Even for small degrees like 8, the search space for an attacker becomes astronomical.
  • Non-Linearity: While the matrix operations are linear over the extension field, the relationship between the individual integer coefficients becomes highly complex due to the polynomial reduction step (the modulo of the irreducible polynomial).
  • Efficiency: As the timings showed, even though the math becomes much more sophisticated, the actual encryption of data remains incredibly fast because it still relies on the fundamental efficiency of matrix-vector multiplication.

 

This small example is a 2x2 matrix inside a 2x2 outer matrix, and each cell is filled with a random polynomial with three coefficients.

Please recall, from my paper, that the representation in parenthesis "(566514,537509,14284)" is the polynomial: 566514x^2 + 537509x + 14284

matrix-within-matrix filled with polynomials in GF(997727^3)

What does the programming for FIVE DIMENSIONS look like?
There are five subscripts inside the program: [i][j][r][c][k]


// 1. Initialize random Encryption Matrix (E)
    let mut encr_data = vec![vec![vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM]; DIM]; DIM];
    for i in 0..DIM {
        for j in 0..DIM {
            for r in 0..SUB_DIM {
                for c in 0..SUB_DIM {
                    let mut coeffs = vec![0; DEGREE];
                    for k in 0..DEGREE { coeffs[k] = rng.gen_range(0..MOD); }
                    encr_data[i][j][r][c] = Poly { coeffs };
                }
            }
        }
    }

 

This is what Encryption looks like: Matrix * Plaintext = Ciphertext, all are Polynomials

Matrix-within-Matrix times Vector equals Vector, all filled with polynomials
 
The numbers in the plaintext vector vector (87, 101, 32, 116, 104, 101, 32, 80, 101, 111, 112, 108)
represent the ASCII string "We the Peopl" the first 12 letters in the Preamble to the US Constitution."
 
Matrix-within-Matrix times ciphertext vector equals plaintext vector

The program below has an 8x8 matrix inside every element of an 8x8 matrix (so effectively a 64x64 matrix)
and each element of the matrix is a polynomial, specifically in GF(997727^8) so each has eight coeffiecients.
The encryption key matrix contains 32,768 integers each is 20 bits, so 655,360 bits in this encryption key.
Each block of plaintext is 512 integers each is 20 bits, so 10,240 bits in each plaintext block (more than 1,000 bytes).

Rust Source Highlighting
use rand::Rng;
use std::time::Instant; 
use rayon::prelude::*;
use std::fs::File;
use std::io::{Write, BufWriter, Read}; 

const MOD: u64 = 997727;
const DEGREE: usize = 8;
const DIM: usize = 8;        
const SUB_DIM: usize = 8;   

lazy_static::lazy_static! {
    static ref IRREDUCIBLE: Vec<u64> = {
        let mut v = vec![0; DEGREE + 1];
        v[DEGREE] = 1; 
        v[1] = 1;      
        v[0] = 1;      
        v
    };
}

#[derive(Clone, Debug, PartialEq, Eq)]
struct Poly {
    coeffs: Vec<u64>,
}

impl Poly {
    fn zero() -> Self { Poly { coeffs: vec![0; DEGREE] } }
    fn identity() -> Self {
        let mut p = Self::zero();
        p.coeffs[0] = 1;
        p
    }
    fn add(&self, other: &Self) -> Self {
        let mut res = vec![0; DEGREE];
        for i in 0..DEGREE { res[i] = (self.coeffs[i] + other.coeffs[i]) % MOD; }
        Poly { coeffs: res }
    }
    fn sub(&self, other: &Self) -> Self {
        let mut res = vec![0; DEGREE];
        for i in 0..DEGREE { res[i] = (self.coeffs[i] + MOD - other.coeffs[i]) % MOD; }
        Poly { coeffs: res }
    }
    fn mul(&self, other: &Self) -> Self {
        let mut prod = vec![0u64; 2 * DEGREE - 1];
        for i in 0..DEGREE {
            for j in 0..DEGREE {
                prod[i + j] = (prod[i + j] + (self.coeffs[i] * other.coeffs[j])) % MOD;
            }
        }
        for i in (DEGREE..prod.len()).rev() {
            let factor = prod[i];
            for j in 0..DEGREE {
                let term = (factor * IRREDUCIBLE[j]) % MOD;
                prod[i - DEGREE + j] = (prod[i - DEGREE + j] + MOD - term) % MOD;
            }
        }
        Poly { coeffs: prod[0..DEGREE].to_vec() }
    }
}

// --- Math Helpers ---
fn mod_inv(a: u64, m: u64) -> u64 {
    let mut mn = (m as i64, a as i64);
    let mut xy = (0, 1);
    while mn.1 != 0 {
        xy = (xy.1, xy.0 - (mn.0 / mn.1) * xy.1);
        mn = (mn.1, mn.0 % mn.1);
    }
    let mut res = xy.0;
    if res < 0 { res += m as i64; }
    res as u64
}

fn poly_inv(poly: &Poly) -> Option<Poly> {
    let mut r0 = IRREDUCIBLE.clone();
    let mut r1 = poly.coeffs.clone();
    while r1.len() > 0 && r1[r1.len()-1] == 0 { r1.pop(); }
    if r1.is_empty() { return None; }
    let mut t0 = vec![0];
    let mut t1 = vec![1];
    while !r1.is_empty() {
        let mut q = vec![0; r0.len() - r1.len() + 1];
        let inv_lead = mod_inv(r1[r1.len() - 1], MOD);
        let mut temp_r = r0.clone();
        for i in (0..q.len()).rev() {
            q[i] = (temp_r[r1.len() - 1 + i] * inv_lead) % MOD;
            for j in 0..r1.len() {
                let sub = (q[i] * r1[j]) % MOD;
                temp_r[i + j] = (temp_r[i + j] + MOD - sub) % MOD;
            }
        }
        r0 = r1;
        while temp_r.len() > 0 && temp_r[temp_r.len()-1] == 0 { temp_r.pop(); }
        r1 = temp_r;
        let mut q_t1 = vec![0; q.len() + t1.len() - 1];
        for i in 0..q.len() {
            for j in 0..t1.len() { q_t1[i+j] = (q_t1[i+j] + q[i] * t1[j]) % MOD; }
        }
        let mut next_t = t0.clone();
        if next_t.len() < q_t1.len() { next_t.resize(q_t1.len(), 0); }
        for i in 0..q_t1.len() { next_t[i] = (next_t[i] + MOD - q_t1[i]) % MOD; }
        t0 = t1; t1 = next_t;
    }
    let scale = mod_inv(r0[0], MOD);
    let mut final_coeffs = vec![0; DEGREE];
    for i in 0..t0.len().min(DEGREE) { final_coeffs[i] = (t0[i] * scale) % MOD; }
    Some(Poly { coeffs: final_coeffs })
}

type Matrix = Vec<Vec<Poly>>;
struct BlockMatrix { data: Vec<Vec<Matrix>> }

fn get_identity_sub() -> Matrix {
    let mut m = vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM];
    for i in 0..SUB_DIM { m[i][i] = Poly::identity(); }
    m
}
fn get_zero_sub() -> Matrix { vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM] }

fn sub_mat_mul(a: &Matrix, b: &Matrix) -> Matrix {
    let mut res = vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM];
    for i in 0..SUB_DIM {
        for j in 0..SUB_DIM {
            let mut sum = Poly::zero();
            for k in 0..SUB_DIM { sum = sum.add(&a[i][k].mul(&b[k][j])); }
            res[i][j] = sum;
        }
    }
    res
}

fn block_mat_mul(a: &BlockMatrix, b: &BlockMatrix) -> BlockMatrix {
    let mut res_data = vec![vec![get_zero_sub(); DIM]; DIM];
    for i in 0..DIM {
        for j in 0..DIM {
            let mut sum_mat = get_zero_sub();
            for k in 0..DIM {
                let prod = sub_mat_mul(&a.data[i][k], &b.data[k][j]);
                for r in 0..SUB_DIM {
                    for c in 0..SUB_DIM {
                        sum_mat[r][c] = sum_mat[r][c].add(&prod[r][c]);
                    }
                }
            }
            res_data[i][j] = sum_mat;
        }
    }
    BlockMatrix { data: res_data }
}

fn sub_mat_inv(a: &Matrix) -> Option<Matrix> {
    let mut aug = vec![vec![Poly::zero(); 2 * SUB_DIM]; SUB_DIM];
    for i in 0..SUB_DIM {
        for j in 0..SUB_DIM { aug[i][j] = a[i][j].clone(); }
        aug[i][SUB_DIM + i] = Poly::identity();
    }
    for i in 0..SUB_DIM {
        let mut pivot = i;
        while pivot < SUB_DIM && aug[pivot][i] == Poly::zero() { pivot += 1; }
        if pivot == SUB_DIM { return None; }
        aug.swap(i, pivot);
        let inv = poly_inv(&aug[i][i])?;
        for j in 0..2 * SUB_DIM { aug[i][j] = aug[i][j].mul(&inv); }
        for k in 0..SUB_DIM {
            if k != i {
                let factor = aug[k][i].clone();
                for j in 0..2 * SUB_DIM {
                    let sub = factor.mul(&aug[i][j]);
                    aug[k][j] = aug[k][j].sub(&sub);
                }
            }
        }
    }
    let mut res = vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM];
    for i in 0..SUB_DIM {
        for j in 0..SUB_DIM { res[i][j] = aug[i][SUB_DIM + j].clone(); }
    }
    Some(res)
}

fn block_invert(bm: &BlockMatrix) -> BlockMatrix {
    let mut aug = vec![vec![vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM]; 2 * DIM]; DIM];
    for i in 0..DIM {
        for j in 0..DIM { aug[i][j] = bm.data[i][j].clone(); }
        for j in DIM..2 * DIM { aug[i][j] = if j - DIM == i { get_identity_sub() } else { get_zero_sub() }; }
    }
    for i in 0..DIM {
        let p_inv = sub_mat_inv(&aug[i][i]).expect("Singular Block");
        aug[i].par_iter_mut().for_each(|block| *block = sub_mat_mul(&p_inv, block));
        let pivot_row = aug[i].clone();
        aug.par_iter_mut().enumerate().for_each(|(k, row)| {
            if k != i {
                let factor = row[i].clone();
                for j in 0..2 * DIM {
                    let prod = sub_mat_mul(&factor, &pivot_row[j]);
                    for r in 0..SUB_DIM {
                        for c in 0..SUB_DIM { row[j][r][c] = row[j][r][c].sub(&prod[r][c]); }
                    }
                }
            }
        });
    }
    let mut inv_data = vec![vec![get_zero_sub(); DIM]; DIM];
    for i in 0..DIM { for j in 0..DIM { inv_data[i][j] = aug[i][DIM + j].clone(); } }
    BlockMatrix { data: inv_data }
}

fn process_block(p_vec: &Vec<Vec<Poly>>, key: &BlockMatrix) -> Vec<Vec<Poly>> {
    let mut res_vec = vec![vec![Poly::zero(); SUB_DIM]; DIM];
    for j in 0..DIM {
        for sc in 0..SUB_DIM {
            let mut sum = Poly::zero();
            for i in 0..DIM {
                for sr in 0..SUB_DIM {
                    sum = sum.add(&p_vec[i][sr].mul(&key.data[i][j][sr][sc]));
                }
            }
            res_vec[j][sc] = sum;
        }
    }
    res_vec
}

// --- IO Logic ---

fn save_nested_data(data: &Vec<Vec<Poly>>, filename: &str, header: &str) -> std::io::Result<()> {
    let file = File::create(filename)?;
    let mut writer = BufWriter::new(file);
    writeln!(writer, "{}", header)?;
    writeln!(writer, "MOD: {}, DIM: {}, SUB_DIM: {}, DEGREE: {}", MOD, DIM, SUB_DIM, DEGREE)?;
    for i in 0..DIM {
        write!(writer, "Block-Row [{}]: [", i)?;
        for j in 0..SUB_DIM {
            let poly = &data[i][j];
            write!(writer, "(")?;
            for k in 0..DEGREE {
                write!(writer, "{}", poly.coeffs[k])?;
                if k < DEGREE - 1 { write!(writer, ",")?; }
            }
            write!(writer, ")")?;
            if j < SUB_DIM - 1 { write!(writer, ", ")?; }
        }
        writeln!(writer, "]")?;
    }
    Ok(())
}

fn save_key_pair_file(encr: &BlockMatrix, decr: &BlockMatrix, filename: &str) -> std::io::Result<()> {
    let file = File::create(filename)?;
    let mut writer = BufWriter::new(file);
    writeln!(writer, "--- HILL-GF-POLY FULL KEY PAIR ---")?;
    writeln!(writer, "MOD: {}, DIM: {}, SUB_DIM: {}, DEGREE: {}", MOD, DIM, SUB_DIM, DEGREE)?;
    writeln!(writer, "\n=== ENCRYPTION MATRIX (E) ==="")?;
    for i in 0..DIM {
        for j in 0..DIM {
            write!(writer, "E[{},{}]: [", i, j)?;
            for r in 0..SUB_DIM {
                for c in 0..SUB_DIM {
                    let poly = &encr.data[i][j][r][c];
                    write!(writer, "(")?;
                    for k in 0..DEGREE {
                        write!(writer, "{}", poly.coeffs[k])?;
                        if k < DEGREE - 1 { write!(writer, ",")?; }
                    }
                    write!(writer, ")")?;
                    if r == SUB_DIM - 1 && c == SUB_DIM - 1 { } else { write!(writer, ", ")?; }
                }
            }
            writeln!(writer, "]")?;
        }
    }
    writeln!(writer, "\n=== DECRYPTION MATRIX (D) ==="")?;
    for i in 0..DIM {
        for j in 0..DIM {
            write!(writer, "D[{},{}]: [", i, j)?;
            for r in 0..SUB_DIM {
                for c in 0..SUB_DIM {
                    let poly = &decr.data[i][j][r][c];
                    write!(writer, "(")?;
                    for k in 0..DEGREE {
                        write!(writer, "{}", poly.coeffs[k])?;
                        if k < DEGREE - 1 { write!(writer, ",")?; }
                    }
                    write!(writer, ")")?;
                    if r == SUB_DIM - 1 && c == SUB_DIM - 1 { } else { write!(writer, ", ")?; }
                }
            }
            writeln!(writer, "]")?;
        }
    }
    Ok(())
}

fn main() {
    let mut rng = rand::thread_rng();
    println!("--- Hill-GF-Poly 5D Cipher Engine ---");
    println!("CONFIG: MOD={}, DIM={}, SUB_DIM={}, DEGREE={}", MOD, DIM, SUB_DIM, DEGREE);

    // 1. Initialize random Encryption Matrix (E)
    let mut encr_data = vec![vec![vec![vec![Poly::zero(); SUB_DIM]; SUB_DIM]; DIM]; DIM];
    for i in 0..DIM {
        for j in 0..DIM {
            for r in 0..SUB_DIM {
                for c in 0..SUB_DIM {
                    let mut coeffs = vec![0; DEGREE];
                    for k in 0..DEGREE { coeffs[k] = rng.gen_range(0..MOD); }
                    encr_data[i][j][r][c] = Poly { coeffs };
                }
            }
        }
    }
    let encr = BlockMatrix { data: encr_data };
    println!("Step 1: Encryption Matrix E initialized.");

    // 2. Calculate functional Inverse (D)
    let start_keys = Instant::now();
    let decr = block_invert(&encr);
    println!("Step 2: Decryption Matrix D calculated in {:?}", start_keys.elapsed());

    // 3. Write Key Pair to disk
    let key_filename = format!("POLY-M{}-D{}-S{}-DEG{}.key", MOD, DIM, SUB_DIM, DEGREE);
    save_key_pair_file(&encr, &decr, &key_filename).expect("Failed to save keys");
    println!("Step 3: Key file [{}] written.", key_filename);

    // 4. EXPLICIT CHECK: E * D = I
    println!("Step 4: Performing explicit Matrix Check (E * D)...");
    let start_check = Instant::now();
    let result_mat = block_mat_mul(&encr, &decr);
    println!("Step 4: Matrix multiplication completed in {:?}", start_check.elapsed());

    // 5. Verify Identity
    let mut is_identity = true;
    for i in 0..DIM {
        for j in 0..DIM {
            let expected = if i == j { get_identity_sub() } else { get_zero_sub() };
            if result_mat.data[i][j] != expected { is_identity = false; }
        }
    }
    if is_identity {
        println!("✅ Step 5: SUCCESS! E * D = Identity Matrix.");
    } else {
        println!("❌ Step 5: FAILURE! Matrices are not inverses.");
    }

    // 6. Load and display Plaintext
    let mut f = File::open("plaintext.txt").expect("Could not find plaintext.txt");
    let mut raw_bytes = Vec::new();
    f.read_to_end(&mut raw_bytes).unwrap();
    let block_bytes = DIM * SUB_DIM * DEGREE;
    let input_content = if raw_bytes.len() >= block_bytes {
        raw_bytes[0..block_bytes].to_vec()
    } else {
        let mut padded = raw_bytes.clone();
        while padded.len() < block_bytes { padded.push(b' '); }
        padded
    };
    println!("\nStep 6: Original Plaintext Block:");
    println!("\"{}\"\n", String::from_utf8_lossy(&input_content));

    // 7. Convert text to Polynomial Vector
    let mut p_vec = vec![vec![Poly::zero(); SUB_DIM]; DIM];
    let mut b_idx = 0;
    for i in 0..DIM {
        for j in 0..SUB_DIM {
            let mut c = vec![0; DEGREE];
            for k in 0..DEGREE { c[k] = input_content[b_idx] as u64; b_idx += 1; }
            p_vec[i][j] = Poly { coeffs: c };
        }
    }
    println!("Step 7: Plaintext converted to polynomial vector.");

    // 8. Encrypt
    let enc_start = Instant::now();
    let cipher_vec = process_block(&p_vec, &encr);
    save_nested_data(&cipher_vec, "ciphertext.txt", "POLY-HILL-5D-CIPHERTEXT").unwrap();
    println!("Step 8: Encryption complete in {:?}. Saved to ciphertext.txt", enc_start.elapsed());

    // 9. Decrypt (Functional Verification)
    let dec_start = Instant::now();
    let recovered_vec = process_block(&cipher_vec, &decr);
    println!("Step 9: Decryption complete in {:?}.", dec_start.elapsed());

    // 10. Recover text and display
    let mut recovered_bytes = Vec::new();
    for i in 0..DIM {
        for j in 0..SUB_DIM {
            for k in 0..DEGREE { recovered_bytes.push(recovered_vec[i][j].coeffs[k] as u8); }
        }
    }
    println!("\nStep 10: Recovered Plaintext Block:");
    println!("\"{}\"\n", String::from_utf8_lossy(&recovered_bytes));

    // 11. Final Integrity check
    if String::from_utf8_lossy(&input_content).trim_end() == String::from_utf8_lossy(&recovered_bytes).trim_end() {
        println!("✅ Step 11: SUCCESS! Full data cycle verified.");
    } else {
        println!("❌ Step 11: FAILURE! Data mismatch detected.");
    }
}

 
matrix-within-matrix Excel spreadsheet showing matrix key sizes, plaintext vector sizes, and timings
 

Plaintext: The Preamble to the Constitution of the United States

We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence,
promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America.

The Encryption using 16x16x16x16 4D matrix with polynomials in GF(997727^8)

# cargo run --bin parallel-poly --release
   Compiling matrix-within-matrix v0.1.0 (/var/www/html/crypto/GF997727/matrix-within-matrix)
    Finished `release` profile [optimized] target(s) in 2.81s
     Running `target/release/parallel-poly`
--- Hill-GF-Poly 5D Cipher Engine ---
CONFIG: MOD=997727, DIM=16, SUB_DIM=16, DEGREE=8
Step 1: Encryption Matrix E initialized.
Step 2: Decryption Matrix D calculated in 2.7023054s
Step 3: Key file [POLY-M997727-D16-S16-DEG8.key] written.
Step 4: Performing explicit Matrix Check (E * D)...
Step 4: Matrix multiplication completed in 10.421212572s
✅ Step 5: SUCCESS! E * D = Identity Matrix.
Step 6: Original Plaintext Block:
"We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence, 
promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America."
Step 7: Plaintext converted to polynomial vector.
Step 8: Encryption complete in 45.473363ms. Saved to ciphertext.txt
Step 9: Decryption complete in 45.029343ms.
Step 10: Recovered Plaintext Block:
"We the People of the United States, in Order to form a more perfect Union, establish Justice, insure domestic Tranquility, provide for the common defence,
 promote the general Welfare, and secure the Blessings of Liberty to ourselves and our Posterity, do ordain and establish this Constitution for the United States of America."
✅ Step 11: SUCCESS! Full data cycle verified.

Ciphertext using 16x16 matrices inside every cell of a 16x16 matrix, filled with 8-coefficient polynomials in GF(997727^8)

Ciphertext-Row [0] contains 16 polynomials:
[(269252,94110,56829,149882,474362,189479,773161,859485), (297082,73796,367200,668477,98240,204115,97707,744870),
(639991,4050,670432,134307,248454,823466,625306,530963), (580189,144299,344803,324488,181314,758225,97740,583407),
(447450,731612,434018,317672,984007,292166,180585,848437), (658935,283686,255581,728067,517892,358595,257034,505038),
(688920,574969,231419,342327,523245,132899,6945,323117), (167460,635070,809213,436326,96199,485595,329918,984311),
(263851,37716,828141,981186,665062,107153,404668,780826), (967445,807447,640859,484836,638783,590457,952264,274655),
(252757,650027,438392,58592,710116,514111,709323,927700), (855299,883710,87360,712623,316964,448471,766266,949268),
(713853,119077,44890,781685,882207,317452,825302,119149), (3138,126060,238672,885372,159888,534366,763441,870498),
(990575,646610,368632,398039,584392,407369,186593,873921), (788200,266052,476406,63862,248822,384134,875260,270817)]
 
Ciphertext-Row [1]: contains 16 polynomials
[(168138,160922,191489,163172,460447,258442,923105,946566), (127426,527589,136366,342619,468502,243876,776114,511456),
(158996,216586,427737,654315,701415,168284,928259,972951), (974689,610550,465153,521157,426121,587683,176425,354305),
(354677,31522,190230,522671,890164,945066,524706,951216), (794533,876254,662267,649403,613432,75225,436525,824271),
(839134,949001,771741,746287,55099,763147,44446,13550), (103379,601393,348894,156920,315570,277173,508567,381288),
(779175,393012,309265,500713,507862,799951,677050,633169), (858495,604931,123630,556025,710228,496265,280813,92079),
(225001,40567,77394,76531,658740,896190,473713,793805), (437868,194395,365711,29535,816083,4433,25486,710234),
(647385,295820,432089,801427,922351,806678,256018,107855), (36866,767937,251286,812908,250438,499213,339720,104378),
(855967,524890,497817,54701,517670,920348,477996,749361), (91603,255232,60481,870912,955303,142721,823819,376106)]
 
Ciphertext-Row [2] contains 16 polynomials:
[(146520,656836,746042,690380,784297,257866,354229,63359), (357337,116460,554929,878857,688570,450295,63156,856795),
(690199,776661,258754,856555,919883,540229,530266,120189), (41169,19630,544148,505545,575867,149141,380197,196909),
(281373,139935,540513,263702,986387,364704,762701,302451), (683421,922515,261572,331714,923401,797556,18613,594488),
(908980,951841,188702,983665,860116,87156,485011,696865), (71421,942925,359180,806263,578514,506725,36469,192694),
(348326,512324,761819,424212,396199,256293,259166,100912), (709647,84527,200350,788768,722153,187929,31634,380282),
(434196,201756,149664,772574,416534,705624,928789,33643), (272090,449054,595200,649943,619824,914190,404753,600530),
(186431,732015,258495,243389,956490,744333,937828,464047), (727938,176683,840205,347214,226080,591152,701030,338856),
(779523,49871,630710,302770,598000,834190,72958,596304), (261137,875228,982192,973176,555590,82579,961859,204293)]
 
Ciphertext-Row [3] contains 16 polynomials:
[(919987,638504,580725,531511,997542,238722,689365,940840), (180437,090998,529590,370867,698477,678276,888081,930393),
 (801475,834813,800521,050241,045723,200813,402301,919407), (388318,453124,278453,851598,872472,078627,896131,057215),
 (535184,774975,987386,180793,693641,053827,791825,933598), (273612,020244,730994,720340,738106,191802,362856,807589),
 (376059,486974,286279,471189,084690,343513,470959,880649), (835207,508964,203909,787635,264774,540978,705501,523134),
 (075681,066277,234077,488103,240689,941595,986381,626740), (781677,317517,458934,881253,210375,190798,540349,433601),
 (517231,190930,861596,485286,574792,435094,502996,274210), (384222,374199,444300,598735,068973,041504,920576,021642),
 (860797,595556,821924,313501,125356,208496,662980,039581), (062598,930058,325135,336327,357305,564608,363913,081822),
 (175512,917380,571528,039346,367782,994019,978146,137326), (548976,682845,113839,399790,935968,543283,235574,493641)]

 
Ciphertext-Row [4] contains 16 polynomials:
[(847285,358770,278957,598782,925734,629329,609276,247180), (906452,263817,1651,965552,924664,401065,873263,996806), (439363,297033,122865,496458,740957,765103,507263,58561), (843441,278468,309999,68890,117263,380029,877842,195432), (269769,960777,971914,471790,708753,463950,574377,35217), (248387,179952,276182,304974,342904,755618,146300,892780), (3399,573915,914797,28193,525612,783377,146065,558724), (463252,444616,979945,652576,967691,64438,179912,377641), (935152,146683,264956,306090,3367,776232,272150,200810), (879754,553998,121236,368044,324393,384377,723458,438650), (882125,438440,359501,826164,156544,969170,58661,615380), (727119,872399,39967,228680,410222,118583,72481,164641), (996782,753724,543551,808542,723650,478285,206848,574384), (6361,597303,558068,370426,760009,287344,483296,993878), (981072,895478,247648,19039,549273,616102,279217,267871), (204550,526921,565430,661975,612473,66602,387655,741764)]
 
Ciphertext-Row [5] contains 16 polynomials:
[(943356,609516,886235,69059,742405,324642,716135,743364), (758675,505629,148886,472059,253219,224359,364794,511365), (215676,552986,945536,735105,712700,224754,311119,987656), (67254,846147,13185,824668,183490,342296,770028,957838), (81307,98469,22659,687813,987526,502021,287509,352014), (191042,536219,166922,199007,719048,239132,723632,561123), (190801,836669,644989,77656,270546,821808,652825,877533), (496687,842051,886006,389391,387371,898078,717171,699474), (135774,210828,739848,239473,373362,985566,723713,489942), (218723,192757,53504,502189,783945,611107,741071,664058), (744779,247558,17861,409948,870853,227066,131961,630783), (244863,239416,690750,256992,409983,328415,782259,465796), (368433,277884,221070,149981,292985,902957,975266,38116), (610016,773906,53524,125308,282205,908060,106093,906882), (864985,587245,794528,734826,249974,234844,707083,832644), (739309,219607,564038,740146,947657,941640,992461,256507)]
 
Ciphertext-Row [6] contains 16 polynomials:
[(189423,76120,31494,912064,271104,945111,195267,930256), (688737,641664,174611,475146,787735,128867,209629,624746), (357089,616168,711100,203405,691711,574146,821223,906072), (263187,44854,52942,94466,945504,530689,961411,669964), (715000,516545,157792,304737,497119,382976,512549,499847), (837172,851948,174889,591975,537577,228227,882520,377727), (179523,975466,466005,377442,935843,793276,505524,931204), (136397,214699,386137,690020,390631,470664,238802,338416), (202444,786025,435373,419967,604866,779223,525191,371092), (705176,664926,179648,577279,922155,592270,542852,848812), (571406,301688,689799,102387,61266,266675,566962,28812), (449127,996476,679570,554506,69393,867226,953113,75652), (243310,720383,975431,372453,565718,303164,608772,694318), (657615,3211,455196,348962,898811,335426,607466,947950), (831233,931382,611023,481857,432602,252841,698711,986415), (15245,552597,89163,43540,165928,773626,209936,309873)]
 
Ciphertext-Row [7] contains 16 polynomials:
[(146002,992089,974977,236448,438325,111276,603988,471924), (809180,379248,177702,448152,854489,244825,938937,499237), (666994,369760,684903,862650,193644,646675,910341,769452), (964719,451705,63185,814948,312606,470822,75526,521149), (219378,126734,114402,526517,498679,189594,887264,194794), (979565,749501,324144,491795,264556,363308,771121,187107), (769601,319078,949271,912434,794607,243454,88349,223529), (246484,831940,51258,886260,604042,611315,119459,939524), (940946,843540,461500,225077,624098,432859,420065,824353), (561551,963402,778040,960492,913493,87413,507178,267976), (121209,968543,749109,231641,867672,950589,402848,627673), (106540,789430,441399,91527,126969,117490,973592,380091), (396778,233375,775046,351831,42901,848628,946191,943303), (412093,434215,709834,979534,462661,923822,397043,413069), (279606,827601,821133,815026,552913,236356,74574,9106), (840695,284468,109257,229797,601562,439089,133789,451105)]
 
Ciphertext-Row [8] contains 16 polynomials:
[(899866,154005,653123,780515,551798,93295,655558,440158), (995042,986415,200292,17688,154061,269401,985722,346393), (399713,50290,563670,220879,181227,170578,69848,468607), (442917,196944,871611,100354,890944,389480,774443,274249), (901724,205708,360228,464950,829645,161511,542334,69297), (394878,858552,687495,411234,89423,549807,142270,903328), (750774,666229,191074,245169,778738,73625,785465,972761), (61559,621102,988627,808016,51866,615947,960505,678659), (268647,389402,326,317307,974515,908557,319685,787794), (793561,543958,336737,542824,730321,522980,531508,895487), (863563,531682,428390,944393,339341,14703,598358,35231), (98724,439739,324219,717778,503121,891974,382349,72565), (917012,984662,269906,494911,717645,435431,717201,634824), (708982,595495,739170,139293,64706,101838,465443,391190), (605072,122030,412465,610431,541059,2733,681256,726740), (51888,261678,811627,797597,946612,918277,291327,72474)]
 
Ciphertext-Row [9] contains 16 polynomials:
[(334751,313366,762006,804257,373810,138609,626190,159339), (310392,815865,293398,911202,519365,847958,757252,985308), (466887,654588,712290,613657,833533,308393,415040,48676), (32463,809129,594329,840018,804803,365706,759264,462241), (200086,490691,954602,431414,447071,634050,393599,110849), (872505,755453,957636,145519,405318,838040,498460,450157), (397811,401070,835013,790301,242132,978187,971812,476325), (136345,432824,60984,559614,400030,593870,46528,151972), (261845,53995,575753,99068,427501,44942,672678,685752), (648987,130745,192597,984889,709447,44866,450666,37033), (686249,678507,848484,271914,853416,896579,432433,482337), (690350,517816,483249,939051,488636,77801,968085,460309), (20740,954887,539274,385828,91030,501497,347446,753064), (700793,629775,79270,232254,255766,984557,401398,554544), (596567,758804,599954,717557,172655,511170,519910,358645), (289299,817420,372633,507512,988966,10412,54738,433216)]
 
Ciphertext-Row [10] contains 16 polynomials:
[(548097,11023,414677,812377,160412,708586,128586,317321), (984689,551123,719958,935354,747956,272850,894399,118780), (893109,443135,139244,272178,847105,741099,773051,638550), (328441,631964,62341,728519,381260,920191,658303,148778), (198789,478130,718721,366932,692400,777921,55776,869024), (49672,212215,723776,448868,770764,738970,524464,678400), (810486,229279,144026,818575,981448,777075,753071,476594), (54303,355029,24628,293253,810413,637502,983944,821868), (186772,732180,901928,964234,149189,23261,947242,8414), (434482,128184,542223,907105,426149,99766,709837,903100), (45155,117424,609248,411863,121379,184724,497807,705504), (52282,39674,383596,136872,387126,14105,815966,557929), (630536,552941,679071,147710,98364,731341,71005,127733), (488326,987016,900520,987765,70957,67900,658965,705079), (543598,56633,594121,559316,414000,202138,91629,941894), (295930,690463,549406,742786,992621,297008,923414,870930)]
 
Ciphertext-Row [11] contains 16 polynomials:
[(645118,377111,893693,732501,235634,541675,741832,184496), (474728,112881,670752,866,346939,550310,122850,530061), (751819,298726,883747,670154,272621,388912,873013,309257), (439645,439521,462848,116386,741086,31725,735019,349979), (804850,511922,508150,641115,794734,498598,631262,310663), (595665,902346,45622,230819,271409,462044,386920,639090), (763991,834417,2421,295986,509835,249452,290201,455721), (728664,774223,641127,986214,424400,674402,485710,612738), (686722,123998,731560,677956,518888,927973,605965,295982), (759424,353527,233375,228966,369042,32373,341246,301436), (336588,420465,288721,61844,81924,690698,478301,588348), (50335,45107,872550,241856,973616,170853,301782,537564), (252765,718321,702459,358716,290323,82096,361024,993572), (106154,330236,551402,170373,155402,158956,264569,146066), (278283,690575,416135,119304,474075,804160,357345,486994), (244504,991307,599819,76114,744579,354259,151822,890501)]
 
Ciphertext-Row [12] contains 16 polynomials:
[(331752,35067,207858,354625,702341,265461,743668,892122), (814482,47,220627,452333,64063,43045,416543,900091), (70728,744670,873747,657710,501979,881605,627108,476830), (864341,495067,554392,227094,343510,597616,374424,295783), (842902,872099,175848,550758,269319,662189,569534,558712), (498506,644952,627657,232340,366705,781465,908602,127324), (101352,42485,136163,619909,851467,48824,310931,123020), (419990,122734,519222,688987,208462,758788,880820,269302), (468171,252673,170692,463503,487654,190311,922281,921019), (685233,757969,330172,261143,806689,369617,202919,908636), (571436,378508,483897,746789,297042,814332,395074,490323), (674900,392194,530704,607382,68060,933137,584904,98390), (687214,950645,197999,309629,667680,543424,851976,47874), (380603,52719,524684,616706,631796,295042,908119,640467), (400192,199877,898842,75378,420732,277711,507456,586432), (937123,631028,945930,367904,612742,242014,152170,411848)]
 
Ciphertext-Row [13] contains 16 polynomials:
[(32420,255306,806004,592772,865540,615727,520141,569711), (440118,415694,203615,101999,51441,879693,19408,233483), (258877,395365,304862,855551,365740,98937,89853,848122), (540129,676592,810533,703619,995469,525056,798624,687501), (281230,739645,565084,163784,334399,332226,137979,879535), (562676,788211,2986,674812,577074,324767,189143,994934), (271899,176882,811578,805997,320963,578186,171680,915632), (890087,196866,586191,694245,419788,182814,830431,607025), (44921,858118,827516,990512,26886,101970,525617,935248), (996388,921974,964250,334113,346936,503782,181727,960902), (778035,405721,535149,307729,718330,552993,495414,556463), (983442,533070,701232,36559,596002,288805,172932,142146), (552750,305314,413533,206931,506577,866154,585147,113438), (482331,335875,99997,745152,103413,984029,205954,288446), (848931,838667,696410,119958,847620,714503,148664,269156), (715257,103867,598526,736531,407192,86454,393375,162968)]
 
Ciphertext-Row [14] contains 16 polynomials:
[(448760,266788,84115,80808,378193,219407,121274,158948), (690073,474254,458413,641335,813957,623550,683860,588153), (195174,665908,922815,526849,925117,599747,884594,156890), (727381,104357,565013,62490,278246,690320,9366,860337), (105493,458937,12121,642809,700635,169439,223404,167472), (433706,143325,217415,636439,205779,849504,123692,591181), (554896,527183,322042,590226,768564,189462,438641,544689), (878040,223916,471712,68948,413794,323475,352467,761048), (588318,577827,353931,870976,684908,473616,541411,845595), (141294,421045,456970,119176,584608,266647,580416,555539), (893457,801523,910974,65082,575808,71744,569276,181827), (792394,701093,620111,956624,123035,128641,393517,286633), (751422,514780,469089,110423,939440,44657,159184,632152), (367218,425189,52758,162899,794450,394163,988845,360007), (874810,123063,973786,193056,681901,433933,644073,659286), (881550,459487,272156,683795,507021,258456,374088,13525)]
 
Ciphertext-Row [15] contains 16 polynomials:
[(968236,857302,805425,223131,445618,210327,785974,787252), (724135,745974,725391,257905,874745,493517,636903,668062), (235043,334680,268433,861344,201288,219330,954781,681619), (81708,651381,342970,922113,98906,310787,26421,128685), (329566,504411,73779,304273,182118,647396,423600,267666), (928700,67642,112761,867778,122882,34566,328621,812792), (253165,475912,75561,158889,314774,407629,623962,296069), (779726,386848,983148,334779,983576,106951,286069,187511), (786686,159837,295463,385861,360267,946552,280013,932165), (807196,479085,546255,557237,519338,548564,917124,425310), (308671,437013,211882,995688,735509,447289,518757,545598), (67362,656348,561983,749859,587391,440632,678887,139040), (3422,285740,952846,602929,901465,692330,413645,93437), (452458,645122,178014,613468,552804,94644,79958,154403), (502650,613617,820193,579567,359577,217174,947852,593882), (438304,267311,367815,687078,958914,623445,661474,888881)]

 
If you have any questions, feedback, suggestions: I welcome your feedback!.