Split integrations and export into ext module

feat: move integrations from lib to ext
feat: move content export from lib to ext
feat: rename affected packages for separate module distribution
chore: update Gradle module wiring
chore: adjust JPMS descriptors and dependencies
docs: update module structure documentation
This commit is contained in:
2026-04-01 20:43:10 +02:00
parent 354e9dd9bc
commit d1bdf7d9df
30 changed files with 233 additions and 49 deletions

View File

@@ -13,13 +13,6 @@
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/resources">
<attributes>
<attribute name="gradle_scope" value="test"/>
<attribute name="gradle_used_by_scope" value="test"/>
<attribute name="test" value="true"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>

View File

@@ -8,7 +8,6 @@ group='org.egothor'
dependencies {
api 'org.bouncycastle:bcpkix-jdk18on'
implementation 'org.egothor:conflux'
implementation 'org.apache.commons:commons-imaging'
}

View File

@@ -1,197 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.content.export;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
import java.util.Base64.Encoder;
/**
* A streaming {@link InputStream} that encodes binary input data into Base64
* format with optional line prefixes and suffixes for each encoded line.
*
* <p>
* This class is designed for script-friendly output formatting, such as
* generating platform-specific batch or shell script lines where each line
* might begin with a command (e.g., {@code echo }) and end with a
* platform-specific line terminator (e.g., {@code \n} for UNIX-like systems or
* {@code \r\n} for Windows).
* </p>
*
* <p>
* It supports chunked streaming and avoids holding the entire Base64-encoded
* content in memory, making it suitable for large binary inputs. Lines are
* split according to the specified maximum line length and are composed as
* follows:
* </p>
*
* <pre>
* [prefix][base64-encoded data][suffix]
* </pre>
*
* <p>
* If a suffix is defined, the actual Base64 data per line will be truncated to
* {@code lineLength - suffix.length} characters to ensure total line length
* does not exceed {@code lineLength}.
* </p>
*
* <p>
* This stream does not automatically insert newlines between lines unless
* explicitly provided via the {@code suffix} argument (e.g.,
* {@code "\n".getBytes()}).
* </p>
*
* <p>
* <strong>Usage example:</strong>
* </p>
*
* <pre>{@code
* InputStream source = new FileInputStream("input.bin");
* InputStream encoded = new Base64Stream(
* source,
* "echo ".getBytes(StandardCharsets.UTF_8),
* 76,
* "\n".getBytes(StandardCharsets.UTF_8)
* );
* encoded.transferTo(System.out);
* }</pre>
*
* @author Leo Galambos
*/
public class Base64Stream extends InputStream {
private final InputStream source;
private InputStream in;
private InputStream prefix;
private InputStream suffix;
private final Encoder base64;
private int pos;
private final int lineLength;
private int lineBreak;
private InputStream is = nullInputStream(); // NOPMD
private int breakPos; // NOPMD
private boolean closed;
private final static int TRIPLES_INPUT = 1000;
/**
* Constructs a new {@code Base64Stream}.
*
* @param source the raw binary input stream to be Base64 encoded
* @param linePrefix optional prefix to prepend at the beginning of each line
* (e.g., {@code "echo "}); can be {@code null} for no prefix
* @param lineLength the total maximum length of each output line, including any
* prefix and suffix
* @param lineSuffix optional suffix to append at the end of each line (e.g.,
* newline); can be {@code null}
*/
public Base64Stream(InputStream source, byte[] linePrefix, int lineLength, byte[] lineSuffix) {
super();
this.source = source;
this.lineLength = lineLength;
in = nullInputStream();
base64 = Base64.getEncoder();
if (linePrefix != null) {
prefix = new ByteArrayInputStream(linePrefix);
}
if (lineSuffix != null) {
suffix = new ByteArrayInputStream(lineSuffix);
}
lineBreak = lineLength - ((lineSuffix == null) ? 0 : lineSuffix.length);
}
@Override
public int read() throws IOException {
byte[] result = { 0 };
int count = read(result, 0, 1);
return (count == 0) ? -1 : result[0] & 0xff;
}
@Override
public int read(byte[] b, int off, int len) throws IOException {
if (pos == lineLength) {
pos = 0;
}
if (pos == 0 && prefix != null) {
prefix.reset();
breakPos = Integer.MAX_VALUE;
is = prefix;
} else {
if (pos == lineBreak && suffix != null && !closed) {
suffix.reset();
breakPos = Integer.MAX_VALUE;
is = suffix;
} else {
if (in.available() == 0) {
ensureData();
}
breakPos = lineBreak;
is = in;
}
}
int l = Math.min(Math.min(is.available(), len), breakPos - pos);
pos += l;
return is.read(b, off, l);
}
/**
* Reads the next block of raw bytes from the source and encodes them into
* Base64. Handles stream exhaustion and final line suffix if applicable.
*/
private void ensureData() throws IOException {
byte[] buf = source.readNBytes(3 * TRIPLES_INPUT);
if (buf.length == 0 && suffix != null && !closed) {
System.out.println("suffix");
closed = true;
suffix.reset();
breakPos = lineBreak = Integer.MAX_VALUE;
in = suffix;
return;
}
if (buf.length == 3 * TRIPLES_INPUT) {
in = new ByteArrayInputStream(base64.withoutPadding().encode(buf));
} else {
in = new ByteArrayInputStream(base64.encode(buf));
}
}
}

View File

@@ -1,234 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.content.export;
import java.io.BufferedWriter;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.SequenceInputStream;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URI;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import zeroecho.sdk.content.api.AbstractExportableDataContent;
/**
* {@code PiwigoExportDataContent} is a specialized exportable content class
* that supports uploading an image file to a Piwigo photo gallery server,
* either directly via HTTP POST or by generating platform-specific scripts for
* deferred uploading.
*
* <p>
* Depending on the export mode (RAW, BASH_SCRIPT, CMD_SCRIPT), it can:
* <ul>
* <li>Upload the image to a Piwigo server directly using HTTP
* multipart/form-data</li>
* <li>Generate a Bash script that decodes the base64-encoded image and uploads
* it using curl</li>
* <li>Generate a CMD (Windows batch) script that reconstructs the image using
* certutil and uploads it</li>
* </ul>
*
* <p>
* This class integrates with {@code AbstractExportableDataContent} and expects
* its {@code input} field to be set before invoking {@link #getStream()}.
* </p>
*
* <p>
* The image is uploaded using the {@code pwg.images.add} method of the Piwigo
* API.
* </p>
*/
class PiwigoExportDataContent extends AbstractExportableDataContent {
private final String imageFileName;
private final String piwigoUrl;
private final String username;
private final String password;
private final String albumId;
/**
* Constructs a new exportable Piwigo upload object.
*
* @param imageFileName the name of the image file to assign during upload or
* script output
* @param piwigoUrl the URL of the Piwigo API endpoint
* @param username the Piwigo username for authentication
* @param password the Piwigo password
* @param albumId the ID of the Piwigo album to which the image will be
* uploaded
*/
public PiwigoExportDataContent(String imageFileName, String piwigoUrl, String username, String password,
String albumId) {
super();
this.imageFileName = imageFileName;
this.piwigoUrl = piwigoUrl;
this.username = username;
this.password = password;
this.albumId = albumId;
}
/**
* Returns an {@code InputStream} that provides either the raw upload stream, or
* a platform-specific script depending on the export mode.
*
* @return the resulting {@code InputStream}
* @throws IOException if reading the input or creating the stream fails
*/
@Override
public InputStream getStream() throws IOException {
if (input == null) {
throw new IllegalStateException("Input not set.");
}
return switch (mode) {
case RAW -> performDirectUpload(input.getStream());
case BASH_SCRIPT -> generateBashScript(input.getStream());
case CMD_SCRIPT -> generateCmdScript(input.getStream());
};
}
/**
* Performs a direct upload of the image data to the Piwigo server using a
* multipart/form-data HTTP POST request.
*
* @param dataStream the input stream of the binary image data
* @return the server's response stream
* @throws IOException if the upload fails
*/
private InputStream performDirectUpload(InputStream dataStream) throws IOException {
String boundary = "----Boundary" + System.currentTimeMillis();
URL url = URI.create(piwigoUrl).toURL();
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
conn.setDoOutput(true);
conn.setRequestMethod("POST");
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
try (OutputStream out = conn.getOutputStream();
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
writeFormField(writer, boundary, "method", "pwg.images.add");
writeFormField(writer, boundary, "username", username);
writeFormField(writer, boundary, "password", password);
writeFormField(writer, boundary, "category", albumId);
writer.write("--" + boundary + "\r\n");
writer.write("Content-Disposition: form-data; name=\"image\"; filename=\"" + imageFileName + "\"\r\n");
writer.write("Content-Type: image/jpeg\r\n\r\n");
writer.flush();
dataStream.transferTo(out);
out.flush();
writer.write("\r\n--" + boundary + "--\r\n");
writer.flush();
}
InputStream responseStream;
try {
responseStream = conn.getInputStream();
} catch (IOException e) {
InputStream errorStream = conn.getErrorStream();
if (errorStream != null) {
return errorStream;
}
return new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
}
return responseStream;
}
/**
* Writes a single form field as part of a multipart/form-data HTTP request.
*
* @param writer the writer to output the field to
* @param boundary the multipart boundary
* @param name the name of the form field
* @param value the value of the form field
* @throws IOException if writing fails
*/
private void writeFormField(Writer writer, String boundary, String name, String value) throws IOException {
writer.write("--" + boundary + "\r\n");
writer.write("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
writer.write(value + "\r\n");
}
/**
* Generates a Bash script that reconstructs the image using a Base64 heredoc
* block and uploads it using {@code curl}.
*
* @param originalStream the original binary stream of the image
* @return a stream containing the complete shell script
*/
private InputStream generateBashScript(InputStream originalStream) {
InputStream header = new ByteArrayInputStream(("#!/bin/bash\nset -e\n\ncurl -X POST \"" + piwigoUrl + "\" \\\n"
+ " -F method=\"pwg.images.add\" \\\n" + " -F username=\"" + username + "\" \\\n" + " -F password=\""
+ password + "\" \\\n" + " -F category=\"" + albumId + "\" \\\n" + " -F image=@<(base64 -d <<'EOF'\n")
.getBytes(StandardCharsets.UTF_8));
@SuppressWarnings("resource")
InputStream body = new Base64Stream(originalStream, null, 76, new byte[] { 10 });
InputStream footer = new ByteArrayInputStream("EOF\n)\n".getBytes(StandardCharsets.UTF_8));
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
}
/**
* Generates a CMD batch script that reconstructs the image using certutil and
* uploads it using {@code curl}.
*
* @param originalStream the original binary stream of the image
* @return a stream containing the complete Windows batch script
*/
private InputStream generateCmdScript(InputStream originalStream) {
InputStream header = new ByteArrayInputStream(
"@echo off\nsetlocal\necho -----BEGIN BASE64----- > tmp.b64\n".getBytes(StandardCharsets.UTF_8));
@SuppressWarnings("resource")
InputStream body = new Base64Stream(originalStream, "echo ".getBytes(), 76, " >> tmp.b64\r\n".getBytes());
InputStream footer = new ByteArrayInputStream(
("echo -----END BASE64----- >> tmp.b64\n" + "certutil -decode tmp.b64 \"" + imageFileName + "\" >nul\n"
+ "del tmp.b64\n" + "curl -X POST \"" + piwigoUrl + "\" ^\n" + " -F method=pwg.images.add ^\n"
+ " -F username=" + username + " ^\n" + " -F password=" + password + " ^\n" + " -F category="
+ albumId + " ^\n" + " -F image=@" + imageFileName + "\n" + "del \"" + imageFileName + "\"\n")
.getBytes(StandardCharsets.UTF_8));
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
}
}

View File

@@ -1,110 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Export helpers and platform deployers for SDK content.
*
* <p>
* This package provides streaming utilities and exportable content
* implementations that render {@link zeroecho.sdk.content.api.DataContent} for
* deployment to external platforms or for script-based transport. Exports can
* be produced as raw bytes or as platform-specific scripts according to
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.
* </p>
*
* <h2>Scope</h2>
* <ul>
* <li>Streaming utilities that reformat content for export-friendly forms (for
* example, Base64 with line control).</li>
* <li>Exportable content that uploads directly to a remote service or generates
* scripts for deferred execution.</li>
* <li>Foundations for additional deployers, including steganographic carriers
* and other public platforms beyond Piwigo.</li>
* </ul>
*
* <h2>Key elements</h2>
* <ul>
* <li>{@link Base64Stream} - an {@link java.io.InputStream} that encodes a
* binary stream to Base64 with optional per-line prefix/suffix and configurable
* line length (useful for script emitters).</li>
* <li><i>Piwigo uploader</i> - an exportable content implementation
* (package-private) that can either upload an image directly to a Piwigo server
* or generate Bash/CMD scripts that reconstruct and upload the image. It is
* built on {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and
* honors
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.</li>
* </ul>
*
* <h2>Typical usage</h2>
* <h3>Format a stream as Base64 command lines</h3> <pre>{@code
* java.io.InputStream source = ... raw bytes ...;
* java.io.InputStream encoded = new zeroecho.sdk.content.export.Base64Stream(
* source,
* "echo ".getBytes(java.nio.charset.StandardCharsets.UTF_8),
* 76,
* "\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)
* );
* encoded.transferTo(System.out);
* }</pre>
*
* <h3>Render an exportable content in a chosen mode</h3> <pre>{@code
* zeroecho.sdk.content.api.ExportableDataContent content = ... some exportable content ...;
* content.setExportMode(zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
* try (java.io.InputStream script = content.getStream()) {
* script.transferTo(out);
* }
* }</pre>
*
* <h2>Security notes</h2>
* <ul>
* <li>Prefer exporting {@link zeroecho.sdk.content.api.EncryptedContent} when
* targeting untrusted destinations.</li>
* <li>Avoid embedding secrets in scripts; pass credentials via environment
* variables or secure stores when possible.</li>
* <li>When adding steganographic exporters, document cover formats and
* concealment limits clearly.</li>
* </ul>
*
* <h2>Extensibility</h2>
* <ul>
* <li>New deployers should extend
* {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and select a
* default {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}
* appropriate for the platform.</li>
* <li>Utilities like {@link Base64Stream} can be reused to generate
* platform-friendly payloads without buffering whole files.</li>
* </ul>
*
* @since 1.0
*/
package zeroecho.sdk.content.export;

View File

@@ -54,10 +54,6 @@
* {@link zeroecho.sdk.content.builtin.PlainString},
* {@link zeroecho.sdk.content.builtin.PlainFile}, and
* {@link zeroecho.sdk.content.builtin.SecretPassword}.</li>
* <li>{@link zeroecho.sdk.content.export} - export helpers and platform
* deployers, such as {@link zeroecho.sdk.content.export.Base64Stream} and
* exportable content that targets external destinations (for example, gallery
* platforms) or script-based transports.</li>
* </ul>
*
* <h2>Responsibilities</h2>

View File

@@ -1,177 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.covert;
import java.util.ArrayDeque;
import java.util.Map;
import java.util.NavigableMap;
import java.util.Queue;
import java.util.TreeMap;
import zeroecho.sdk.util.RandomSupport;
/**
* A utility class for generating pseudo-random textual content based on
* predefined character frequency distributions.
* <p>
* The {@code TextualCodec} class contains a nested {@link Generator} class that
* can be configured with a set of character frequencies (e.g., English letter
* frequencies) to produce text that mimics the statistical distribution of the
* given language or character set.
*/
public final class TextualCodec { // NOPMD
/**
* Private constructor to prevent instantiation of {@code TextualCodec}.
* <p>
* This class is intended to be used as a utility container for static members
* and should not be instantiated.
*/
private TextualCodec() {
}
/**
* Generates characters or strings using a frequency-based distribution.
* <p>
* This generator uses a cumulative frequency table internally to map random
* numbers to characters, enabling the creation of realistic-looking text that
* follows the given character frequency distribution. The generator also avoids
* consecutive duplicate characters by employing a simple queuing mechanism.
*/
public static class Generator {
/**
* Internal map representing the cumulative frequency ranges mapped to
* characters.
*/
private final NavigableMap<Double, Character> ranges = new TreeMap<>();
/**
* The maximum value of the cumulative frequency range.
*/
private final double maxRange;
/**
* A predefined English character frequency distribution including the space
* character, based on typical usage in English text.
*/
public final static Map<Character, Double> ENGLISH = Map.ofEntries(Map.entry('a', 8.2), Map.entry('b', 1.5),
Map.entry('c', 2.8), Map.entry('d', 4.3), Map.entry('e', 12.7), Map.entry('f', 2.2),
Map.entry('g', 2.0), Map.entry('h', 6.1), Map.entry('i', 7.0), Map.entry('j', 0.15),
Map.entry('k', 0.77), Map.entry('l', 4.0), Map.entry('m', 2.4), Map.entry('n', 6.7),
Map.entry('o', 7.5), Map.entry('p', 1.9), Map.entry('q', 0.095), Map.entry('r', 6.0),
Map.entry('s', 6.3), Map.entry('t', 9.1), Map.entry('u', 2.8), Map.entry('v', 0.98),
Map.entry('w', 2.4), Map.entry('x', 0.15), Map.entry('y', 2.0), Map.entry('z', 0.074),
Map.entry(' ', 25.4));
/**
* A default generator using the {@link #ENGLISH} frequency distribution.
*/
public final static Generator EN = new Generator(ENGLISH);
private Character lastChar = '~';
private final Queue<Character> backlog = new ArrayDeque<>();
/**
* Constructs a new {@code Generator} with the specified character frequency
* distribution.
*
* @param frequencies a map of characters to their relative frequencies (must be
* non-negative)
*/
public Generator(Map<Character, Double> frequencies) {
double cumulative = 0.0;
for (Map.Entry<Character, Double> entry : frequencies.entrySet()) {
double freq = entry.getValue();
if (freq <= 0) {
continue;
}
ranges.put(cumulative, entry.getKey());
cumulative = cumulative + freq;
}
maxRange = cumulative;
}
/**
* Generates a string of the specified length using the configured character
* frequency distribution. Consecutive duplicate characters are avoided when
* possible.
*
* @param length the number of characters to generate
* @return a randomly generated string
*/
public String getText(int length) {
StringBuffer sb = new StringBuffer();
while (length-- > 0) { // NOPMD
sb.append(getChar());
}
return sb.toString();
}
/**
* Returns the next randomly generated character, avoiding consecutive
* duplicates when possible.
*
* @return the next character in the generated sequence
*/
public char getChar() {
if (backlog.isEmpty() || lastChar.equals(backlog.peek())) {
Character next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
while (lastChar.equals(next)) {
backlog.add(next);
next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
}
lastChar = next;
return lastChar;
}
return lastChar = backlog.poll();
}
/**
* Returns a character based on the provided value in the frequency range.
*
* @param value a value between 0 (inclusive) and {@code maxRange} (exclusive)
* @return the corresponding character for the specified value
* @throws IllegalArgumentException if the value is out of range
*/
public char getChar(double value) {
if (value < 0.0 || value >= maxRange) {
throw new IllegalArgumentException("Value must be in [0.0, " + maxRange + ")");
}
Map.Entry<Double, Character> entry = ranges.floorEntry(value);
return (entry == null) ? ranges.firstEntry().getValue() : entry.getValue();
}
}
}

View File

@@ -1,227 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.covert.jpeg;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteOrder;
import java.nio.charset.StandardCharsets;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Base64;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.imaging.Imaging;
import org.apache.commons.imaging.ImagingException;
import org.apache.commons.imaging.common.ImageMetadata;
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
import org.apache.commons.imaging.formats.tiff.TiffField;
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
import zeroecho.core.io.Util;
import zeroecho.sdk.util.Pack7LStreamWriter;
/**
* Utility class for embedding and extracting binary payloads across multiple
* EXIF slots in JPEG files.
* <p>
* The configured slot list defines both the location and chunking order of the
* embedded payload. Slot types are respected — for example, ASCII fields
* automatically use Base64 encoding to ensure compatibility with textual EXIF
* constraints.
* </p>
* <p>
* The original JPEG EXIF metadata is preserved, except for any overwritten
* fields defined via slot configuration.
* </p>
*/
public final class JpegExifEmbedder {
private static final Logger LOG = Logger.getLogger(JpegExifEmbedder.class.getName());
private final List<Slot> slots = new ArrayList<>();
/**
* Configures the list of EXIF slots to be used for payload embedding and
* extraction. The order determines the splitting/joining order of the payload.
*
* @param slots List of slot configurations
*/
public void setSlots(List<Slot> slots) {
this.slots.clear();
this.slots.addAll(slots);
}
/**
* Embeds a binary payload into a JPEG file by spreading the data across a
* series of pre-configured EXIF slots.
* <p>
* The payload is read from the provided {@code InputStream}, prefixed using
* 7-bit variable-length encoding, and split across EXIF fields based on their
* capacity and type. Fields that accept only text data will store the payload
* encoded as Base64. The modified JPEG is then written to the given output
* stream using a lossless EXIF update.
* </p>
*
* @param jpegPath the path to the input JPEG file; must not be {@code null}
* @param payloadInput the input stream containing the binary payload to embed;
* it must be prefixed using 7-bit length encoding (e.g.
* {@code writePack7L})
* @param jpegOutput the output stream where the modified JPEG will be written
* @return the total number of bytes read from {@code payloadInput}, including
* the prefix
* @throws IOException if an I/O error occurs while reading the payload,
* processing EXIF metadata, or writing the output JPEG
*/
public int embed(Path jpegPath, InputStream payloadInput, OutputStream jpegOutput) throws IOException {
try {
ImageMetadata metadata = Imaging.getMetadata(jpegPath.toFile());
TiffImageMetadata exif = (metadata instanceof JpegImageMetadata jmeta) ? jmeta.getExif() : null;
TiffOutputSet outputSet = (exif != null) ? exif.getOutputSet() : new TiffOutputSet(ByteOrder.BIG_ENDIAN);
byte[] allBytes = Util.readWithPackedLengthPrefix(payloadInput, 1024 * 1024);
int offset = 0;
for (Slot slot : slots) {
boolean useBase64 = slot.tagInfo.isText();
int chunkLimit = slot.defaultCapacity;
byte[] encoded;
if (useBase64) {
// base64 expands ~33%, so reduce raw chunk size
int safeRawSize = chunkLimit * 3 / 4;
int size = Math.min(safeRawSize, allBytes.length - offset);
byte[] raw = new byte[size]; // NOPMD
System.arraycopy(allBytes, offset, raw, 0, size);
offset += size;
byte[] base64Bytes = Base64.getEncoder().encode(raw);
String base64String = new String(base64Bytes, StandardCharsets.US_ASCII); // NOPMD
encoded = slot.tagInfo.encodeValue(slot.tagInfo.dataTypes.get(0), base64String,
outputSet.byteOrder);
} else {
int size = Math.min(chunkLimit, allBytes.length - offset);
byte[] chunk = new byte[size]; // NOPMD
System.arraycopy(allBytes, offset, chunk, 0, size);
offset += size;
encoded = slot.tagInfo.encodeValue(slot.tagInfo.dataTypes.get(0), chunk, outputSet.byteOrder);
}
int directoryType = slot.tagInfo.directoryType.directoryType;
TiffOutputDirectory directory = outputSet.findDirectory(directoryType);
if (directory == null) {
directory = new TiffOutputDirectory(directoryType, outputSet.byteOrder); // NOPMD
outputSet.addDirectory(directory);
}
TiffOutputField field = new TiffOutputField(slot.tagInfo, slot.tagInfo.dataTypes.get(0), encoded.length, // NOPMD
encoded);
directory.removeField(slot.tagInfo); // Remove only if collides
directory.add(field);
if (offset >= allBytes.length) {
break;
}
}
new ExifRewriter().updateExifMetadataLossless(jpegPath.toFile(), jpegOutput, outputSet);
return allBytes.length;
} catch (ImagingException e) {
throw new IOException("Failed to process EXIF data", e);
}
}
/**
* Extracts a binary payload that was previously embedded into the JPEG via EXIF
* slots.
*
* @param jpegPath input JPEG path
* @param payloadOutput stream to write the reconstructed binary payload
* @throws IOException if the extraction fails
*/
public void extract(Path jpegPath, OutputStream payloadOutput) throws IOException {
try {
ImageMetadata metadata = Imaging.getMetadata(jpegPath.toFile());
TiffImageMetadata exif = (metadata instanceof JpegImageMetadata jmeta) ? jmeta.getExif() : null;
if (exif == null) {
LOG.warning("EXIF metadata not found in image.");
return;
}
Pack7LStreamWriter output = new Pack7LStreamWriter(payloadOutput);
for (Slot slot : slots) {
TiffField field = exif.findField(slot.tagInfo);
if (field == null) {
continue;
}
Object value = slot.tagInfo.getValue(field);
byte[] chunk;
if (value instanceof byte[] binary) {
chunk = slot.tagInfo.isText() ? Base64.getDecoder().decode(binary) : binary;
} else if (value instanceof String str) {
chunk = slot.tagInfo.isText() ? Base64.getDecoder().decode(str.getBytes(StandardCharsets.US_ASCII))
: str.getBytes(StandardCharsets.UTF_8);
} else {
LOG.log(Level.WARNING, "Unsupported EXIF field value type for tag: {0}", slot.tagInfo.name);
continue;
}
if (chunk.length > output.write(chunk)) {
// all data has been read
break;
}
}
} catch (ImagingException e) {
throw new IOException("Failed to read EXIF from JPEG", e);
}
}
}

View File

@@ -1,302 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.covert.jpeg;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii;
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined;
/**
* Represents a target slot in an EXIF/TIFF metadata directory for covert data
* embedding.
* <p>
* A {@code Slot} defines where and how binary payloads may be hidden within the
* structure of JPEG metadata. It supports both predefined slots (registered by
* name) and fully custom slots specified via tag metadata.
* </p>
*
* <p>
* Slot specifications can be written as strings using the following syntax:
* </p>
*
* <pre>{@code
* [group.]name[/tag=tagId,type,count,dir][:capacity]
* }</pre>
*
* <ul>
* <li><b>group</b>: Optional slot group, one of EXIF, GPS, INTEROP, TIFF, or
* THUMBNAIL. Default is EXIF.</li>
* <li><b>name</b>: Slot name (must match a registered name or be part of a tag
* definition).</li>
* <li><b>tag=...</b>: Optional explicit tag definition for custom slots.</li>
* <li><b>capacity</b>: Optional maximum capacity in bytes (default is
* 1024).</li>
* </ul>
*
* <p>
* Predefined slots are stored in a registry and provide convenient aliases for
* common EXIF tags. Custom slots can be created dynamically using tag
* information and are parsed using the {@link #parse(String)} method.
* </p>
*
* <p>
* This class is immutable and not intended to be extended.
* </p>
*
* @author Leo Galambos
*/
public final class Slot { // NOPMD
private static final Map<String, Slot> REGISTRY = new HashMap<>();
static {
register("usercomment", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096);
register("makernote", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096);
register("exifversion", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024);
register("software", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_SOFTWARE, 2048);
register("interoptag", SlotGroup.INTEROP,
new TagInfoUndefined("CustomInterop", 0xC4A6, TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD), 2048);
register("thumbcomment", SlotGroup.THUMBNAIL,
new TagInfoAscii("ThumbComment", 0xC4A7, 64, TiffDirectoryType.TIFF_DIRECTORY_IFD1), 1024);
}
private static final Map<String, TiffDirectoryType> DIRECTORY_MAP = Map.ofEntries(
Map.entry("ifd0", TiffDirectoryType.TIFF_DIRECTORY_IFD0),
Map.entry("ifd1", TiffDirectoryType.TIFF_DIRECTORY_IFD1),
Map.entry("ifd2", TiffDirectoryType.TIFF_DIRECTORY_IFD2),
Map.entry("ifd3", TiffDirectoryType.TIFF_DIRECTORY_IFD3),
Map.entry("exif", TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD),
Map.entry("gps", TiffDirectoryType.EXIF_DIRECTORY_GPS),
Map.entry("interop", TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD),
Map.entry("maker", TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTES),
Map.entry("root", TiffDirectoryType.TIFF_DIRECTORY_ROOT),
Map.entry("sub_ifd", TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD));
private static final Map<String, AbstractFieldType> FIELD_TYPE_MAP = new HashMap<>();
static {
for (AbstractFieldType t : AbstractFieldType.ANY) {
FIELD_TYPE_MAP.put(t.getName().toLowerCase(Locale.ROOT), t);
}
}
/**
* Logical groupings of EXIF/TIFF directories used to categorize slots.
* <p>
* Each group corresponds to a specific metadata section within an image file,
* typically used in image metadata standards such as EXIF and TIFF.
*/
public enum SlotGroup {
/**
* EXIF metadata group, containing tags related to camera settings, image
* capture information, and other extended image metadata defined by the EXIF
* standard.
*/
EXIF,
/**
* TIFF metadata group, which includes baseline TIFF tags such as image
* dimensions, compression type, and color format.
*/
TIFF,
/**
* GPS metadata group, storing geolocation information like latitude, longitude,
* altitude, and GPS timestamp.
*/
GPS,
/**
* Interoperability metadata group, used to ensure compatibility across
* different file formats or devices. Typically contains a few specific tags.
*/
INTEROP,
/**
* Thumbnail metadata group, storing metadata and image data for embedded
* preview thumbnails.
*/
THUMBNAIL
}
/**
* The logical group (e.g., EXIF, GPS, TIFF) this slot belongs to.
*/
public final SlotGroup group;
/**
* Metadata tag info describing the structure of this slot.
*/
public final TagInfo tagInfo;
/**
* The default capacity, in bytes, of this slot for data embedding.
*/
public final int defaultCapacity;
private Slot(SlotGroup group, TagInfo tagInfo, int defaultCapacity) {
this.group = group;
this.tagInfo = tagInfo;
this.defaultCapacity = defaultCapacity;
}
/**
* Registers a named predefined slot in the internal registry. This allows slots
* to be referenced later by name in shorthand form.
*
* @param name the lowercase name used for lookup
* @param group the logical slot group
* @param tagInfo the tag metadata for the slot
* @param defaultCapacity the default maximum capacity for this slot in bytes
* @return the registered {@code Slot} instance
*/
public static Slot register(String name, SlotGroup group, TagInfo tagInfo, int defaultCapacity) {
Slot slot = new Slot(group, tagInfo, defaultCapacity);
REGISTRY.put(name.toLowerCase(Locale.ROOT), slot);
return slot;
}
/**
* Parses a slot specification string into a {@code Slot} object.
* <p>
* The input may refer to a predefined slot by name, or a custom slot defined
* with full tag details. See class-level documentation for the full
* specification syntax.
* </p>
*
* @param spec the slot specification string
* @return a {@code Slot} representing the parsed definition
* @throws IllegalArgumentException if the input format is invalid or references
* unknown components
*/
public static Slot parse(String spec) {
Pattern pattern = Pattern.compile(
"(?:(\\w+)\\.)?(\\w+)(?:/tag=([\\d]+),([a-z0-9_]+),(\\d+),(\\w+))?(?::(\\d+))?",
Pattern.CASE_INSENSITIVE);
Matcher m = pattern.matcher(spec);
if (!m.matches()) {
throw new IllegalArgumentException("Invalid slot spec: " + spec);
}
String groupStr = m.group(1);
String name = m.group(2);
String tagIdStr = m.group(3);
String typeStr = m.group(4);
String countStr = m.group(5);
String dirStr = m.group(6);
String sizeStr = m.group(7);
SlotGroup group = groupStr != null ? SlotGroup.valueOf(groupStr.toUpperCase(Locale.ROOT)) : SlotGroup.EXIF;
int size = sizeStr != null ? Integer.parseInt(sizeStr) : 1024;
if (tagIdStr == null) {
Slot predefined = REGISTRY.get(name.toLowerCase(Locale.ROOT));
if (predefined == null) {
throw new IllegalArgumentException("No predefined slot found: " + name);
}
return predefined;
}
int tagId = Integer.parseInt(tagIdStr);
int count = Integer.parseInt(countStr);
TiffDirectoryType dir = Optional.ofNullable(DIRECTORY_MAP.get(dirStr.toLowerCase(Locale.ROOT)))
.orElseThrow(() -> new IllegalArgumentException("Unknown directory: " + dirStr));
AbstractFieldType fieldType = Optional.ofNullable(FIELD_TYPE_MAP.get(typeStr.toLowerCase(Locale.ROOT)))
.orElseThrow(() -> new IllegalArgumentException("Unsupported type: " + typeStr));
TagInfo tagInfo = new TagInfo(name, tagId, fieldType, count, dir);
return new Slot(group, tagInfo, size);
}
/**
* Returns a human-readable representation of the slot, including its tag name
* and capacity.
*
* @return string representation of the slot
*/
@Override
public String toString() {
return tagInfo.name + ":" + defaultCapacity;
}
/**
* Compares this slot to another for equality based on tag ID and directory
* type.
*
* @param o the object to compare
* @return true if the other object is a slot with the same tag and directory
*/
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Slot slot = (Slot) o;
return tagInfo.tag == slot.tagInfo.tag && tagInfo.directoryType == slot.tagInfo.directoryType;
}
/**
* Computes a hash code based on the slot's tag and directory.
*
* @return the hash code
*/
@Override
public int hashCode() {
return Objects.hash(tagInfo.tag, tagInfo.directoryType);
}
/**
* Returns a list of default slot configurations commonly available in standard
* EXIF metadata. These slots are selected for compatibility and relatively high
* storage capacity.
*
* @return list of default {@code Slot} instances
*/
public static List<Slot> defaults() {
return List.of(new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096),
new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096),
new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024),
new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_SOFTWARE, 2048));
}
}

View File

@@ -1,113 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Covert data embedding and extraction using JPEG EXIF metadata.
*
* <p>
* This package provides utilities for hiding and recovering binary payloads
* inside EXIF/TIFF fields of JPEG images. The embedder preserves existing
* metadata where possible and distributes the payload across a caller-defined
* sequence of slots, transparently applying Base64 for text-only tags. The
* implementation performs lossless EXIF updates using a JPEG metadata library.
* </p>
*
* <h2>Key elements</h2>
* <ul>
* <li>{@link JpegExifEmbedder} - embeds a length-prefixed binary payload into a
* JPEG by splitting it across configured slots and writes the result using a
* lossless EXIF rewrite; also reconstructs the payload by reading the same
* slots in order.</li>
* <li>{@link Slot} - an immutable description of one EXIF/TIFF field with a
* default capacity and grouping; supports a compact specification syntax and a
* small registry of predefined, high-capacity tags.</li>
* </ul>
*
* <h2>Slot configuration</h2>
* <p>
* Slots identify where and in what order payload chunks are written or read.
* They can be referenced by predefined name (for example, {@code usercomment},
* {@code makernote}) or specified explicitly using tag metadata. The textual
* format is:
* </p>
* <pre>{@code
* [group.]name[/tag=tagId,type,count,dir][:capacity]
* }</pre>
* <ul>
* <li><b>group</b>: EXIF, GPS, INTEROP, TIFF, or THUMBNAIL (default EXIF).</li>
* <li><b>name</b>: registry key or custom tag name.</li>
* <li><b>tag=...</b>: explicit tag definition for a custom slot.</li>
* <li><b>capacity</b>: maximum bytes for this slot (default 1024).</li>
* </ul>
* <p>
* Use {@link Slot#parse(String)} for custom definitions,
* {@link Slot#register(String, Slot.SlotGroup, org.apache.commons.imaging.formats.tiff.taginfos.TagInfo, int)}
* to extend the registry, and {@link Slot#defaults()} to obtain a conservative
* default set.
* </p>
*
* <h2>Payload format and processing</h2>
* <ul>
* <li><b>Length prefix</b>: Embedding reads the payload using a 7-bit
* variable-length size prefix, then splits the bytes across slots in order;
* extraction writes the same prefix back before streaming the reconstructed
* bytes.</li>
* <li><b>Text vs binary slots</b>: When a slot is text-only, the embedder
* Base64-encodes the chunk before storing; binary-capable slots store raw
* bytes.</li>
* <li><b>Metadata preservation</b>: Existing EXIF is retained except for fields
* explicitly overwritten by the chosen slots; updates are written
* losslessly.</li>
* </ul>
*
* <h2>Thread-safety</h2>
* <ul>
* <li>{@link JpegExifEmbedder} maintains a mutable, ordered slot list and is
* not intended for concurrent reconfiguration.</li>
* <li>{@link Slot} instances are immutable and safe to share across
* threads.</li>
* </ul>
*
* <h2>Use cases and extensions</h2>
* <ul>
* <li>Transport encrypted streams disguised within benign JPEG metadata.</li>
* <li>Covert carriers for small secrets with compatibility across common image
* tools.</li>
* <li>Future extension: combine with exporters or steganographic pipelines in
* other packages to automate deployment to public platforms while keeping
* payloads concealed.</li>
* </ul>
*
* @since 1.0
*/
package zeroecho.sdk.integrations.covert.jpeg;

View File

@@ -1,81 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Helpers for generating pseudo-random cover text using character frequency
* tables.
*
* <p>
* This package provides utilities for producing benign-looking textual fillers
* that mimic the statistical properties of a target language. The primary use
* is to generate cover text for covert or steganographic workflows where
* ciphertext or opaque data needs a plausible textual carrier. These helpers do
* not offer confidentiality by themselves; combine them with encryption when
* handling secrets.
* </p>
*
* <h2>Key elements</h2>
* <ul>
* <li>{@link TextualCodec} - utility container for text-oriented helpers.</li>
* <li>{@link TextualCodec.Generator} - frequency-driven character generator
* with a default English distribution and APIs to produce single characters or
* fixed-length strings.</li>
* </ul>
*
* <h2>Typical usage</h2> <pre>{@code
* // Use the built-in English distribution to create a short cover text.
* zeroecho.sdk.integrations.covert.TextualCodec.Generator gen =
* zeroecho.sdk.integrations.covert.TextualCodec.Generator.EN;
* String sample = gen.getText(256);
*
* // Or construct a custom distribution.
* java.util.Map<Character, Double> freq = java.util.Map.ofEntries(
* java.util.Map.entry('e', 12.7), java.util.Map.entry('t', 9.1),
* java.util.Map.entry('a', 8.2), java.util.Map.entry(' ', 25.4)
* );
* zeroecho.sdk.integrations.covert.TextualCodec.Generator custom =
* new zeroecho.sdk.integrations.covert.TextualCodec.Generator(freq);
* String cover = custom.getText(128);
* }</pre>
*
* <h2>Notes</h2>
* <ul>
* <li>Generated text is intended as camouflage, not as a security control.</li>
* <li>Distributions should be non-negative and representative of the intended
* cover domain.</li>
* <li>Instances are not thread-safe; use a separate generator per thread.</li>
* </ul>
*
* @since 1.0
*/
package zeroecho.sdk.integrations.covert;

View File

@@ -1,70 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.stegano;
/**
* Enumeration of image formats supported for steganographic processing.
*
* <p>
* These formats are chosen because they provide predictable binary layouts and
* are commonly used in practical steganographic embedding and extraction.
* </p>
*/
public enum ImageFormat {
/**
* Portable Network Graphics format.
* <p>
* PNG uses lossless compression, which makes it suitable for precise bit-level
* embedding without introducing distortion into the carrier image.
* </p>
*/
PNG,
/**
* Bitmap format.
* <p>
* BMP is an uncompressed raster format. Its simple and direct structure makes
* it reliable for low-level manipulation during steganographic processing.
* </p>
*/
BMP,
/**
* Joint Photographic Experts Group format.
* <p>
* JPEG is a lossy format based on DCT (Discrete Cosine Transform) blocks.
* Steganographic methods for JPEG must work safely within the compression
* domain to preserve image integrity while embedding data.
* </p>
*/
JPEG
}

View File

@@ -1,253 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.stegano;
import java.awt.image.BufferedImage;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
/**
* Least Significant Bit steganography implementation operating in the spatial
* domain.
*
* <p>
* This class embeds and extracts message bits from image pixels by manipulating
* the least significant bit of each grayscale value. The implementation uses
* stream-based I/O to support large inputs without loading everything into
* memory at once.
* </p>
*
* <h2>Algorithm and characteristics</h2>
* <p>
* Embedding proceeds left-to-right, top-to-bottom across pixels. The message is
* prefixed with a 4-byte length header (big-endian) and then written bit-by-bit
* into the LSB of each pixel's grayscale value. During embedding the image
* content is converted to grayscale and written back to all color channels,
* which discards original chroma.
* </p>
* <p>
* Extraction reads the first 32 bits to recover the message length and then
* continues to read that many message bytes from subsequent pixel LSBs.
* </p>
*
* <h2>Capacity</h2>
* <p>
* Effective capacity is {@code width * height - 32} bits, where 32 bits are
* reserved for the length header. The caller must ensure that the message
* length in bits does not exceed the available capacity; otherwise the embedded
* payload will be incomplete.
* </p>
*
* <h2>Format support</h2>
* <p>
* Output must be a lossless format such as PNG or BMP. JPEG is not supported
* because its lossy compression would destroy LSB-embedded payloads.
* </p>
*
* <h2>Usage</h2> <pre>{@code
* SteganographyMethod method = new LSBSteganographyMethod();
* try (InputStream carrier = Files.newInputStream(Path.of("cover.png"));
* InputStream secret = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8));
* InputStream stego = method.embed(carrier, ImageFormat.PNG, secret)) {
* Files.write(Path.of("stego.png"), stego.readAllBytes());
* }
*
* try (InputStream stego = Files.newInputStream(Path.of("stego.png"));
* InputStream recovered = method.extract(stego)) {
* String text = new String(recovered.readAllBytes(), StandardCharsets.UTF_8);
* }
* }</pre>
*/
public class LSBSteganographyMethod implements SteganographyMethod {
private static final Logger LOG = Logger.getLogger(LSBSteganographyMethod.class.getName());
/**
* Embeds the provided message into the least significant bits of the image and
* returns a new image stream in the requested lossless format.
*
* <p>
* The output image is grayscale and may differ visually from the input because
* the algorithm writes identical gray values to all RGB channels. The message
* is stored as a 4-byte big-endian length prefix followed by the raw message
* bytes.
* </p>
*
* @param inputImage the source image stream to use as the cover image
* @param outputFormat the desired output format; must be lossless (for example,
* {@code PNG} or {@code BMP})
* @param message the message stream to embed
* @return an {@code InputStream} containing the stego image encoded in
* {@code outputFormat}
* @throws IOException if an I/O error occurs while reading the
* image or message, or while writing the
* output image
* @throws IllegalArgumentException if {@code outputFormat} is unsupported for
* this method (for example, {@code JPEG})
*/
@Override
public InputStream embed(InputStream inputImage, ImageFormat outputFormat, InputStream message) throws IOException {
if (outputFormat == ImageFormat.JPEG) {
throw new IllegalArgumentException("LSB does not support lossy formats like JPEG.");
}
BufferedImage img = ImageIO.read(inputImage);
if (img == null) {
throw new IOException("Failed to read input image.");
}
byte[] messageBytes = message.readAllBytes();
ByteArrayOutputStream fullData = new ByteArrayOutputStream();
DataOutputStream dos = new DataOutputStream(fullData);
dos.writeInt(messageBytes.length);
dos.write(messageBytes);
byte[] fullMessage = fullData.toByteArray();
final int width = img.getWidth();
final int height = img.getHeight();
int msgBitIndex = 0;
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "embed to {2} picture w={0} x h={1}", new Object[] { width, height, outputFormat });
}
outer: for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
if (msgBitIndex >= fullMessage.length * 8) {
break outer;
}
int rgb = img.getRGB(x, y);
int gray = rgb & 0xFF;
int bit = (fullMessage[msgBitIndex / 8] >> (7 - (msgBitIndex % 8))) & 1;
gray = (gray & 0xFE) | bit;
int newRgb = (gray << 16) | (gray << 8) | gray;
img.setRGB(x, y, newRgb);
msgBitIndex++;
}
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
ImageIO.write(img, outputFormat.name().toLowerCase(Locale.ROOT), out);
return new ByteArrayInputStream(out.toByteArray());
}
/**
* Extracts the embedded message from the least significant bits of the provided
* image.
*
* <p>
* This method expects the message to be stored as a 4-byte big-endian length
* prefix followed by exactly that many message bytes. The input should be a
* lossless image produced by
* {@link #embed(InputStream, ImageFormat, InputStream)} or an equivalent LSB
* spatial-domain encoder.
* </p>
*
* @param inputImage the stego image stream that contains an embedded message
* @return an {@code InputStream} of the extracted message bytes
* @throws IOException if an I/O error occurs while reading the image or if the
* image cannot be decoded
*/
@Override
public InputStream extract(InputStream inputImage) throws IOException {
BufferedImage img = ImageIO.read(inputImage);
if (img == null) {
throw new IOException("Failed to read input image.");
}
final int width = img.getWidth();
final int height = img.getHeight();
ByteArrayOutputStream messageBytes = new ByteArrayOutputStream();
if (LOG.isLoggable(Level.INFO)) {
LOG.log(Level.INFO, "extract from picture w={0} x h={1}", new Object[] { width, height });
}
int byteVal = 0;
int messageLength = -1;
int bitsCollected = 0;
outer: for (int y = 0; y < height; y++) {
for (int x = 0; x < width; x++) {
int gray = img.getRGB(x, y) & 0xFF;
int bit = gray & 1;
byteVal = (byteVal << 1) | bit;
bitsCollected++;
if (bitsCollected == 8) { // NOPMD
messageBytes.write(byteVal);
byteVal = 0;
bitsCollected = 0;
if (messageLength == -1 && messageBytes.size() == 4) {
byte[] lenBytes = messageBytes.toByteArray();
messageBytes.reset();
messageLength = ByteBuffer.wrap(lenBytes).getInt();
}
if (messageLength != -1 && messageBytes.size() == messageLength) {
break outer;
}
}
}
}
return new ByteArrayInputStream(messageBytes.toByteArray());
}
/**
* Returns metadata describing this LSB spatial-domain steganography method.
*
* <p>
* The metadata includes a short identifier, a descriptive long name, and a
* concise description of the embedding approach.
* </p>
*
* @return method metadata for LSB spatial-domain steganography
*/
@Override
public StegoMetadata getMetadata() {
return new StegoMetadata("LSB", "Least Significant Bit Spatial Domain Image Steganography",
"Embeds message bits into the least significant bits of grayscale pixel values.");
}
}

View File

@@ -1,109 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.stegano;
import java.io.IOException;
import java.io.InputStream;
/**
* Contract for stream-based steganographic methods.
*
* <p>
* Implementations of this interface provide algorithms to hide and recover
* messages in digital images. Streams are used for both image data and message
* payloads to support large files and efficient, pipeline-friendly processing.
* </p>
*
* <p>
* A typical implementation will consume an input image stream, apply an
* embedding or extraction algorithm, and return a new stream without requiring
* the entire image to reside in memory.
* </p>
*/
public interface SteganographyMethod {
/**
* Embeds a message into a source image and produces a new image in the
* requested format.
*
* <p>
* The caller provides an input image, a message stream, and specifies the
* desired output format. The implementation applies its embedding algorithm and
* returns an {@link InputStream} representing the new image.
* </p>
*
* @param inputImage the source image stream; format may vary depending on
* implementation capabilities
* @param outputFormat the desired format for the resulting image; must be
* supported and generally lossless (for example,
* {@link ImageFormat#PNG})
* @param message the stream containing the message to be embedded in the
* image
* @return an input stream of the new image containing the embedded message,
* encoded in the requested format
* @throws IOException if an I/O error occurs while reading or
* writing streams
* @throws IllegalArgumentException if the specified output format is not
* supported by this method
*/
InputStream embed(InputStream inputImage, ImageFormat outputFormat, InputStream message) throws IOException;
/**
* Extracts a hidden message from an image.
*
* <p>
* The caller supplies an image stream that contains an embedded message. The
* implementation applies its extraction algorithm and returns the recovered
* message as a new stream.
* </p>
*
* @param inputImage the image stream containing an embedded message; must be in
* a format compatible with this method
* @return an input stream of the extracted message data
* @throws IOException if an I/O error occurs while reading or writing streams
*/
InputStream extract(InputStream inputImage) throws IOException;
/**
* Returns metadata describing this steganographic method.
*
* <p>
* The metadata includes a short identifier, a descriptive name, and a textual
* explanation of the embedding technique.
* </p>
*
* @return the metadata object associated with this steganographic method
*/
StegoMetadata getMetadata();
}

View File

@@ -1,61 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.stegano;
/**
* Metadata that describes a steganographic method.
*
* <p>
* Each instance carries both a short identifier and a more descriptive
* technical name, together with a human-readable explanation of the embedding
* approach. This record is typically used for discovery, logging, or user
* interfaces that need to present details about available steganographic
* techniques.
* </p>
*
* <h2>Examples</h2> <pre>{@code
* StegoMetadata meta = new StegoMetadata(
* "LSB",
* "Least Significant Bit Embedding",
* "Encodes hidden data into the least significant bits of pixel values"
* );
* }</pre>
*
* @param name method identifier, for example {@code "LSB"} or
* {@code "DCT"}
* @param fullName full technical name of the steganographic method
* @param description short explanation of how the method works
*/
public record StegoMetadata(String name, String fullName, String description) {
}

View File

@@ -1,84 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
/**
* Steganographic integrations for ZeroEcho SDK.
*
* <p>
* This package provides abstractions and implementations for hiding and
* recovering messages in digital images. Steganography allows information to be
* concealed in carrier media so that its presence is not apparent to a casual
* observer.
* </p>
*
* <h2>Design principles</h2>
* <ul>
* <li>All algorithms implement the
* {@link zeroecho.sdk.integrations.stegano.SteganographyMethod} interface,
* which defines stream-oriented {@code embed} and {@code extract} operations
* and supplies a metadata descriptor.</li>
* <li>Supported carrier formats are represented by
* {@link zeroecho.sdk.integrations.stegano.ImageFormat}. Only lossless formats
* are suitable for direct bit-level embedding, but some algorithms may also
* provide support for lossy domains such as JPEG with dedicated
* techniques.</li>
* <li>Methods are designed to operate on {@link java.io.InputStream} and
* {@link java.io.OutputStream} pipelines, making them compatible with large
* files and streaming workflows.</li>
* </ul>
*
* <h2>Extensibility</h2>
* <p>
* The package is not limited to one specific algorithm. In addition to classic
* spatial-domain approaches, implementors can provide methods that operate in
* the frequency domain (for example, DCT coefficients for JPEG) or more
* advanced hybrid techniques. New methods can be registered by implementing the
* {@code SteganographyMethod} interface and returning appropriate
* {@link zeroecho.sdk.integrations.stegano.StegoMetadata}.
* </p>
*
* <h2>Typical workflow</h2> <pre>{@code
* SteganographyMethod method = ...; // e.g. resolved from a registry
* try (InputStream carrier = Files.newInputStream(Path.of("cover.png"));
* InputStream secret = new ByteArrayInputStream("hello".getBytes(UTF_8));
* InputStream stego = method.embed(carrier, ImageFormat.PNG, secret)) {
* Files.write(Path.of("stego.png"), stego.readAllBytes());
* }
*
* try (InputStream stego = Files.newInputStream(Path.of("stego.png"));
* InputStream recovered = method.extract(stego)) {
* String text = new String(recovered.readAllBytes(), UTF_8);
* }
* }</pre>
*/
package zeroecho.sdk.integrations.stegano;

View File

@@ -1,112 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.content.export;
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.security.SecureRandom;
import java.util.Base64;
import org.junit.jupiter.api.Test;
class Base64StreamTest {
@Test
void testEncodedOutputFrom4KBInput() throws Exception {
System.out.println("testEncodedOutputFrom4KBInput");
byte[] inputBytes = new byte[4096];
new SecureRandom().nextBytes(inputBytes); // random 4KB data
byte[] inputEncoded = Base64.getEncoder().encode(inputBytes);
System.out.println("...encoded length should be: " + inputEncoded.length);
int lineLength = 76;
String[] prefixes = { "echo ", null };
byte[][] lineSeparators = { " >> file.tmp\r\n".getBytes(StandardCharsets.US_ASCII),
"\n".getBytes(StandardCharsets.US_ASCII), null };
for (String prefix : prefixes) {
for (byte[] lineSep : lineSeparators) {
System.out.printf("...***** testing with prefix=%s and lineSep=%s%n",
prefix == null ? "null" : "\"" + prefix + "\"",
lineSep == null ? "null" : "\"" + new String(lineSep, StandardCharsets.US_ASCII) + "\"");
try (InputStream base64Stream = new Base64Stream(new ByteArrayInputStream(inputBytes),
prefix == null ? null : prefix.getBytes(StandardCharsets.US_ASCII), lineLength, lineSep)) {
String encodedOutput = new String(base64Stream.readAllBytes(), StandardCharsets.US_ASCII);
System.out.println(encodedOutput);
String[] lines = encodedOutput.split("\n");
for (int i = 0; i < lines.length; i++) {
int space = lines[i].indexOf(' ');
if (space > 0) {
lines[i] = lines[i].split(" ")[prefix == null ? 0 : 1];
}
}
if (lineSep != null) {
for (String line : lines) {
assertTrue(line.length() <= lineLength, "Line exceeds max length: " + line.length());
}
}
String joined = String.join("", lines);
System.out.println("...lines: " + lines.length);
System.out.println("......encoded length: " + joined.length());
if (lineSep == null && prefix != null) {
continue;
}
byte[] decoded = Base64.getDecoder().decode(joined);
System.out.println("...decoded length: " + decoded.length);
assertArrayEquals(inputBytes, decoded, "Decoded content does not match original input");
}
}
}
System.out.println("...ok");
}
}

View File

@@ -1,74 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.covert;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Test;
public class TextualCodecTest {
@Test
void testGenerate100CharacterText() {
System.out.println("testGenerate100CharacterText");
TextualCodec.Generator generator = TextualCodec.Generator.EN;
String result = generator.getText(100);
System.out.println("...generated text: " + result);
assertNotNull(result, "Generated text should not be null");
assertEquals(100, result.length(), "Generated text should have 100 characters");
// Ensure all characters are from the expected alphabet
for (char c : result.toCharArray()) {
assertTrue(TextualCodec.Generator.ENGLISH.containsKey(c),
"Generated character '" + c + "' is not part of the English frequency table");
}
// Optional: Analyze distribution for debugging
Map<Character, Integer> histogram = new HashMap<>();
for (char c : result.toCharArray()) {
histogram.merge(c, 1, Integer::sum);
}
System.out.println("...character distribution: " + histogram);
System.out.println("...ok");
}
}

View File

@@ -1,185 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.covert.jpeg;
import static org.junit.jupiter.api.Assertions.assertEquals;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import javax.crypto.SecretKey;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import conflux.Ctx;
import conflux.CtxInterface;
import zeroecho.core.CryptoAlgorithms;
import zeroecho.core.alg.aes.AesKeyGenSpec;
import zeroecho.core.alg.aes.AesSpec;
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
import zeroecho.sdk.builders.core.DataContentChainBuilder;
import zeroecho.sdk.builders.core.PlainBytesBuilder;
import zeroecho.sdk.content.api.DataContent;
class JpegExifIntegrationTest {
@TempDir
Path tempDir;
private static byte[] readAll(InputStream in) throws IOException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
in.transferTo(out);
return out.toByteArray();
}
@Test
void testEncryptEmbedExtractDecrypt() throws Exception {
System.out.println("testEncryptEmbedExtractDecrypt");
String inputText = "Hello, this is a secret message to embed in JPEG.";
inputText = inputText + inputText;
inputText = inputText + inputText;
inputText = inputText + inputText;
final byte[] inputBytes = inputText.getBytes(StandardCharsets.UTF_8);
final byte[] aad = { 1, 2, 3 };
System.out.println("...inputBytes.length=" + inputBytes.length);
// AES encryption setup
/*
* CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key =
* aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.
* aes256()); AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key,
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" +
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
* enc).setContext(session);
*/
SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
.generateSecret(AesKeyGenSpec.aes256());
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
byte[] encryptedBytes;
DataContent dccb = DataContentChainBuilder.encrypt()
// input
.add(PlainBytesBuilder.builder().bytes(inputBytes))
// encryption
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded())
// using general AES/GCM/128 without specified header
.spec(AesSpec.gcm128(null))
// but let the builder add the default header for storing AAD and IV
.withHeader().withAad(aad).context(session))
// and create the pipeline
.build();
try (InputStream in = dccb.getStream()) {
encryptedBytes = readAll(in);
}
System.out.println("...encryptedBytes.length=" + encryptedBytes.length);
System.out.println("...-> " + Arrays.toString(encryptedBytes));
// Prepare JPEG test image
Path jpegOriginal = getResourcePath("test.jpg");
Path jpegEmbedded = tempDir.resolve("stego.jpg");
// Embed payload
try (InputStream payloadInput = new ByteArrayInputStream(encryptedBytes);
OutputStream jpegOutput = Files.newOutputStream(jpegEmbedded)) {
JpegExifEmbedder embedder = new JpegExifEmbedder();
embedder.setSlots(Slot.defaults());
int embed_size = embedder.embed(jpegOriginal, payloadInput, jpegOutput);
System.out.println("...embeddedStream.length=" + embed_size);
}
// Extract payload
ByteArrayOutputStream extractedEncrypted = new ByteArrayOutputStream();
JpegExifEmbedder embedder = new JpegExifEmbedder();
embedder.setSlots(Slot.defaults());
embedder.extract(jpegEmbedded, extractedEncrypted);
byte[] extractedEncryptedBytes = extractedEncrypted.toByteArray();
System.out.println("...extractedEncryptedBytes.length=" + extractedEncryptedBytes.length);
System.out.println("...-> " + Arrays.toString(extractedEncryptedBytes));
// Decrypt
dccb = DataContentChainBuilder.decrypt()
// input
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
// encryption
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null))
// let us use the default header for AAD and IV
.withHeader().withAad(aad).context(session))
// and create the pipeline
.build();
byte[] pt1;
try (InputStream in = dccb.getStream()) {
pt1 = readAll(in);
}
/*
* AesSpec spec =
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
* EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT,
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
* byte[] pt1 = readAll(dec1.attach(new
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();
*/
String decrypted = new String(pt1, StandardCharsets.UTF_8);
assertEquals(inputText, decrypted);
System.out.println("...ok");
}
private Path getResourcePath(String resource) throws URISyntaxException {
URL url = getClass().getClassLoader().getResource(resource);
if (url == null) {
throw new IllegalArgumentException("Missing resource: " + resource);
}
return Paths.get(url.toURI());
}
}

View File

@@ -1,88 +0,0 @@
/*******************************************************************************
* Copyright (C) 2026, Leo Galambos
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* 3. All advertising materials mentioning features or use of this software must
* display the following acknowledgement:
* This product includes software developed by the Egothor project.
*
* 4. Neither the name of the copyright holder nor the names of its contributors
* may be used to endorse or promote products derived from this software without
* specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
******************************************************************************/
package zeroecho.sdk.integrations.stegano;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.io.ByteArrayInputStream;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.nio.file.StandardOpenOption;
import org.junit.jupiter.api.Test;
public class LSBSteganographyMethodTest {
@Test
void testEmbedAndExtract() throws Exception {
SteganographyMethod method = new LSBSteganographyMethod();
String message = "Hello from LSB!";
InputStream imageIn = getClass().getResourceAsStream("/test.jpg");
InputStream msgIn = new ByteArrayInputStream(message.getBytes());
// Embed
InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn);
// Extract
InputStream extracted = method.extract(embedded);
String result = new String(extracted.readAllBytes());
assertEquals(message, result);
}
@Test
void testMetadata() {
SteganographyMethod method = new LSBSteganographyMethod();
StegoMetadata meta = method.getMetadata();
assertEquals("LSB", meta.name());
assertTrue(meta.fullName().contains("Least Significant"));
}
@Test
void createSample() throws Exception {
// for verification purposes
SteganographyMethod method = new LSBSteganographyMethod();
String message = "Hello from LSB!";
InputStream imageIn = getClass().getResourceAsStream("/test.jpg");
InputStream msgIn = new ByteArrayInputStream(message.getBytes());
// Embed
InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn);
embedded.transferTo(Files.newOutputStream(Paths.get("/tmp/lsb-test.png"), StandardOpenOption.CREATE));
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 230 KiB