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

32
ext/.classpath Normal file
View File

@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="src" output="bin/main" path="src/main/java">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/main" path="src/main/resources">
<attributes>
<attribute name="gradle_scope" value="main"/>
<attribute name="gradle_used_by_scope" value="main,test"/>
</attributes>
</classpathentry>
<classpathentry kind="src" output="bin/test" path="src/test/java">
<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/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="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-21/"/>
<classpathentry kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
<classpathentry kind="output" path="bin/default"/>
</classpath>

23
ext/.project Normal file
View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
<name>ext</name>
<comment>Project ext created by Buildship.</comment>
<projects>
</projects>
<buildSpec>
<buildCommand>
<name>org.eclipse.jdt.core.javabuilder</name>
<arguments>
</arguments>
</buildCommand>
<buildCommand>
<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
<arguments>
</arguments>
</buildCommand>
</buildSpec>
<natures>
<nature>org.eclipse.jdt.core.javanature</nature>
<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
</natures>
</projectDescription>

31
ext/LICENSE Normal file
View File

@@ -0,0 +1,31 @@
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.

13
ext/build.gradle Normal file
View File

@@ -0,0 +1,13 @@
plugins {
id 'buildlogic.java-library-conventions'
id 'com.palantir.git-version'
}
group='org.egothor'
dependencies {
api 'org.bouncycastle:bcpkix-jdk18on'
implementation 'org.apache.commons:commons-imaging'
implementation project(':lib')
testImplementation 'org.egothor:conflux'
}

View File

@@ -0,0 +1,197 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,234 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,110 @@
/*******************************************************************************
* 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.ext.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.ext.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.ext.content.api.AbstractExportableDataContent} and
* honors
* {@link zeroecho.ext.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.ext.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.ext.content.api.ExportableDataContent content = ... some exportable content ...;
* content.setExportMode(zeroecho.ext.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.ext.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.ext.content.api.AbstractExportableDataContent} and select a
* default {@link zeroecho.ext.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.ext.content.export;

View File

@@ -0,0 +1,89 @@
/*******************************************************************************
* 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.
******************************************************************************/
/**
* Content abstractions and streaming payloads for the SDK.
*
* <p>
* This package defines the common content model used by the SDK and organizes
* concrete sources and exporters under dedicated subpackages. Content objects
* expose streaming access to bytes and can be chained into pipelines produced
* by builder APIs.
* </p>
*
* <h2>Subpackages</h2>
* <ul>
* <li>{@link zeroecho.ext.content.export} - export helpers and platform
* deployers, such as {@link zeroecho.ext.content.export.Base64Stream} and
* exportable content that targets external destinations (for example, gallery
* platforms) or script-based transports.</li>
* </ul>
*
* <h2>Responsibilities</h2>
* <ul>
* <li>Support exportable content that can render itself as raw bytes or
* platform-specific scripts via
* {@link zeroecho.sdk.content.api.ExportableDataContent}.</li>
* </ul>
*
* <h2>Typical usage</h2> <pre>{@code
* // Stream a DataContent instance to an OutputStream.
* zeroecho.sdk.content.api.DataContent content = ... obtained from a builder chain ...;
* try (java.io.InputStream in = content.getStream()) {
* in.transferTo(out);
* }
*
* // If exportable, render in an alternate mode (for example, a shell script).
* if (content instanceof zeroecho.sdk.content.api.ExportableDataContent exportable) {
* exportable.setExportMode(
* zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
* try (java.io.InputStream script = exportable.getStream()) {
* script.transferTo(out);
* }
* }
* }</pre>
*
* <h2>Extensibility</h2>
* <ul>
* <li>New exporters should extend
* {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and honor the
* selected
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.</li>
* <li>The export area is intended to host deployers for additional public
* platforms and, where appropriate, steganographic carriers for concealed
* delivery of encrypted streams.</li>
* </ul>
*
* @since 1.0
*/
package zeroecho.ext.content;

View File

@@ -0,0 +1,177 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,227 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,302 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,113 @@
/*******************************************************************************
* 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.ext.integrations.covert.jpeg;

View File

@@ -0,0 +1,81 @@
/*******************************************************************************
* 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.ext.integrations.covert.TextualCodec.Generator gen =
* zeroecho.ext.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.ext.integrations.covert.TextualCodec.Generator custom =
* new zeroecho.ext.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.ext.integrations.covert;

View File

@@ -0,0 +1,70 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,253 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,109 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,61 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,84 @@
/*******************************************************************************
* 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.ext.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.ext.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.ext.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.ext.integrations.stegano;

View File

@@ -0,0 +1,112 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,74 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,185 @@
/*******************************************************************************
* 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.ext.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

@@ -0,0 +1,88 @@
/*******************************************************************************
* 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.ext.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.

After

Width:  |  Height:  |  Size: 230 KiB