Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions src/main/java/com/thealgorithms/ciphers/AESEncryption.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,24 @@
* Java. Please note that secret key and encrypted text is unreadable binary and
* hence in the following program we display it in hexadecimal format of the
* underlying bytes.
*
* <p>Runnable worked example ({@link #main(String[])}):
* <pre>{@code
* SecretKey secKey = AESEncryption.getSecretEncryptionKey(); // 128-bit (16-byte / 32-hex-char) key
* byte[] cipherText = AESEncryption.encryptText("Hello World", secKey); // 11B + 16B GCM tag = 27B (54 hex chars)
* String decrypted = AESEncryption.decryptText(cipherText, secKey); // uses aesCipher.getIV() -> "Hello World"
* }</pre>
*
* <p>Decisive state transition: {@link #encryptText(String, SecretKey)} initializes the static
* {@code aesCipher} with {@code AES/GCM/NoPadding} and a fresh random 12-byte GCM IV, and
* {@link #decryptText(byte[], SecretKey)} reads {@code aesCipher.getIV()} with a 128-bit
* authentication tag to verify and recover the plaintext. Running {@code main} prints:
* <pre>{@code
* Original Text:Hello World
* AES Key (Hex Form):<32 uppercase hex chars>
* Encrypted Text (Hex Form):<54 uppercase hex chars>
* Decryption successful. Decrypted text matches original: true
* }</pre>
*/
public final class AESEncryption {
private AESEncryption() {
Expand Down
128 changes: 128 additions & 0 deletions src/test/java/com/thealgorithms/ciphers/AESEncryptionExampleTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package com.thealgorithms.ciphers;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.io.ByteArrayOutputStream;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import javax.crypto.AEADBadTagException;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.junit.jupiter.api.Test;

/**
* Executable documentation and worked-example tests for {@link AESEncryption}.
*
* <p>Source preconditions ({@code AESEncryption.java}):
* <ul>
* <li>line 39: private constructor {@code AESEncryption()}</li>
* <li>line 42: {@code HEX_ARRAY} encodes each byte into 2 uppercase hex chars (lines 113-121)</li>
* <li>line 43: static {@code aesCipher} stores the GCM cipher instance initialized during
* {@code encryptText} (lines 87-88); {@code decryptText} reads {@code aesCipher.getIV()} with a
* 128-bit authentication tag (line 100)</li>
* <li>lines 69-73: {@code getSecretEncryptionKey()} generates a 128-bit (16-byte) AES key</li>
* <li>lines 85-90: {@code encryptText(String, SecretKey)} uses {@code AES/GCM/NoPadding},
* producing {@code plainText.getBytes().length + 16} bytes (ciphertext + 16-byte GCM tag)</li>
* <li>lines 50-60: {@code main(String[])} runs the worked example on {@code "Hello World"}</li>
* </ul>
*/
public class AESEncryptionExampleTest {

private static void logStep(String id, String input, Object expected, Object actual) {
System.out.println("[R45-T017] " + id + " | input=" + input
+ " | expected=" + expected + " | actual=" + actual);
}

/**
* Smallest valid inputs: empty string {@code ""} (0 bytes -> 16-byte GCM tag only, 32 hex
* chars) and single-character {@code "A"} (1 byte -> 17 bytes, 34 hex chars).
*/
@Test
public void smallestValidInputsTraceIntermediateSizesAndRoundTrip() throws Exception {
SecretKey key = AESEncryption.getSecretEncryptionKey();
assertEquals(16, key.getEncoded().length);
assertEquals(32, AESEncryption.bytesToHex(key.getEncoded()).length());

byte[] emptyCipher = AESEncryption.encryptText("", key);
logStep("smallest-empty-cipher-len", "plainText=\"\"", 16, emptyCipher.length);
assertEquals(16, emptyCipher.length);
assertEquals(32, AESEncryption.bytesToHex(emptyCipher).length());

String emptyRecovered = AESEncryption.decryptText(emptyCipher, key);
logStep("smallest-empty-decrypt", "cipherLen=16", "\"\"", "\"" + emptyRecovered + "\"");
assertEquals("", emptyRecovered);

byte[] singleCipher = AESEncryption.encryptText("A", key);
logStep("smallest-single-char-len", "plainText=\"A\"", 17, singleCipher.length);
assertEquals(17, singleCipher.length);
assertEquals(34, AESEncryption.bytesToHex(singleCipher).length());

String singleRecovered = AESEncryption.decryptText(singleCipher, key);
logStep("smallest-single-char-decrypt", "cipherLen=17", "A", singleRecovered);
assertEquals("A", singleRecovered);
}

/**
* Ordinary input ({@code "Hello World"}, 11 bytes = {@code 48656C6C6F20576F726C64} in hex):
* traces fixed 128-bit key -> 27-byte ciphertext+tag (54 hex chars) -> decisive
* {@code aesCipher.getIV()} state transition -> recovered {@code "Hello World"}.
*/
@Test
public void ordinaryWorkedExampleTracesIntermediateStatesAndDecisiveIvState() throws Exception {
byte[] rawFixedKey = new byte[16];
SecretKey fixedKey = new SecretKeySpec(rawFixedKey, "AES");
String keyHex = AESEncryption.bytesToHex(fixedKey.getEncoded());
logStep("ordinary-key-hex", "16 zero bytes", "00000000000000000000000000000000", keyHex);
assertEquals("00000000000000000000000000000000", keyHex);

String plainText = "Hello World";
String plainHex = AESEncryption.bytesToHex(plainText.getBytes(StandardCharsets.UTF_8));
logStep("ordinary-plaintext-hex", "\"Hello World\"", "48656C6C6F20576F726C64", plainHex);
assertEquals("48656C6C6F20576F726C64", plainHex);

byte[] firstCipher = AESEncryption.encryptText(plainText, fixedKey);
logStep("ordinary-cipher-len", "\"Hello World\" (11B + 16B tag)", 27, firstCipher.length);
assertEquals(27, firstCipher.length);
assertEquals(54, AESEncryption.bytesToHex(firstCipher).length());

String recovered = AESEncryption.decryptText(firstCipher, fixedKey);
logStep("ordinary-decrypt", "firstCipher (27B)", "Hello World", recovered);
assertEquals("Hello World", recovered);

// Decisive state transition: a second encryptText call overwrites static aesCipher with a new
// random GCM IV (lines 87-88), so decrypting firstCipher against the overwritten IV fails.
byte[] secondCipher = AESEncryption.encryptText(plainText, fixedKey);
assertNotEquals(AESEncryption.bytesToHex(firstCipher), AESEncryption.bytesToHex(secondCipher));
assertThrows(AEADBadTagException.class, () -> AESEncryption.decryptText(firstCipher, fixedKey));
assertEquals("Hello World", AESEncryption.decryptText(secondCipher, fixedKey));
}

/**
* Executes the documented {@link AESEncryption#main(String[])} runnable example and asserts
* its displayed console output format and final verification line.
*/
@Test
public void runnableDocumentationExamplePrintsExpectedOutput() throws Exception {
PrintStream originalOut = System.out;
ByteArrayOutputStream captured = new ByteArrayOutputStream();
try {
System.setOut(new PrintStream(captured, true, StandardCharsets.UTF_8));
AESEncryption.main(new String[0]);
} finally {
System.setOut(originalOut);
}

String[] lines = captured.toString(StandardCharsets.UTF_8).trim().split("\\R");
logStep("main-output-lines", "AESEncryption.main()", 4, lines.length);
assertEquals(4, lines.length);
assertEquals("Original Text:Hello World", lines[0]);
assertTrue(lines[1].matches("^AES Key \\(Hex Form\\):[0-9A-F]{32}$"),
"Unexpected key line: " + lines[1]);
assertTrue(lines[2].matches("^Encrypted Text \\(Hex Form\\):[0-9A-F]{54}$"),
"Unexpected cipher line: " + lines[2]);
assertEquals("Decryption successful. Decrypted text matches original: true", lines[3]);
}
}
70 changes: 70 additions & 0 deletions work/R45-T017/check.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env python3
"""Runner for task R45-T017: runnable example checks for AESEncryption.

Runs the focused JUnit test through Maven, requires a nonzero test count,
saves inputs, expected versus actual results, and tool versions into work/R45-T017/,
and exits non-zero on any failure.
"""
import glob
import subprocess
import sys
import xml.etree.ElementTree as ET
from pathlib import Path

HERE = Path(__file__).resolve().parent
REPO = HERE.parent.parent
TEST_CLASS = "AESEncryptionExampleTest"
CMD = ["mvn", "-B", "test", f"-Dtest={TEST_CLASS}", "-Dsurefire.failIfNoSpecifiedTests=false"]


def tool_version(cmd):
try:
out = subprocess.run(cmd, capture_output=True, text=True, cwd=REPO)
return "\n".join(l for l in (out.stdout + out.stderr).splitlines()
if not l.startswith("Picked up")).strip()
except FileNotFoundError:
return f"{cmd[0]}: not found"


def main():
(HERE / "tool_versions.txt").write_text(
"java:\n" + tool_version(["java", "-version"]) + "\n\nmvn:\n"
+ tool_version(["mvn", "-v"]) + "\n\npython:\n" + sys.version + "\n")

print("Running:", " ".join(CMD))
proc = subprocess.run(CMD, capture_output=True, text=True, cwd=REPO)
combined_output = proc.stdout + proc.stderr
(HERE / "mvn_output.txt").write_text(combined_output)

records = [line.strip() for line in combined_output.splitlines()
if line.strip().startswith("[R45-T017]")]
(HERE / "comparisons.txt").write_text(
("\n".join(records) + "\n") if records else "No example records captured.\n")

reports = glob.glob(str(REPO / "target/surefire-reports" / f"TEST-*{TEST_CLASS}.xml"))
if not reports:
(HERE / "result.txt").write_text(
f"command: {' '.join(CMD)}\nFAIL: no surefire report found\n")
print("FAIL: no surefire report found")
return 1

suite = ET.parse(reports[0]).getroot()
tests = int(suite.get("tests", 0))
failures = int(suite.get("failures", 0)) + int(suite.get("errors", 0))
skipped = int(suite.get("skipped", 0))
passed = proc.returncode == 0 and failures == 0 and tests > 0 and len(records) > 0
status = "PASS" if passed else "FAIL"
summary = (
f"command: {' '.join(CMD)}\n"
f"tests={tests} failures/errors={failures} skipped={skipped} "
f"records={len(records)} mvn_exit={proc.returncode}\n"
f"status={status}\n"
)
(HERE / "result.txt").write_text(summary)
print(summary)

return 0 if passed else 1


if __name__ == "__main__":
sys.exit(main())
9 changes: 9 additions & 0 deletions work/R45-T017/comparisons.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
[R45-T017] main-output-lines | input=AESEncryption.main() | expected=4 | actual=4
[R45-T017] smallest-empty-cipher-len | input=plainText="" | expected=16 | actual=16
[R45-T017] smallest-empty-decrypt | input=cipherLen=16 | expected="" | actual=""
[R45-T017] smallest-single-char-len | input=plainText="A" | expected=17 | actual=17
[R45-T017] smallest-single-char-decrypt | input=cipherLen=17 | expected=A | actual=A
[R45-T017] ordinary-key-hex | input=16 zero bytes | expected=00000000000000000000000000000000 | actual=00000000000000000000000000000000
[R45-T017] ordinary-plaintext-hex | input="Hello World" | expected=48656C6C6F20576F726C64 | actual=48656C6C6F20576F726C64
[R45-T017] ordinary-cipher-len | input="Hello World" (11B + 16B tag) | expected=27 | actual=27
[R45-T017] ordinary-decrypt | input=firstCipher (27B) | expected=Hello World | actual=Hello World
58 changes: 58 additions & 0 deletions work/R45-T017/mvn_output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
[INFO] Scanning for projects...
[INFO] Loaded 24447 auto-discovered prefixes for remote repository central (prefixes-central.txt)
[INFO]
[INFO] -----------------------< com.thealgorithms:Java >-----------------------
[INFO] Building Java 1.0-SNAPSHOT
[INFO] from pom.xml
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- jacoco:0.8.16:prepare-agent (default) @ Java ---
[INFO] Loaded 64 auto-discovered prefixes for remote repository apache.snapshots (prefixes-apache.snapshots.txt)
[INFO] argLine set to -javaagent:/Users/shyamlibakale/.m2/repository/org/jacoco/org.jacoco.agent/0.8.16/org.jacoco.agent-0.8.16-runtime.jar=destfile=/Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO]
[INFO] --- resources:3.5.0:resources (default-resources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/main/resources
[INFO]
[INFO] --- compiler:3.16.0:compile (default-compile) @ Java ---
[INFO] Recompiling the module because of changed source code.
[INFO] Compiling 848 source files with javac [debug release 21] to target/classes
[INFO]
[INFO] --- resources:3.5.0:testResources (default-testResources) @ Java ---
[INFO] skip non existing resourceDirectory /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/src/test/resources
[INFO]
[INFO] --- compiler:3.16.0:testCompile (default-testCompile) @ Java ---
[INFO] Recompiling the module because of changed dependency.
[INFO] Compiling 797 source files with javac [debug release 21] to target/test-classes
[INFO]
[INFO] --- surefire:3.6.0:test (default-test) @ Java ---
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
[INFO]
[INFO] -------------------------------------------------------
[INFO] T E S T S
[INFO] -------------------------------------------------------
[INFO] Running com.thealgorithms.ciphers.AESEncryptionExampleTest
[R45-T017] main-output-lines | input=AESEncryption.main() | expected=4 | actual=4
[R45-T017] smallest-empty-cipher-len | input=plainText="" | expected=16 | actual=16
[R45-T017] smallest-empty-decrypt | input=cipherLen=16 | expected="" | actual=""
[R45-T017] smallest-single-char-len | input=plainText="A" | expected=17 | actual=17
[R45-T017] smallest-single-char-decrypt | input=cipherLen=17 | expected=A | actual=A
[R45-T017] ordinary-key-hex | input=16 zero bytes | expected=00000000000000000000000000000000 | actual=00000000000000000000000000000000
[R45-T017] ordinary-plaintext-hex | input="Hello World" | expected=48656C6C6F20576F726C64 | actual=48656C6C6F20576F726C64
[R45-T017] ordinary-cipher-len | input="Hello World" (11B + 16B tag) | expected=27 | actual=27
[R45-T017] ordinary-decrypt | input=firstCipher (27B) | expected=Hello World | actual=Hello World
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.053 s -- in com.thealgorithms.ciphers.AESEncryptionExampleTest
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
[INFO]
[INFO]
[INFO] --- jacoco:0.8.16:report (generate-code-coverage-report) @ Java ---
[INFO] Loading execution data file /Users/shyamlibakale/TheAlgorithms-Java/TheAlgorithms-Java/target/jacoco.exec
[INFO] Analyzed bundle 'Java' with 1073 classes
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 6.887 s
[INFO] Finished at: 2026-11-11T17:48:25+05:30
[INFO] ------------------------------------------------------------------------
3 changes: 3 additions & 0 deletions work/R45-T017/result.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
command: mvn -B test -Dtest=AESEncryptionExampleTest -Dsurefire.failIfNoSpecifiedTests=false
tests=3 failures/errors=0 skipped=0 records=9 mvn_exit=0
status=PASS
14 changes: 14 additions & 0 deletions work/R45-T017/tool_versions.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
java:
openjdk version "21.0.13" 2026-10-20 LTS
OpenJDK Runtime Environment Temurin-21.0.13+9 (build 21.0.13+9-LTS)
OpenJDK 64-Bit Server VM Temurin-21.0.13+9 (build 21.0.13+9-LTS, mixed mode, sharing)

mvn:
Apache Maven 3.10.0 (c43a36b8d67be7e0805a411bc0898af1a51f5472)
Maven home: /opt/homebrew/Cellar/maven/3.10.0/libexec
Java version: 27.0.1.0.3, vendor: Homebrew, runtime: /opt/homebrew/Cellar/openjdk/27.0.1.0.3/libexec/openjdk.jdk/Contents/Home
Default locale: en_IN, platform encoding: UTF-8, time zone: Asia/Kolkata
OS name: "mac os x", version: "27.0", arch: "aarch64", family: "mac"

python:
3.14.7 (v3.14.7:823f0323ee6, Aug 5 2026, 07:07:01) [Clang 21.0.0 (clang-2100.1.1.101)]