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:
@@ -1,197 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.content.export;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.util.Base64;
|
||||
import java.util.Base64.Encoder;
|
||||
|
||||
/**
|
||||
* A streaming {@link InputStream} that encodes binary input data into Base64
|
||||
* format with optional line prefixes and suffixes for each encoded line.
|
||||
*
|
||||
* <p>
|
||||
* This class is designed for script-friendly output formatting, such as
|
||||
* generating platform-specific batch or shell script lines where each line
|
||||
* might begin with a command (e.g., {@code echo }) and end with a
|
||||
* platform-specific line terminator (e.g., {@code \n} for UNIX-like systems or
|
||||
* {@code \r\n} for Windows).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* It supports chunked streaming and avoids holding the entire Base64-encoded
|
||||
* content in memory, making it suitable for large binary inputs. Lines are
|
||||
* split according to the specified maximum line length and are composed as
|
||||
* follows:
|
||||
* </p>
|
||||
*
|
||||
* <pre>
|
||||
* [prefix][base64-encoded data][suffix]
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* If a suffix is defined, the actual Base64 data per line will be truncated to
|
||||
* {@code lineLength - suffix.length} characters to ensure total line length
|
||||
* does not exceed {@code lineLength}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This stream does not automatically insert newlines between lines unless
|
||||
* explicitly provided via the {@code suffix} argument (e.g.,
|
||||
* {@code "\n".getBytes()}).
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* <strong>Usage example:</strong>
|
||||
* </p>
|
||||
*
|
||||
* <pre>{@code
|
||||
* InputStream source = new FileInputStream("input.bin");
|
||||
* InputStream encoded = new Base64Stream(
|
||||
* source,
|
||||
* "echo ".getBytes(StandardCharsets.UTF_8),
|
||||
* 76,
|
||||
* "\n".getBytes(StandardCharsets.UTF_8)
|
||||
* );
|
||||
* encoded.transferTo(System.out);
|
||||
* }</pre>
|
||||
*
|
||||
* @author Leo Galambos
|
||||
*/
|
||||
public class Base64Stream extends InputStream {
|
||||
|
||||
private final InputStream source;
|
||||
private InputStream in;
|
||||
private InputStream prefix;
|
||||
private InputStream suffix;
|
||||
private final Encoder base64;
|
||||
private int pos;
|
||||
private final int lineLength;
|
||||
private int lineBreak;
|
||||
|
||||
private InputStream is = nullInputStream(); // NOPMD
|
||||
private int breakPos; // NOPMD
|
||||
private boolean closed;
|
||||
|
||||
private final static int TRIPLES_INPUT = 1000;
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Base64Stream}.
|
||||
*
|
||||
* @param source the raw binary input stream to be Base64 encoded
|
||||
* @param linePrefix optional prefix to prepend at the beginning of each line
|
||||
* (e.g., {@code "echo "}); can be {@code null} for no prefix
|
||||
* @param lineLength the total maximum length of each output line, including any
|
||||
* prefix and suffix
|
||||
* @param lineSuffix optional suffix to append at the end of each line (e.g.,
|
||||
* newline); can be {@code null}
|
||||
*/
|
||||
public Base64Stream(InputStream source, byte[] linePrefix, int lineLength, byte[] lineSuffix) {
|
||||
super();
|
||||
|
||||
this.source = source;
|
||||
this.lineLength = lineLength;
|
||||
in = nullInputStream();
|
||||
base64 = Base64.getEncoder();
|
||||
if (linePrefix != null) {
|
||||
prefix = new ByteArrayInputStream(linePrefix);
|
||||
}
|
||||
if (lineSuffix != null) {
|
||||
suffix = new ByteArrayInputStream(lineSuffix);
|
||||
}
|
||||
lineBreak = lineLength - ((lineSuffix == null) ? 0 : lineSuffix.length);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read() throws IOException {
|
||||
byte[] result = { 0 };
|
||||
int count = read(result, 0, 1);
|
||||
return (count == 0) ? -1 : result[0] & 0xff;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int read(byte[] b, int off, int len) throws IOException {
|
||||
if (pos == lineLength) {
|
||||
pos = 0;
|
||||
}
|
||||
|
||||
if (pos == 0 && prefix != null) {
|
||||
prefix.reset();
|
||||
breakPos = Integer.MAX_VALUE;
|
||||
is = prefix;
|
||||
} else {
|
||||
if (pos == lineBreak && suffix != null && !closed) {
|
||||
suffix.reset();
|
||||
breakPos = Integer.MAX_VALUE;
|
||||
is = suffix;
|
||||
} else {
|
||||
if (in.available() == 0) {
|
||||
ensureData();
|
||||
}
|
||||
breakPos = lineBreak;
|
||||
is = in;
|
||||
}
|
||||
}
|
||||
|
||||
int l = Math.min(Math.min(is.available(), len), breakPos - pos);
|
||||
pos += l;
|
||||
return is.read(b, off, l);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the next block of raw bytes from the source and encodes them into
|
||||
* Base64. Handles stream exhaustion and final line suffix if applicable.
|
||||
*/
|
||||
private void ensureData() throws IOException {
|
||||
byte[] buf = source.readNBytes(3 * TRIPLES_INPUT);
|
||||
|
||||
if (buf.length == 0 && suffix != null && !closed) {
|
||||
System.out.println("suffix");
|
||||
|
||||
closed = true;
|
||||
suffix.reset();
|
||||
breakPos = lineBreak = Integer.MAX_VALUE;
|
||||
in = suffix;
|
||||
return;
|
||||
}
|
||||
|
||||
if (buf.length == 3 * TRIPLES_INPUT) {
|
||||
in = new ByteArrayInputStream(base64.withoutPadding().encode(buf));
|
||||
} else {
|
||||
in = new ByteArrayInputStream(base64.encode(buf));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,234 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.content.export;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.io.SequenceInputStream;
|
||||
import java.io.Writer;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URI;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
|
||||
import zeroecho.sdk.content.api.AbstractExportableDataContent;
|
||||
|
||||
/**
|
||||
* {@code PiwigoExportDataContent} is a specialized exportable content class
|
||||
* that supports uploading an image file to a Piwigo photo gallery server,
|
||||
* either directly via HTTP POST or by generating platform-specific scripts for
|
||||
* deferred uploading.
|
||||
*
|
||||
* <p>
|
||||
* Depending on the export mode (RAW, BASH_SCRIPT, CMD_SCRIPT), it can:
|
||||
* <ul>
|
||||
* <li>Upload the image to a Piwigo server directly using HTTP
|
||||
* multipart/form-data</li>
|
||||
* <li>Generate a Bash script that decodes the base64-encoded image and uploads
|
||||
* it using curl</li>
|
||||
* <li>Generate a CMD (Windows batch) script that reconstructs the image using
|
||||
* certutil and uploads it</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* This class integrates with {@code AbstractExportableDataContent} and expects
|
||||
* its {@code input} field to be set before invoking {@link #getStream()}.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* The image is uploaded using the {@code pwg.images.add} method of the Piwigo
|
||||
* API.
|
||||
* </p>
|
||||
*/
|
||||
class PiwigoExportDataContent extends AbstractExportableDataContent {
|
||||
private final String imageFileName;
|
||||
private final String piwigoUrl;
|
||||
private final String username;
|
||||
private final String password;
|
||||
private final String albumId;
|
||||
|
||||
/**
|
||||
* Constructs a new exportable Piwigo upload object.
|
||||
*
|
||||
* @param imageFileName the name of the image file to assign during upload or
|
||||
* script output
|
||||
* @param piwigoUrl the URL of the Piwigo API endpoint
|
||||
* @param username the Piwigo username for authentication
|
||||
* @param password the Piwigo password
|
||||
* @param albumId the ID of the Piwigo album to which the image will be
|
||||
* uploaded
|
||||
*/
|
||||
public PiwigoExportDataContent(String imageFileName, String piwigoUrl, String username, String password,
|
||||
String albumId) {
|
||||
super();
|
||||
|
||||
this.imageFileName = imageFileName;
|
||||
this.piwigoUrl = piwigoUrl;
|
||||
this.username = username;
|
||||
this.password = password;
|
||||
this.albumId = albumId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@code InputStream} that provides either the raw upload stream, or
|
||||
* a platform-specific script depending on the export mode.
|
||||
*
|
||||
* @return the resulting {@code InputStream}
|
||||
* @throws IOException if reading the input or creating the stream fails
|
||||
*/
|
||||
@Override
|
||||
public InputStream getStream() throws IOException {
|
||||
if (input == null) {
|
||||
throw new IllegalStateException("Input not set.");
|
||||
}
|
||||
|
||||
return switch (mode) {
|
||||
case RAW -> performDirectUpload(input.getStream());
|
||||
case BASH_SCRIPT -> generateBashScript(input.getStream());
|
||||
case CMD_SCRIPT -> generateCmdScript(input.getStream());
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs a direct upload of the image data to the Piwigo server using a
|
||||
* multipart/form-data HTTP POST request.
|
||||
*
|
||||
* @param dataStream the input stream of the binary image data
|
||||
* @return the server's response stream
|
||||
* @throws IOException if the upload fails
|
||||
*/
|
||||
private InputStream performDirectUpload(InputStream dataStream) throws IOException {
|
||||
String boundary = "----Boundary" + System.currentTimeMillis();
|
||||
|
||||
URL url = URI.create(piwigoUrl).toURL();
|
||||
HttpURLConnection conn = (HttpURLConnection) url.openConnection();
|
||||
conn.setDoOutput(true);
|
||||
conn.setRequestMethod("POST");
|
||||
conn.setRequestProperty("Content-Type", "multipart/form-data; boundary=" + boundary);
|
||||
|
||||
try (OutputStream out = conn.getOutputStream();
|
||||
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(out, StandardCharsets.UTF_8))) {
|
||||
|
||||
writeFormField(writer, boundary, "method", "pwg.images.add");
|
||||
writeFormField(writer, boundary, "username", username);
|
||||
writeFormField(writer, boundary, "password", password);
|
||||
writeFormField(writer, boundary, "category", albumId);
|
||||
|
||||
writer.write("--" + boundary + "\r\n");
|
||||
writer.write("Content-Disposition: form-data; name=\"image\"; filename=\"" + imageFileName + "\"\r\n");
|
||||
writer.write("Content-Type: image/jpeg\r\n\r\n");
|
||||
writer.flush();
|
||||
|
||||
dataStream.transferTo(out);
|
||||
out.flush();
|
||||
writer.write("\r\n--" + boundary + "--\r\n");
|
||||
writer.flush();
|
||||
}
|
||||
|
||||
InputStream responseStream;
|
||||
try {
|
||||
responseStream = conn.getInputStream();
|
||||
} catch (IOException e) {
|
||||
InputStream errorStream = conn.getErrorStream();
|
||||
if (errorStream != null) {
|
||||
return errorStream;
|
||||
}
|
||||
return new ByteArrayInputStream(("Error: " + e.getMessage()).getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
return responseStream;
|
||||
}
|
||||
|
||||
/**
|
||||
* Writes a single form field as part of a multipart/form-data HTTP request.
|
||||
*
|
||||
* @param writer the writer to output the field to
|
||||
* @param boundary the multipart boundary
|
||||
* @param name the name of the form field
|
||||
* @param value the value of the form field
|
||||
* @throws IOException if writing fails
|
||||
*/
|
||||
private void writeFormField(Writer writer, String boundary, String name, String value) throws IOException {
|
||||
writer.write("--" + boundary + "\r\n");
|
||||
writer.write("Content-Disposition: form-data; name=\"" + name + "\"\r\n\r\n");
|
||||
writer.write(value + "\r\n");
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a Bash script that reconstructs the image using a Base64 heredoc
|
||||
* block and uploads it using {@code curl}.
|
||||
*
|
||||
* @param originalStream the original binary stream of the image
|
||||
* @return a stream containing the complete shell script
|
||||
*/
|
||||
private InputStream generateBashScript(InputStream originalStream) {
|
||||
InputStream header = new ByteArrayInputStream(("#!/bin/bash\nset -e\n\ncurl -X POST \"" + piwigoUrl + "\" \\\n"
|
||||
+ " -F method=\"pwg.images.add\" \\\n" + " -F username=\"" + username + "\" \\\n" + " -F password=\""
|
||||
+ password + "\" \\\n" + " -F category=\"" + albumId + "\" \\\n" + " -F image=@<(base64 -d <<'EOF'\n")
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
InputStream body = new Base64Stream(originalStream, null, 76, new byte[] { 10 });
|
||||
InputStream footer = new ByteArrayInputStream("EOF\n)\n".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a CMD batch script that reconstructs the image using certutil and
|
||||
* uploads it using {@code curl}.
|
||||
*
|
||||
* @param originalStream the original binary stream of the image
|
||||
* @return a stream containing the complete Windows batch script
|
||||
*/
|
||||
private InputStream generateCmdScript(InputStream originalStream) {
|
||||
InputStream header = new ByteArrayInputStream(
|
||||
"@echo off\nsetlocal\necho -----BEGIN BASE64----- > tmp.b64\n".getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
@SuppressWarnings("resource")
|
||||
InputStream body = new Base64Stream(originalStream, "echo ".getBytes(), 76, " >> tmp.b64\r\n".getBytes());
|
||||
InputStream footer = new ByteArrayInputStream(
|
||||
("echo -----END BASE64----- >> tmp.b64\n" + "certutil -decode tmp.b64 \"" + imageFileName + "\" >nul\n"
|
||||
+ "del tmp.b64\n" + "curl -X POST \"" + piwigoUrl + "\" ^\n" + " -F method=pwg.images.add ^\n"
|
||||
+ " -F username=" + username + " ^\n" + " -F password=" + password + " ^\n" + " -F category="
|
||||
+ albumId + " ^\n" + " -F image=@" + imageFileName + "\n" + "del \"" + imageFileName + "\"\n")
|
||||
.getBytes(StandardCharsets.UTF_8));
|
||||
|
||||
return new SequenceInputStream(new SequenceInputStream(header, body), footer);
|
||||
}
|
||||
}
|
||||
@@ -1,110 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Export helpers and platform deployers for SDK content.
|
||||
*
|
||||
* <p>
|
||||
* This package provides streaming utilities and exportable content
|
||||
* implementations that render {@link zeroecho.sdk.content.api.DataContent} for
|
||||
* deployment to external platforms or for script-based transport. Exports can
|
||||
* be produced as raw bytes or as platform-specific scripts according to
|
||||
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Scope</h2>
|
||||
* <ul>
|
||||
* <li>Streaming utilities that reformat content for export-friendly forms (for
|
||||
* example, Base64 with line control).</li>
|
||||
* <li>Exportable content that uploads directly to a remote service or generates
|
||||
* scripts for deferred execution.</li>
|
||||
* <li>Foundations for additional deployers, including steganographic carriers
|
||||
* and other public platforms beyond Piwigo.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Key elements</h2>
|
||||
* <ul>
|
||||
* <li>{@link Base64Stream} - an {@link java.io.InputStream} that encodes a
|
||||
* binary stream to Base64 with optional per-line prefix/suffix and configurable
|
||||
* line length (useful for script emitters).</li>
|
||||
* <li><i>Piwigo uploader</i> - an exportable content implementation
|
||||
* (package-private) that can either upload an image directly to a Piwigo server
|
||||
* or generate Bash/CMD scripts that reconstruct and upload the image. It is
|
||||
* built on {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and
|
||||
* honors
|
||||
* {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Typical usage</h2>
|
||||
* <h3>Format a stream as Base64 command lines</h3> <pre>{@code
|
||||
* java.io.InputStream source = ... raw bytes ...;
|
||||
* java.io.InputStream encoded = new zeroecho.sdk.content.export.Base64Stream(
|
||||
* source,
|
||||
* "echo ".getBytes(java.nio.charset.StandardCharsets.UTF_8),
|
||||
* 76,
|
||||
* "\n".getBytes(java.nio.charset.StandardCharsets.UTF_8)
|
||||
* );
|
||||
* encoded.transferTo(System.out);
|
||||
* }</pre>
|
||||
*
|
||||
* <h3>Render an exportable content in a chosen mode</h3> <pre>{@code
|
||||
* zeroecho.sdk.content.api.ExportableDataContent content = ... some exportable content ...;
|
||||
* content.setExportMode(zeroecho.sdk.content.api.ExportableDataContent.ExportMode.BASH_SCRIPT);
|
||||
* try (java.io.InputStream script = content.getStream()) {
|
||||
* script.transferTo(out);
|
||||
* }
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Security notes</h2>
|
||||
* <ul>
|
||||
* <li>Prefer exporting {@link zeroecho.sdk.content.api.EncryptedContent} when
|
||||
* targeting untrusted destinations.</li>
|
||||
* <li>Avoid embedding secrets in scripts; pass credentials via environment
|
||||
* variables or secure stores when possible.</li>
|
||||
* <li>When adding steganographic exporters, document cover formats and
|
||||
* concealment limits clearly.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Extensibility</h2>
|
||||
* <ul>
|
||||
* <li>New deployers should extend
|
||||
* {@link zeroecho.sdk.content.api.AbstractExportableDataContent} and select a
|
||||
* default {@link zeroecho.sdk.content.api.ExportableDataContent.ExportMode}
|
||||
* appropriate for the platform.</li>
|
||||
* <li>Utilities like {@link Base64Stream} can be reused to generate
|
||||
* platform-friendly payloads without buffering whole files.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.content.export;
|
||||
@@ -54,10 +54,6 @@
|
||||
* {@link zeroecho.sdk.content.builtin.PlainString},
|
||||
* {@link zeroecho.sdk.content.builtin.PlainFile}, and
|
||||
* {@link zeroecho.sdk.content.builtin.SecretPassword}.</li>
|
||||
* <li>{@link zeroecho.sdk.content.export} - export helpers and platform
|
||||
* deployers, such as {@link zeroecho.sdk.content.export.Base64Stream} and
|
||||
* exportable content that targets external destinations (for example, gallery
|
||||
* platforms) or script-based transports.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Responsibilities</h2>
|
||||
|
||||
@@ -1,177 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.covert;
|
||||
|
||||
import java.util.ArrayDeque;
|
||||
import java.util.Map;
|
||||
import java.util.NavigableMap;
|
||||
import java.util.Queue;
|
||||
import java.util.TreeMap;
|
||||
|
||||
import zeroecho.sdk.util.RandomSupport;
|
||||
|
||||
/**
|
||||
* A utility class for generating pseudo-random textual content based on
|
||||
* predefined character frequency distributions.
|
||||
* <p>
|
||||
* The {@code TextualCodec} class contains a nested {@link Generator} class that
|
||||
* can be configured with a set of character frequencies (e.g., English letter
|
||||
* frequencies) to produce text that mimics the statistical distribution of the
|
||||
* given language or character set.
|
||||
*/
|
||||
public final class TextualCodec { // NOPMD
|
||||
/**
|
||||
* Private constructor to prevent instantiation of {@code TextualCodec}.
|
||||
* <p>
|
||||
* This class is intended to be used as a utility container for static members
|
||||
* and should not be instantiated.
|
||||
*/
|
||||
private TextualCodec() {
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates characters or strings using a frequency-based distribution.
|
||||
* <p>
|
||||
* This generator uses a cumulative frequency table internally to map random
|
||||
* numbers to characters, enabling the creation of realistic-looking text that
|
||||
* follows the given character frequency distribution. The generator also avoids
|
||||
* consecutive duplicate characters by employing a simple queuing mechanism.
|
||||
*/
|
||||
public static class Generator {
|
||||
/**
|
||||
* Internal map representing the cumulative frequency ranges mapped to
|
||||
* characters.
|
||||
*/
|
||||
private final NavigableMap<Double, Character> ranges = new TreeMap<>();
|
||||
/**
|
||||
* The maximum value of the cumulative frequency range.
|
||||
*/
|
||||
private final double maxRange;
|
||||
/**
|
||||
* A predefined English character frequency distribution including the space
|
||||
* character, based on typical usage in English text.
|
||||
*/
|
||||
public final static Map<Character, Double> ENGLISH = Map.ofEntries(Map.entry('a', 8.2), Map.entry('b', 1.5),
|
||||
Map.entry('c', 2.8), Map.entry('d', 4.3), Map.entry('e', 12.7), Map.entry('f', 2.2),
|
||||
Map.entry('g', 2.0), Map.entry('h', 6.1), Map.entry('i', 7.0), Map.entry('j', 0.15),
|
||||
Map.entry('k', 0.77), Map.entry('l', 4.0), Map.entry('m', 2.4), Map.entry('n', 6.7),
|
||||
Map.entry('o', 7.5), Map.entry('p', 1.9), Map.entry('q', 0.095), Map.entry('r', 6.0),
|
||||
Map.entry('s', 6.3), Map.entry('t', 9.1), Map.entry('u', 2.8), Map.entry('v', 0.98),
|
||||
Map.entry('w', 2.4), Map.entry('x', 0.15), Map.entry('y', 2.0), Map.entry('z', 0.074),
|
||||
Map.entry(' ', 25.4));
|
||||
/**
|
||||
* A default generator using the {@link #ENGLISH} frequency distribution.
|
||||
*/
|
||||
public final static Generator EN = new Generator(ENGLISH);
|
||||
|
||||
private Character lastChar = '~';
|
||||
private final Queue<Character> backlog = new ArrayDeque<>();
|
||||
|
||||
/**
|
||||
* Constructs a new {@code Generator} with the specified character frequency
|
||||
* distribution.
|
||||
*
|
||||
* @param frequencies a map of characters to their relative frequencies (must be
|
||||
* non-negative)
|
||||
*/
|
||||
public Generator(Map<Character, Double> frequencies) {
|
||||
double cumulative = 0.0;
|
||||
for (Map.Entry<Character, Double> entry : frequencies.entrySet()) {
|
||||
double freq = entry.getValue();
|
||||
if (freq <= 0) {
|
||||
continue;
|
||||
}
|
||||
ranges.put(cumulative, entry.getKey());
|
||||
cumulative = cumulative + freq;
|
||||
}
|
||||
|
||||
maxRange = cumulative;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a string of the specified length using the configured character
|
||||
* frequency distribution. Consecutive duplicate characters are avoided when
|
||||
* possible.
|
||||
*
|
||||
* @param length the number of characters to generate
|
||||
* @return a randomly generated string
|
||||
*/
|
||||
public String getText(int length) {
|
||||
StringBuffer sb = new StringBuffer();
|
||||
|
||||
while (length-- > 0) { // NOPMD
|
||||
sb.append(getChar());
|
||||
}
|
||||
|
||||
return sb.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next randomly generated character, avoiding consecutive
|
||||
* duplicates when possible.
|
||||
*
|
||||
* @return the next character in the generated sequence
|
||||
*/
|
||||
public char getChar() {
|
||||
if (backlog.isEmpty() || lastChar.equals(backlog.peek())) {
|
||||
Character next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
|
||||
while (lastChar.equals(next)) {
|
||||
backlog.add(next);
|
||||
next = getChar(RandomSupport.getRandom().nextDouble(maxRange));
|
||||
}
|
||||
lastChar = next;
|
||||
return lastChar;
|
||||
}
|
||||
|
||||
return lastChar = backlog.poll();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a character based on the provided value in the frequency range.
|
||||
*
|
||||
* @param value a value between 0 (inclusive) and {@code maxRange} (exclusive)
|
||||
* @return the corresponding character for the specified value
|
||||
* @throws IllegalArgumentException if the value is out of range
|
||||
*/
|
||||
public char getChar(double value) {
|
||||
if (value < 0.0 || value >= maxRange) {
|
||||
throw new IllegalArgumentException("Value must be in [0.0, " + maxRange + ")");
|
||||
}
|
||||
|
||||
Map.Entry<Double, Character> entry = ranges.floorEntry(value);
|
||||
return (entry == null) ? ranges.firstEntry().getValue() : entry.getValue();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,227 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.covert.jpeg;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.ByteOrder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Path;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import org.apache.commons.imaging.Imaging;
|
||||
import org.apache.commons.imaging.ImagingException;
|
||||
import org.apache.commons.imaging.common.ImageMetadata;
|
||||
import org.apache.commons.imaging.formats.jpeg.JpegImageMetadata;
|
||||
import org.apache.commons.imaging.formats.jpeg.exif.ExifRewriter;
|
||||
import org.apache.commons.imaging.formats.tiff.TiffField;
|
||||
import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
|
||||
import org.apache.commons.imaging.formats.tiff.write.TiffOutputDirectory;
|
||||
import org.apache.commons.imaging.formats.tiff.write.TiffOutputField;
|
||||
import org.apache.commons.imaging.formats.tiff.write.TiffOutputSet;
|
||||
|
||||
import zeroecho.core.io.Util;
|
||||
import zeroecho.sdk.util.Pack7LStreamWriter;
|
||||
|
||||
/**
|
||||
* Utility class for embedding and extracting binary payloads across multiple
|
||||
* EXIF slots in JPEG files.
|
||||
* <p>
|
||||
* The configured slot list defines both the location and chunking order of the
|
||||
* embedded payload. Slot types are respected — for example, ASCII fields
|
||||
* automatically use Base64 encoding to ensure compatibility with textual EXIF
|
||||
* constraints.
|
||||
* </p>
|
||||
* <p>
|
||||
* The original JPEG EXIF metadata is preserved, except for any overwritten
|
||||
* fields defined via slot configuration.
|
||||
* </p>
|
||||
*/
|
||||
public final class JpegExifEmbedder {
|
||||
private static final Logger LOG = Logger.getLogger(JpegExifEmbedder.class.getName());
|
||||
|
||||
private final List<Slot> slots = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* Configures the list of EXIF slots to be used for payload embedding and
|
||||
* extraction. The order determines the splitting/joining order of the payload.
|
||||
*
|
||||
* @param slots List of slot configurations
|
||||
*/
|
||||
public void setSlots(List<Slot> slots) {
|
||||
this.slots.clear();
|
||||
this.slots.addAll(slots);
|
||||
}
|
||||
|
||||
/**
|
||||
* Embeds a binary payload into a JPEG file by spreading the data across a
|
||||
* series of pre-configured EXIF slots.
|
||||
* <p>
|
||||
* The payload is read from the provided {@code InputStream}, prefixed using
|
||||
* 7-bit variable-length encoding, and split across EXIF fields based on their
|
||||
* capacity and type. Fields that accept only text data will store the payload
|
||||
* encoded as Base64. The modified JPEG is then written to the given output
|
||||
* stream using a lossless EXIF update.
|
||||
* </p>
|
||||
*
|
||||
* @param jpegPath the path to the input JPEG file; must not be {@code null}
|
||||
* @param payloadInput the input stream containing the binary payload to embed;
|
||||
* it must be prefixed using 7-bit length encoding (e.g.
|
||||
* {@code writePack7L})
|
||||
* @param jpegOutput the output stream where the modified JPEG will be written
|
||||
* @return the total number of bytes read from {@code payloadInput}, including
|
||||
* the prefix
|
||||
* @throws IOException if an I/O error occurs while reading the payload,
|
||||
* processing EXIF metadata, or writing the output JPEG
|
||||
*/
|
||||
public int embed(Path jpegPath, InputStream payloadInput, OutputStream jpegOutput) throws IOException {
|
||||
try {
|
||||
ImageMetadata metadata = Imaging.getMetadata(jpegPath.toFile());
|
||||
TiffImageMetadata exif = (metadata instanceof JpegImageMetadata jmeta) ? jmeta.getExif() : null;
|
||||
|
||||
TiffOutputSet outputSet = (exif != null) ? exif.getOutputSet() : new TiffOutputSet(ByteOrder.BIG_ENDIAN);
|
||||
|
||||
byte[] allBytes = Util.readWithPackedLengthPrefix(payloadInput, 1024 * 1024);
|
||||
int offset = 0;
|
||||
|
||||
for (Slot slot : slots) {
|
||||
boolean useBase64 = slot.tagInfo.isText();
|
||||
int chunkLimit = slot.defaultCapacity;
|
||||
|
||||
byte[] encoded;
|
||||
if (useBase64) {
|
||||
// base64 expands ~33%, so reduce raw chunk size
|
||||
int safeRawSize = chunkLimit * 3 / 4;
|
||||
int size = Math.min(safeRawSize, allBytes.length - offset);
|
||||
byte[] raw = new byte[size]; // NOPMD
|
||||
System.arraycopy(allBytes, offset, raw, 0, size);
|
||||
offset += size;
|
||||
|
||||
byte[] base64Bytes = Base64.getEncoder().encode(raw);
|
||||
String base64String = new String(base64Bytes, StandardCharsets.US_ASCII); // NOPMD
|
||||
|
||||
encoded = slot.tagInfo.encodeValue(slot.tagInfo.dataTypes.get(0), base64String,
|
||||
outputSet.byteOrder);
|
||||
} else {
|
||||
int size = Math.min(chunkLimit, allBytes.length - offset);
|
||||
byte[] chunk = new byte[size]; // NOPMD
|
||||
System.arraycopy(allBytes, offset, chunk, 0, size);
|
||||
offset += size;
|
||||
|
||||
encoded = slot.tagInfo.encodeValue(slot.tagInfo.dataTypes.get(0), chunk, outputSet.byteOrder);
|
||||
}
|
||||
|
||||
int directoryType = slot.tagInfo.directoryType.directoryType;
|
||||
TiffOutputDirectory directory = outputSet.findDirectory(directoryType);
|
||||
if (directory == null) {
|
||||
directory = new TiffOutputDirectory(directoryType, outputSet.byteOrder); // NOPMD
|
||||
outputSet.addDirectory(directory);
|
||||
}
|
||||
|
||||
TiffOutputField field = new TiffOutputField(slot.tagInfo, slot.tagInfo.dataTypes.get(0), encoded.length, // NOPMD
|
||||
encoded);
|
||||
|
||||
directory.removeField(slot.tagInfo); // Remove only if collides
|
||||
directory.add(field);
|
||||
|
||||
if (offset >= allBytes.length) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
new ExifRewriter().updateExifMetadataLossless(jpegPath.toFile(), jpegOutput, outputSet);
|
||||
|
||||
return allBytes.length;
|
||||
} catch (ImagingException e) {
|
||||
throw new IOException("Failed to process EXIF data", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts a binary payload that was previously embedded into the JPEG via EXIF
|
||||
* slots.
|
||||
*
|
||||
* @param jpegPath input JPEG path
|
||||
* @param payloadOutput stream to write the reconstructed binary payload
|
||||
* @throws IOException if the extraction fails
|
||||
*/
|
||||
public void extract(Path jpegPath, OutputStream payloadOutput) throws IOException {
|
||||
try {
|
||||
ImageMetadata metadata = Imaging.getMetadata(jpegPath.toFile());
|
||||
TiffImageMetadata exif = (metadata instanceof JpegImageMetadata jmeta) ? jmeta.getExif() : null;
|
||||
|
||||
if (exif == null) {
|
||||
LOG.warning("EXIF metadata not found in image.");
|
||||
return;
|
||||
}
|
||||
|
||||
Pack7LStreamWriter output = new Pack7LStreamWriter(payloadOutput);
|
||||
|
||||
for (Slot slot : slots) {
|
||||
TiffField field = exif.findField(slot.tagInfo);
|
||||
if (field == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
Object value = slot.tagInfo.getValue(field);
|
||||
byte[] chunk;
|
||||
|
||||
if (value instanceof byte[] binary) {
|
||||
chunk = slot.tagInfo.isText() ? Base64.getDecoder().decode(binary) : binary;
|
||||
|
||||
} else if (value instanceof String str) {
|
||||
chunk = slot.tagInfo.isText() ? Base64.getDecoder().decode(str.getBytes(StandardCharsets.US_ASCII))
|
||||
: str.getBytes(StandardCharsets.UTF_8);
|
||||
|
||||
} else {
|
||||
LOG.log(Level.WARNING, "Unsupported EXIF field value type for tag: {0}", slot.tagInfo.name);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (chunk.length > output.write(chunk)) {
|
||||
// all data has been read
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
} catch (ImagingException e) {
|
||||
throw new IOException("Failed to read EXIF from JPEG", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,302 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.covert.jpeg;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.Optional;
|
||||
import java.util.regex.Matcher;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
import org.apache.commons.imaging.formats.tiff.constants.ExifTagConstants;
|
||||
import org.apache.commons.imaging.formats.tiff.constants.TiffDirectoryType;
|
||||
import org.apache.commons.imaging.formats.tiff.fieldtypes.AbstractFieldType;
|
||||
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfo;
|
||||
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoAscii;
|
||||
import org.apache.commons.imaging.formats.tiff.taginfos.TagInfoUndefined;
|
||||
|
||||
/**
|
||||
* Represents a target slot in an EXIF/TIFF metadata directory for covert data
|
||||
* embedding.
|
||||
* <p>
|
||||
* A {@code Slot} defines where and how binary payloads may be hidden within the
|
||||
* structure of JPEG metadata. It supports both predefined slots (registered by
|
||||
* name) and fully custom slots specified via tag metadata.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* Slot specifications can be written as strings using the following syntax:
|
||||
* </p>
|
||||
*
|
||||
* <pre>{@code
|
||||
* [group.]name[/tag=tagId,type,count,dir][:capacity]
|
||||
* }</pre>
|
||||
*
|
||||
* <ul>
|
||||
* <li><b>group</b>: Optional slot group, one of EXIF, GPS, INTEROP, TIFF, or
|
||||
* THUMBNAIL. Default is EXIF.</li>
|
||||
* <li><b>name</b>: Slot name (must match a registered name or be part of a tag
|
||||
* definition).</li>
|
||||
* <li><b>tag=...</b>: Optional explicit tag definition for custom slots.</li>
|
||||
* <li><b>capacity</b>: Optional maximum capacity in bytes (default is
|
||||
* 1024).</li>
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* Predefined slots are stored in a registry and provide convenient aliases for
|
||||
* common EXIF tags. Custom slots can be created dynamically using tag
|
||||
* information and are parsed using the {@link #parse(String)} method.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* This class is immutable and not intended to be extended.
|
||||
* </p>
|
||||
*
|
||||
* @author Leo Galambos
|
||||
*/
|
||||
public final class Slot { // NOPMD
|
||||
private static final Map<String, Slot> REGISTRY = new HashMap<>();
|
||||
|
||||
static {
|
||||
register("usercomment", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096);
|
||||
register("makernote", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096);
|
||||
register("exifversion", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024);
|
||||
register("software", SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_SOFTWARE, 2048);
|
||||
register("interoptag", SlotGroup.INTEROP,
|
||||
new TagInfoUndefined("CustomInterop", 0xC4A6, TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD), 2048);
|
||||
register("thumbcomment", SlotGroup.THUMBNAIL,
|
||||
new TagInfoAscii("ThumbComment", 0xC4A7, 64, TiffDirectoryType.TIFF_DIRECTORY_IFD1), 1024);
|
||||
}
|
||||
|
||||
private static final Map<String, TiffDirectoryType> DIRECTORY_MAP = Map.ofEntries(
|
||||
Map.entry("ifd0", TiffDirectoryType.TIFF_DIRECTORY_IFD0),
|
||||
Map.entry("ifd1", TiffDirectoryType.TIFF_DIRECTORY_IFD1),
|
||||
Map.entry("ifd2", TiffDirectoryType.TIFF_DIRECTORY_IFD2),
|
||||
Map.entry("ifd3", TiffDirectoryType.TIFF_DIRECTORY_IFD3),
|
||||
Map.entry("exif", TiffDirectoryType.EXIF_DIRECTORY_EXIF_IFD),
|
||||
Map.entry("gps", TiffDirectoryType.EXIF_DIRECTORY_GPS),
|
||||
Map.entry("interop", TiffDirectoryType.EXIF_DIRECTORY_INTEROP_IFD),
|
||||
Map.entry("maker", TiffDirectoryType.EXIF_DIRECTORY_MAKER_NOTES),
|
||||
Map.entry("root", TiffDirectoryType.TIFF_DIRECTORY_ROOT),
|
||||
Map.entry("sub_ifd", TiffDirectoryType.EXIF_DIRECTORY_SUB_IFD));
|
||||
|
||||
private static final Map<String, AbstractFieldType> FIELD_TYPE_MAP = new HashMap<>();
|
||||
|
||||
static {
|
||||
for (AbstractFieldType t : AbstractFieldType.ANY) {
|
||||
FIELD_TYPE_MAP.put(t.getName().toLowerCase(Locale.ROOT), t);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logical groupings of EXIF/TIFF directories used to categorize slots.
|
||||
* <p>
|
||||
* Each group corresponds to a specific metadata section within an image file,
|
||||
* typically used in image metadata standards such as EXIF and TIFF.
|
||||
*/
|
||||
public enum SlotGroup {
|
||||
/**
|
||||
* EXIF metadata group, containing tags related to camera settings, image
|
||||
* capture information, and other extended image metadata defined by the EXIF
|
||||
* standard.
|
||||
*/
|
||||
EXIF,
|
||||
/**
|
||||
* TIFF metadata group, which includes baseline TIFF tags such as image
|
||||
* dimensions, compression type, and color format.
|
||||
*/
|
||||
TIFF,
|
||||
/**
|
||||
* GPS metadata group, storing geolocation information like latitude, longitude,
|
||||
* altitude, and GPS timestamp.
|
||||
*/
|
||||
GPS,
|
||||
/**
|
||||
* Interoperability metadata group, used to ensure compatibility across
|
||||
* different file formats or devices. Typically contains a few specific tags.
|
||||
*/
|
||||
INTEROP,
|
||||
/**
|
||||
* Thumbnail metadata group, storing metadata and image data for embedded
|
||||
* preview thumbnails.
|
||||
*/
|
||||
THUMBNAIL
|
||||
}
|
||||
|
||||
/**
|
||||
* The logical group (e.g., EXIF, GPS, TIFF) this slot belongs to.
|
||||
*/
|
||||
public final SlotGroup group;
|
||||
/**
|
||||
* Metadata tag info describing the structure of this slot.
|
||||
*/
|
||||
public final TagInfo tagInfo;
|
||||
/**
|
||||
* The default capacity, in bytes, of this slot for data embedding.
|
||||
*/
|
||||
public final int defaultCapacity;
|
||||
|
||||
private Slot(SlotGroup group, TagInfo tagInfo, int defaultCapacity) {
|
||||
this.group = group;
|
||||
this.tagInfo = tagInfo;
|
||||
this.defaultCapacity = defaultCapacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a named predefined slot in the internal registry. This allows slots
|
||||
* to be referenced later by name in shorthand form.
|
||||
*
|
||||
* @param name the lowercase name used for lookup
|
||||
* @param group the logical slot group
|
||||
* @param tagInfo the tag metadata for the slot
|
||||
* @param defaultCapacity the default maximum capacity for this slot in bytes
|
||||
* @return the registered {@code Slot} instance
|
||||
*/
|
||||
public static Slot register(String name, SlotGroup group, TagInfo tagInfo, int defaultCapacity) {
|
||||
Slot slot = new Slot(group, tagInfo, defaultCapacity);
|
||||
REGISTRY.put(name.toLowerCase(Locale.ROOT), slot);
|
||||
return slot;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses a slot specification string into a {@code Slot} object.
|
||||
* <p>
|
||||
* The input may refer to a predefined slot by name, or a custom slot defined
|
||||
* with full tag details. See class-level documentation for the full
|
||||
* specification syntax.
|
||||
* </p>
|
||||
*
|
||||
* @param spec the slot specification string
|
||||
* @return a {@code Slot} representing the parsed definition
|
||||
* @throws IllegalArgumentException if the input format is invalid or references
|
||||
* unknown components
|
||||
*/
|
||||
public static Slot parse(String spec) {
|
||||
Pattern pattern = Pattern.compile(
|
||||
"(?:(\\w+)\\.)?(\\w+)(?:/tag=([\\d]+),([a-z0-9_]+),(\\d+),(\\w+))?(?::(\\d+))?",
|
||||
Pattern.CASE_INSENSITIVE);
|
||||
Matcher m = pattern.matcher(spec);
|
||||
if (!m.matches()) {
|
||||
throw new IllegalArgumentException("Invalid slot spec: " + spec);
|
||||
}
|
||||
|
||||
String groupStr = m.group(1);
|
||||
String name = m.group(2);
|
||||
String tagIdStr = m.group(3);
|
||||
String typeStr = m.group(4);
|
||||
String countStr = m.group(5);
|
||||
String dirStr = m.group(6);
|
||||
String sizeStr = m.group(7);
|
||||
|
||||
SlotGroup group = groupStr != null ? SlotGroup.valueOf(groupStr.toUpperCase(Locale.ROOT)) : SlotGroup.EXIF;
|
||||
int size = sizeStr != null ? Integer.parseInt(sizeStr) : 1024;
|
||||
|
||||
if (tagIdStr == null) {
|
||||
Slot predefined = REGISTRY.get(name.toLowerCase(Locale.ROOT));
|
||||
if (predefined == null) {
|
||||
throw new IllegalArgumentException("No predefined slot found: " + name);
|
||||
}
|
||||
return predefined;
|
||||
}
|
||||
|
||||
int tagId = Integer.parseInt(tagIdStr);
|
||||
int count = Integer.parseInt(countStr);
|
||||
|
||||
TiffDirectoryType dir = Optional.ofNullable(DIRECTORY_MAP.get(dirStr.toLowerCase(Locale.ROOT)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unknown directory: " + dirStr));
|
||||
|
||||
AbstractFieldType fieldType = Optional.ofNullable(FIELD_TYPE_MAP.get(typeStr.toLowerCase(Locale.ROOT)))
|
||||
.orElseThrow(() -> new IllegalArgumentException("Unsupported type: " + typeStr));
|
||||
|
||||
TagInfo tagInfo = new TagInfo(name, tagId, fieldType, count, dir);
|
||||
return new Slot(group, tagInfo, size);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a human-readable representation of the slot, including its tag name
|
||||
* and capacity.
|
||||
*
|
||||
* @return string representation of the slot
|
||||
*/
|
||||
@Override
|
||||
public String toString() {
|
||||
return tagInfo.name + ":" + defaultCapacity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares this slot to another for equality based on tag ID and directory
|
||||
* type.
|
||||
*
|
||||
* @param o the object to compare
|
||||
* @return true if the other object is a slot with the same tag and directory
|
||||
*/
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Slot slot = (Slot) o;
|
||||
return tagInfo.tag == slot.tagInfo.tag && tagInfo.directoryType == slot.tagInfo.directoryType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes a hash code based on the slot's tag and directory.
|
||||
*
|
||||
* @return the hash code
|
||||
*/
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(tagInfo.tag, tagInfo.directoryType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of default slot configurations commonly available in standard
|
||||
* EXIF metadata. These slots are selected for compatibility and relatively high
|
||||
* storage capacity.
|
||||
*
|
||||
* @return list of default {@code Slot} instances
|
||||
*/
|
||||
public static List<Slot> defaults() {
|
||||
return List.of(new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_USER_COMMENT, 4096),
|
||||
new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_MAKER_NOTE, 4096),
|
||||
new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_EXIF_VERSION, 1024),
|
||||
new Slot(SlotGroup.EXIF, ExifTagConstants.EXIF_TAG_SOFTWARE, 2048));
|
||||
}
|
||||
}
|
||||
@@ -1,113 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Covert data embedding and extraction using JPEG EXIF metadata.
|
||||
*
|
||||
* <p>
|
||||
* This package provides utilities for hiding and recovering binary payloads
|
||||
* inside EXIF/TIFF fields of JPEG images. The embedder preserves existing
|
||||
* metadata where possible and distributes the payload across a caller-defined
|
||||
* sequence of slots, transparently applying Base64 for text-only tags. The
|
||||
* implementation performs lossless EXIF updates using a JPEG metadata library.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Key elements</h2>
|
||||
* <ul>
|
||||
* <li>{@link JpegExifEmbedder} - embeds a length-prefixed binary payload into a
|
||||
* JPEG by splitting it across configured slots and writes the result using a
|
||||
* lossless EXIF rewrite; also reconstructs the payload by reading the same
|
||||
* slots in order.</li>
|
||||
* <li>{@link Slot} - an immutable description of one EXIF/TIFF field with a
|
||||
* default capacity and grouping; supports a compact specification syntax and a
|
||||
* small registry of predefined, high-capacity tags.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Slot configuration</h2>
|
||||
* <p>
|
||||
* Slots identify where and in what order payload chunks are written or read.
|
||||
* They can be referenced by predefined name (for example, {@code usercomment},
|
||||
* {@code makernote}) or specified explicitly using tag metadata. The textual
|
||||
* format is:
|
||||
* </p>
|
||||
* <pre>{@code
|
||||
* [group.]name[/tag=tagId,type,count,dir][:capacity]
|
||||
* }</pre>
|
||||
* <ul>
|
||||
* <li><b>group</b>: EXIF, GPS, INTEROP, TIFF, or THUMBNAIL (default EXIF).</li>
|
||||
* <li><b>name</b>: registry key or custom tag name.</li>
|
||||
* <li><b>tag=...</b>: explicit tag definition for a custom slot.</li>
|
||||
* <li><b>capacity</b>: maximum bytes for this slot (default 1024).</li>
|
||||
* </ul>
|
||||
* <p>
|
||||
* Use {@link Slot#parse(String)} for custom definitions,
|
||||
* {@link Slot#register(String, Slot.SlotGroup, org.apache.commons.imaging.formats.tiff.taginfos.TagInfo, int)}
|
||||
* to extend the registry, and {@link Slot#defaults()} to obtain a conservative
|
||||
* default set.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Payload format and processing</h2>
|
||||
* <ul>
|
||||
* <li><b>Length prefix</b>: Embedding reads the payload using a 7-bit
|
||||
* variable-length size prefix, then splits the bytes across slots in order;
|
||||
* extraction writes the same prefix back before streaming the reconstructed
|
||||
* bytes.</li>
|
||||
* <li><b>Text vs binary slots</b>: When a slot is text-only, the embedder
|
||||
* Base64-encodes the chunk before storing; binary-capable slots store raw
|
||||
* bytes.</li>
|
||||
* <li><b>Metadata preservation</b>: Existing EXIF is retained except for fields
|
||||
* explicitly overwritten by the chosen slots; updates are written
|
||||
* losslessly.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Thread-safety</h2>
|
||||
* <ul>
|
||||
* <li>{@link JpegExifEmbedder} maintains a mutable, ordered slot list and is
|
||||
* not intended for concurrent reconfiguration.</li>
|
||||
* <li>{@link Slot} instances are immutable and safe to share across
|
||||
* threads.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Use cases and extensions</h2>
|
||||
* <ul>
|
||||
* <li>Transport encrypted streams disguised within benign JPEG metadata.</li>
|
||||
* <li>Covert carriers for small secrets with compatibility across common image
|
||||
* tools.</li>
|
||||
* <li>Future extension: combine with exporters or steganographic pipelines in
|
||||
* other packages to automate deployment to public platforms while keeping
|
||||
* payloads concealed.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.integrations.covert.jpeg;
|
||||
@@ -1,81 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Helpers for generating pseudo-random cover text using character frequency
|
||||
* tables.
|
||||
*
|
||||
* <p>
|
||||
* This package provides utilities for producing benign-looking textual fillers
|
||||
* that mimic the statistical properties of a target language. The primary use
|
||||
* is to generate cover text for covert or steganographic workflows where
|
||||
* ciphertext or opaque data needs a plausible textual carrier. These helpers do
|
||||
* not offer confidentiality by themselves; combine them with encryption when
|
||||
* handling secrets.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Key elements</h2>
|
||||
* <ul>
|
||||
* <li>{@link TextualCodec} - utility container for text-oriented helpers.</li>
|
||||
* <li>{@link TextualCodec.Generator} - frequency-driven character generator
|
||||
* with a default English distribution and APIs to produce single characters or
|
||||
* fixed-length strings.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Typical usage</h2> <pre>{@code
|
||||
* // Use the built-in English distribution to create a short cover text.
|
||||
* zeroecho.sdk.integrations.covert.TextualCodec.Generator gen =
|
||||
* zeroecho.sdk.integrations.covert.TextualCodec.Generator.EN;
|
||||
* String sample = gen.getText(256);
|
||||
*
|
||||
* // Or construct a custom distribution.
|
||||
* java.util.Map<Character, Double> freq = java.util.Map.ofEntries(
|
||||
* java.util.Map.entry('e', 12.7), java.util.Map.entry('t', 9.1),
|
||||
* java.util.Map.entry('a', 8.2), java.util.Map.entry(' ', 25.4)
|
||||
* );
|
||||
* zeroecho.sdk.integrations.covert.TextualCodec.Generator custom =
|
||||
* new zeroecho.sdk.integrations.covert.TextualCodec.Generator(freq);
|
||||
* String cover = custom.getText(128);
|
||||
* }</pre>
|
||||
*
|
||||
* <h2>Notes</h2>
|
||||
* <ul>
|
||||
* <li>Generated text is intended as camouflage, not as a security control.</li>
|
||||
* <li>Distributions should be non-negative and representative of the intended
|
||||
* cover domain.</li>
|
||||
* <li>Instances are not thread-safe; use a separate generator per thread.</li>
|
||||
* </ul>
|
||||
*
|
||||
* @since 1.0
|
||||
*/
|
||||
package zeroecho.sdk.integrations.covert;
|
||||
@@ -1,70 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
|
||||
/**
|
||||
* Enumeration of image formats supported for steganographic processing.
|
||||
*
|
||||
* <p>
|
||||
* These formats are chosen because they provide predictable binary layouts and
|
||||
* are commonly used in practical steganographic embedding and extraction.
|
||||
* </p>
|
||||
*/
|
||||
public enum ImageFormat {
|
||||
/**
|
||||
* Portable Network Graphics format.
|
||||
* <p>
|
||||
* PNG uses lossless compression, which makes it suitable for precise bit-level
|
||||
* embedding without introducing distortion into the carrier image.
|
||||
* </p>
|
||||
*/
|
||||
PNG,
|
||||
/**
|
||||
* Bitmap format.
|
||||
* <p>
|
||||
* BMP is an uncompressed raster format. Its simple and direct structure makes
|
||||
* it reliable for low-level manipulation during steganographic processing.
|
||||
* </p>
|
||||
*/
|
||||
BMP,
|
||||
/**
|
||||
* Joint Photographic Experts Group format.
|
||||
* <p>
|
||||
* JPEG is a lossy format based on DCT (Discrete Cosine Transform) blocks.
|
||||
* Steganographic methods for JPEG must work safely within the compression
|
||||
* domain to preserve image integrity while embedding data.
|
||||
* </p>
|
||||
*/
|
||||
JPEG
|
||||
}
|
||||
@@ -1,253 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.util.Locale;
|
||||
import java.util.logging.Level;
|
||||
import java.util.logging.Logger;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
|
||||
/**
|
||||
* Least Significant Bit steganography implementation operating in the spatial
|
||||
* domain.
|
||||
*
|
||||
* <p>
|
||||
* This class embeds and extracts message bits from image pixels by manipulating
|
||||
* the least significant bit of each grayscale value. The implementation uses
|
||||
* stream-based I/O to support large inputs without loading everything into
|
||||
* memory at once.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Algorithm and characteristics</h2>
|
||||
* <p>
|
||||
* Embedding proceeds left-to-right, top-to-bottom across pixels. The message is
|
||||
* prefixed with a 4-byte length header (big-endian) and then written bit-by-bit
|
||||
* into the LSB of each pixel's grayscale value. During embedding the image
|
||||
* content is converted to grayscale and written back to all color channels,
|
||||
* which discards original chroma.
|
||||
* </p>
|
||||
* <p>
|
||||
* Extraction reads the first 32 bits to recover the message length and then
|
||||
* continues to read that many message bytes from subsequent pixel LSBs.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Capacity</h2>
|
||||
* <p>
|
||||
* Effective capacity is {@code width * height - 32} bits, where 32 bits are
|
||||
* reserved for the length header. The caller must ensure that the message
|
||||
* length in bits does not exceed the available capacity; otherwise the embedded
|
||||
* payload will be incomplete.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Format support</h2>
|
||||
* <p>
|
||||
* Output must be a lossless format such as PNG or BMP. JPEG is not supported
|
||||
* because its lossy compression would destroy LSB-embedded payloads.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Usage</h2> <pre>{@code
|
||||
* SteganographyMethod method = new LSBSteganographyMethod();
|
||||
* try (InputStream carrier = Files.newInputStream(Path.of("cover.png"));
|
||||
* InputStream secret = new ByteArrayInputStream("hello".getBytes(StandardCharsets.UTF_8));
|
||||
* InputStream stego = method.embed(carrier, ImageFormat.PNG, secret)) {
|
||||
* Files.write(Path.of("stego.png"), stego.readAllBytes());
|
||||
* }
|
||||
*
|
||||
* try (InputStream stego = Files.newInputStream(Path.of("stego.png"));
|
||||
* InputStream recovered = method.extract(stego)) {
|
||||
* String text = new String(recovered.readAllBytes(), StandardCharsets.UTF_8);
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
public class LSBSteganographyMethod implements SteganographyMethod {
|
||||
private static final Logger LOG = Logger.getLogger(LSBSteganographyMethod.class.getName());
|
||||
|
||||
/**
|
||||
* Embeds the provided message into the least significant bits of the image and
|
||||
* returns a new image stream in the requested lossless format.
|
||||
*
|
||||
* <p>
|
||||
* The output image is grayscale and may differ visually from the input because
|
||||
* the algorithm writes identical gray values to all RGB channels. The message
|
||||
* is stored as a 4-byte big-endian length prefix followed by the raw message
|
||||
* bytes.
|
||||
* </p>
|
||||
*
|
||||
* @param inputImage the source image stream to use as the cover image
|
||||
* @param outputFormat the desired output format; must be lossless (for example,
|
||||
* {@code PNG} or {@code BMP})
|
||||
* @param message the message stream to embed
|
||||
* @return an {@code InputStream} containing the stego image encoded in
|
||||
* {@code outputFormat}
|
||||
* @throws IOException if an I/O error occurs while reading the
|
||||
* image or message, or while writing the
|
||||
* output image
|
||||
* @throws IllegalArgumentException if {@code outputFormat} is unsupported for
|
||||
* this method (for example, {@code JPEG})
|
||||
*/
|
||||
@Override
|
||||
public InputStream embed(InputStream inputImage, ImageFormat outputFormat, InputStream message) throws IOException {
|
||||
if (outputFormat == ImageFormat.JPEG) {
|
||||
throw new IllegalArgumentException("LSB does not support lossy formats like JPEG.");
|
||||
}
|
||||
|
||||
BufferedImage img = ImageIO.read(inputImage);
|
||||
if (img == null) {
|
||||
throw new IOException("Failed to read input image.");
|
||||
}
|
||||
|
||||
byte[] messageBytes = message.readAllBytes();
|
||||
ByteArrayOutputStream fullData = new ByteArrayOutputStream();
|
||||
DataOutputStream dos = new DataOutputStream(fullData);
|
||||
dos.writeInt(messageBytes.length);
|
||||
dos.write(messageBytes);
|
||||
byte[] fullMessage = fullData.toByteArray();
|
||||
|
||||
final int width = img.getWidth();
|
||||
final int height = img.getHeight();
|
||||
int msgBitIndex = 0;
|
||||
|
||||
if (LOG.isLoggable(Level.INFO)) {
|
||||
LOG.log(Level.INFO, "embed to {2} picture w={0} x h={1}", new Object[] { width, height, outputFormat });
|
||||
}
|
||||
|
||||
outer: for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
if (msgBitIndex >= fullMessage.length * 8) {
|
||||
break outer;
|
||||
}
|
||||
|
||||
int rgb = img.getRGB(x, y);
|
||||
int gray = rgb & 0xFF;
|
||||
int bit = (fullMessage[msgBitIndex / 8] >> (7 - (msgBitIndex % 8))) & 1;
|
||||
gray = (gray & 0xFE) | bit;
|
||||
int newRgb = (gray << 16) | (gray << 8) | gray;
|
||||
img.setRGB(x, y, newRgb);
|
||||
msgBitIndex++;
|
||||
}
|
||||
}
|
||||
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
ImageIO.write(img, outputFormat.name().toLowerCase(Locale.ROOT), out);
|
||||
return new ByteArrayInputStream(out.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the embedded message from the least significant bits of the provided
|
||||
* image.
|
||||
*
|
||||
* <p>
|
||||
* This method expects the message to be stored as a 4-byte big-endian length
|
||||
* prefix followed by exactly that many message bytes. The input should be a
|
||||
* lossless image produced by
|
||||
* {@link #embed(InputStream, ImageFormat, InputStream)} or an equivalent LSB
|
||||
* spatial-domain encoder.
|
||||
* </p>
|
||||
*
|
||||
* @param inputImage the stego image stream that contains an embedded message
|
||||
* @return an {@code InputStream} of the extracted message bytes
|
||||
* @throws IOException if an I/O error occurs while reading the image or if the
|
||||
* image cannot be decoded
|
||||
*/
|
||||
@Override
|
||||
public InputStream extract(InputStream inputImage) throws IOException {
|
||||
BufferedImage img = ImageIO.read(inputImage);
|
||||
if (img == null) {
|
||||
throw new IOException("Failed to read input image.");
|
||||
}
|
||||
|
||||
final int width = img.getWidth();
|
||||
final int height = img.getHeight();
|
||||
ByteArrayOutputStream messageBytes = new ByteArrayOutputStream();
|
||||
|
||||
if (LOG.isLoggable(Level.INFO)) {
|
||||
LOG.log(Level.INFO, "extract from picture w={0} x h={1}", new Object[] { width, height });
|
||||
}
|
||||
|
||||
int byteVal = 0;
|
||||
int messageLength = -1;
|
||||
int bitsCollected = 0;
|
||||
|
||||
outer: for (int y = 0; y < height; y++) {
|
||||
for (int x = 0; x < width; x++) {
|
||||
int gray = img.getRGB(x, y) & 0xFF;
|
||||
int bit = gray & 1;
|
||||
byteVal = (byteVal << 1) | bit;
|
||||
bitsCollected++;
|
||||
|
||||
if (bitsCollected == 8) { // NOPMD
|
||||
messageBytes.write(byteVal);
|
||||
byteVal = 0;
|
||||
bitsCollected = 0;
|
||||
|
||||
if (messageLength == -1 && messageBytes.size() == 4) {
|
||||
byte[] lenBytes = messageBytes.toByteArray();
|
||||
messageBytes.reset();
|
||||
messageLength = ByteBuffer.wrap(lenBytes).getInt();
|
||||
}
|
||||
|
||||
if (messageLength != -1 && messageBytes.size() == messageLength) {
|
||||
break outer;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new ByteArrayInputStream(messageBytes.toByteArray());
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns metadata describing this LSB spatial-domain steganography method.
|
||||
*
|
||||
* <p>
|
||||
* The metadata includes a short identifier, a descriptive long name, and a
|
||||
* concise description of the embedding approach.
|
||||
* </p>
|
||||
*
|
||||
* @return method metadata for LSB spatial-domain steganography
|
||||
*/
|
||||
@Override
|
||||
public StegoMetadata getMetadata() {
|
||||
return new StegoMetadata("LSB", "Least Significant Bit Spatial Domain Image Steganography",
|
||||
"Embeds message bits into the least significant bits of grayscale pixel values.");
|
||||
}
|
||||
}
|
||||
@@ -1,109 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* Contract for stream-based steganographic methods.
|
||||
*
|
||||
* <p>
|
||||
* Implementations of this interface provide algorithms to hide and recover
|
||||
* messages in digital images. Streams are used for both image data and message
|
||||
* payloads to support large files and efficient, pipeline-friendly processing.
|
||||
* </p>
|
||||
*
|
||||
* <p>
|
||||
* A typical implementation will consume an input image stream, apply an
|
||||
* embedding or extraction algorithm, and return a new stream without requiring
|
||||
* the entire image to reside in memory.
|
||||
* </p>
|
||||
*/
|
||||
public interface SteganographyMethod {
|
||||
|
||||
/**
|
||||
* Embeds a message into a source image and produces a new image in the
|
||||
* requested format.
|
||||
*
|
||||
* <p>
|
||||
* The caller provides an input image, a message stream, and specifies the
|
||||
* desired output format. The implementation applies its embedding algorithm and
|
||||
* returns an {@link InputStream} representing the new image.
|
||||
* </p>
|
||||
*
|
||||
* @param inputImage the source image stream; format may vary depending on
|
||||
* implementation capabilities
|
||||
* @param outputFormat the desired format for the resulting image; must be
|
||||
* supported and generally lossless (for example,
|
||||
* {@link ImageFormat#PNG})
|
||||
* @param message the stream containing the message to be embedded in the
|
||||
* image
|
||||
* @return an input stream of the new image containing the embedded message,
|
||||
* encoded in the requested format
|
||||
* @throws IOException if an I/O error occurs while reading or
|
||||
* writing streams
|
||||
* @throws IllegalArgumentException if the specified output format is not
|
||||
* supported by this method
|
||||
*/
|
||||
InputStream embed(InputStream inputImage, ImageFormat outputFormat, InputStream message) throws IOException;
|
||||
|
||||
/**
|
||||
* Extracts a hidden message from an image.
|
||||
*
|
||||
* <p>
|
||||
* The caller supplies an image stream that contains an embedded message. The
|
||||
* implementation applies its extraction algorithm and returns the recovered
|
||||
* message as a new stream.
|
||||
* </p>
|
||||
*
|
||||
* @param inputImage the image stream containing an embedded message; must be in
|
||||
* a format compatible with this method
|
||||
* @return an input stream of the extracted message data
|
||||
* @throws IOException if an I/O error occurs while reading or writing streams
|
||||
*/
|
||||
InputStream extract(InputStream inputImage) throws IOException;
|
||||
|
||||
/**
|
||||
* Returns metadata describing this steganographic method.
|
||||
*
|
||||
* <p>
|
||||
* The metadata includes a short identifier, a descriptive name, and a textual
|
||||
* explanation of the embedding technique.
|
||||
* </p>
|
||||
*
|
||||
* @return the metadata object associated with this steganographic method
|
||||
*/
|
||||
StegoMetadata getMetadata();
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
|
||||
/**
|
||||
* Metadata that describes a steganographic method.
|
||||
*
|
||||
* <p>
|
||||
* Each instance carries both a short identifier and a more descriptive
|
||||
* technical name, together with a human-readable explanation of the embedding
|
||||
* approach. This record is typically used for discovery, logging, or user
|
||||
* interfaces that need to present details about available steganographic
|
||||
* techniques.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Examples</h2> <pre>{@code
|
||||
* StegoMetadata meta = new StegoMetadata(
|
||||
* "LSB",
|
||||
* "Least Significant Bit Embedding",
|
||||
* "Encodes hidden data into the least significant bits of pixel values"
|
||||
* );
|
||||
* }</pre>
|
||||
*
|
||||
* @param name method identifier, for example {@code "LSB"} or
|
||||
* {@code "DCT"}
|
||||
* @param fullName full technical name of the steganographic method
|
||||
* @param description short explanation of how the method works
|
||||
*/
|
||||
public record StegoMetadata(String name, String fullName, String description) {
|
||||
}
|
||||
@@ -1,84 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
/**
|
||||
* Steganographic integrations for ZeroEcho SDK.
|
||||
*
|
||||
* <p>
|
||||
* This package provides abstractions and implementations for hiding and
|
||||
* recovering messages in digital images. Steganography allows information to be
|
||||
* concealed in carrier media so that its presence is not apparent to a casual
|
||||
* observer.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Design principles</h2>
|
||||
* <ul>
|
||||
* <li>All algorithms implement the
|
||||
* {@link zeroecho.sdk.integrations.stegano.SteganographyMethod} interface,
|
||||
* which defines stream-oriented {@code embed} and {@code extract} operations
|
||||
* and supplies a metadata descriptor.</li>
|
||||
* <li>Supported carrier formats are represented by
|
||||
* {@link zeroecho.sdk.integrations.stegano.ImageFormat}. Only lossless formats
|
||||
* are suitable for direct bit-level embedding, but some algorithms may also
|
||||
* provide support for lossy domains such as JPEG with dedicated
|
||||
* techniques.</li>
|
||||
* <li>Methods are designed to operate on {@link java.io.InputStream} and
|
||||
* {@link java.io.OutputStream} pipelines, making them compatible with large
|
||||
* files and streaming workflows.</li>
|
||||
* </ul>
|
||||
*
|
||||
* <h2>Extensibility</h2>
|
||||
* <p>
|
||||
* The package is not limited to one specific algorithm. In addition to classic
|
||||
* spatial-domain approaches, implementors can provide methods that operate in
|
||||
* the frequency domain (for example, DCT coefficients for JPEG) or more
|
||||
* advanced hybrid techniques. New methods can be registered by implementing the
|
||||
* {@code SteganographyMethod} interface and returning appropriate
|
||||
* {@link zeroecho.sdk.integrations.stegano.StegoMetadata}.
|
||||
* </p>
|
||||
*
|
||||
* <h2>Typical workflow</h2> <pre>{@code
|
||||
* SteganographyMethod method = ...; // e.g. resolved from a registry
|
||||
* try (InputStream carrier = Files.newInputStream(Path.of("cover.png"));
|
||||
* InputStream secret = new ByteArrayInputStream("hello".getBytes(UTF_8));
|
||||
* InputStream stego = method.embed(carrier, ImageFormat.PNG, secret)) {
|
||||
* Files.write(Path.of("stego.png"), stego.readAllBytes());
|
||||
* }
|
||||
*
|
||||
* try (InputStream stego = Files.newInputStream(Path.of("stego.png"));
|
||||
* InputStream recovered = method.extract(stego)) {
|
||||
* String text = new String(recovered.readAllBytes(), UTF_8);
|
||||
* }
|
||||
* }</pre>
|
||||
*/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
@@ -1,112 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.content.export;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertArrayEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
class Base64StreamTest {
|
||||
|
||||
@Test
|
||||
void testEncodedOutputFrom4KBInput() throws Exception {
|
||||
System.out.println("testEncodedOutputFrom4KBInput");
|
||||
|
||||
byte[] inputBytes = new byte[4096];
|
||||
new SecureRandom().nextBytes(inputBytes); // random 4KB data
|
||||
|
||||
byte[] inputEncoded = Base64.getEncoder().encode(inputBytes);
|
||||
|
||||
System.out.println("...encoded length should be: " + inputEncoded.length);
|
||||
|
||||
int lineLength = 76;
|
||||
|
||||
String[] prefixes = { "echo ", null };
|
||||
byte[][] lineSeparators = { " >> file.tmp\r\n".getBytes(StandardCharsets.US_ASCII),
|
||||
"\n".getBytes(StandardCharsets.US_ASCII), null };
|
||||
|
||||
for (String prefix : prefixes) {
|
||||
for (byte[] lineSep : lineSeparators) {
|
||||
System.out.printf("...***** testing with prefix=%s and lineSep=%s%n",
|
||||
prefix == null ? "null" : "\"" + prefix + "\"",
|
||||
lineSep == null ? "null" : "\"" + new String(lineSep, StandardCharsets.US_ASCII) + "\"");
|
||||
|
||||
try (InputStream base64Stream = new Base64Stream(new ByteArrayInputStream(inputBytes),
|
||||
prefix == null ? null : prefix.getBytes(StandardCharsets.US_ASCII), lineLength, lineSep)) {
|
||||
|
||||
String encodedOutput = new String(base64Stream.readAllBytes(), StandardCharsets.US_ASCII);
|
||||
System.out.println(encodedOutput);
|
||||
|
||||
String[] lines = encodedOutput.split("\n");
|
||||
for (int i = 0; i < lines.length; i++) {
|
||||
int space = lines[i].indexOf(' ');
|
||||
if (space > 0) {
|
||||
lines[i] = lines[i].split(" ")[prefix == null ? 0 : 1];
|
||||
}
|
||||
}
|
||||
|
||||
if (lineSep != null) {
|
||||
for (String line : lines) {
|
||||
assertTrue(line.length() <= lineLength, "Line exceeds max length: " + line.length());
|
||||
}
|
||||
}
|
||||
|
||||
String joined = String.join("", lines);
|
||||
|
||||
System.out.println("...lines: " + lines.length);
|
||||
System.out.println("......encoded length: " + joined.length());
|
||||
|
||||
if (lineSep == null && prefix != null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
byte[] decoded = Base64.getDecoder().decode(joined);
|
||||
|
||||
System.out.println("...decoded length: " + decoded.length);
|
||||
|
||||
assertArrayEquals(inputBytes, decoded, "Decoded content does not match original input");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
System.out.println("...ok");
|
||||
}
|
||||
}
|
||||
@@ -1,74 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.covert;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class TextualCodecTest {
|
||||
|
||||
@Test
|
||||
void testGenerate100CharacterText() {
|
||||
System.out.println("testGenerate100CharacterText");
|
||||
|
||||
TextualCodec.Generator generator = TextualCodec.Generator.EN;
|
||||
String result = generator.getText(100);
|
||||
|
||||
System.out.println("...generated text: " + result);
|
||||
|
||||
assertNotNull(result, "Generated text should not be null");
|
||||
assertEquals(100, result.length(), "Generated text should have 100 characters");
|
||||
|
||||
// Ensure all characters are from the expected alphabet
|
||||
for (char c : result.toCharArray()) {
|
||||
assertTrue(TextualCodec.Generator.ENGLISH.containsKey(c),
|
||||
"Generated character '" + c + "' is not part of the English frequency table");
|
||||
}
|
||||
|
||||
// Optional: Analyze distribution for debugging
|
||||
Map<Character, Integer> histogram = new HashMap<>();
|
||||
for (char c : result.toCharArray()) {
|
||||
histogram.merge(c, 1, Integer::sum);
|
||||
}
|
||||
|
||||
System.out.println("...character distribution: " + histogram);
|
||||
System.out.println("...ok");
|
||||
}
|
||||
}
|
||||
@@ -1,185 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.covert.jpeg;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.Arrays;
|
||||
|
||||
import javax.crypto.SecretKey;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
|
||||
import conflux.Ctx;
|
||||
import conflux.CtxInterface;
|
||||
import zeroecho.core.CryptoAlgorithms;
|
||||
import zeroecho.core.alg.aes.AesKeyGenSpec;
|
||||
import zeroecho.core.alg.aes.AesSpec;
|
||||
import zeroecho.sdk.builders.alg.AesDataContentBuilder;
|
||||
import zeroecho.sdk.builders.core.DataContentChainBuilder;
|
||||
import zeroecho.sdk.builders.core.PlainBytesBuilder;
|
||||
import zeroecho.sdk.content.api.DataContent;
|
||||
|
||||
class JpegExifIntegrationTest {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private static byte[] readAll(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
in.transferTo(out);
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@Test
|
||||
void testEncryptEmbedExtractDecrypt() throws Exception {
|
||||
System.out.println("testEncryptEmbedExtractDecrypt");
|
||||
|
||||
String inputText = "Hello, this is a secret message to embed in JPEG.";
|
||||
|
||||
inputText = inputText + inputText;
|
||||
inputText = inputText + inputText;
|
||||
inputText = inputText + inputText;
|
||||
|
||||
final byte[] inputBytes = inputText.getBytes(StandardCharsets.UTF_8);
|
||||
final byte[] aad = { 1, 2, 3 };
|
||||
|
||||
System.out.println("...inputBytes.length=" + inputBytes.length);
|
||||
|
||||
// AES encryption setup
|
||||
/*
|
||||
* CryptoAlgorithm aes = CryptoAlgorithms.require("AES"); SecretKey key =
|
||||
* aes.symmetricKeyBuilder(AesKeyGenSpec.class).generateSecret(AesKeyGenSpec.
|
||||
* aes256()); AesSpec spec =
|
||||
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
|
||||
* EncryptionContext enc = CryptoAlgorithms.create("AES", KeyUsage.ENCRYPT, key,
|
||||
* spec); CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" +
|
||||
* System.nanoTime()); session.put(ConfluxKeys.aad("AES"), aad); ((ContextAware)
|
||||
* enc).setContext(session);
|
||||
*/
|
||||
SecretKey key = CryptoAlgorithms.require("AES").symmetricKeyBuilder(AesKeyGenSpec.class)
|
||||
.generateSecret(AesKeyGenSpec.aes256());
|
||||
CtxInterface session = Ctx.INSTANCE.getContext("aes-ctx-" + System.nanoTime());
|
||||
|
||||
byte[] encryptedBytes;
|
||||
DataContent dccb = DataContentChainBuilder.encrypt()
|
||||
// input
|
||||
.add(PlainBytesBuilder.builder().bytes(inputBytes))
|
||||
// encryption
|
||||
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded())
|
||||
// using general AES/GCM/128 without specified header
|
||||
.spec(AesSpec.gcm128(null))
|
||||
// but let the builder add the default header for storing AAD and IV
|
||||
.withHeader().withAad(aad).context(session))
|
||||
// and create the pipeline
|
||||
.build();
|
||||
try (InputStream in = dccb.getStream()) {
|
||||
encryptedBytes = readAll(in);
|
||||
}
|
||||
|
||||
System.out.println("...encryptedBytes.length=" + encryptedBytes.length);
|
||||
System.out.println("...-> " + Arrays.toString(encryptedBytes));
|
||||
|
||||
// Prepare JPEG test image
|
||||
Path jpegOriginal = getResourcePath("test.jpg");
|
||||
Path jpegEmbedded = tempDir.resolve("stego.jpg");
|
||||
|
||||
// Embed payload
|
||||
try (InputStream payloadInput = new ByteArrayInputStream(encryptedBytes);
|
||||
OutputStream jpegOutput = Files.newOutputStream(jpegEmbedded)) {
|
||||
|
||||
JpegExifEmbedder embedder = new JpegExifEmbedder();
|
||||
embedder.setSlots(Slot.defaults());
|
||||
int embed_size = embedder.embed(jpegOriginal, payloadInput, jpegOutput);
|
||||
System.out.println("...embeddedStream.length=" + embed_size);
|
||||
}
|
||||
|
||||
// Extract payload
|
||||
ByteArrayOutputStream extractedEncrypted = new ByteArrayOutputStream();
|
||||
JpegExifEmbedder embedder = new JpegExifEmbedder();
|
||||
embedder.setSlots(Slot.defaults());
|
||||
embedder.extract(jpegEmbedded, extractedEncrypted);
|
||||
|
||||
byte[] extractedEncryptedBytes = extractedEncrypted.toByteArray();
|
||||
System.out.println("...extractedEncryptedBytes.length=" + extractedEncryptedBytes.length);
|
||||
System.out.println("...-> " + Arrays.toString(extractedEncryptedBytes));
|
||||
|
||||
// Decrypt
|
||||
dccb = DataContentChainBuilder.decrypt()
|
||||
// input
|
||||
.add(PlainBytesBuilder.builder().bytes(extractedEncryptedBytes))
|
||||
// encryption
|
||||
.add(AesDataContentBuilder.builder().importKeyRaw(key.getEncoded()).spec(AesSpec.gcm128(null))
|
||||
// let us use the default header for AAD and IV
|
||||
.withHeader().withAad(aad).context(session))
|
||||
// and create the pipeline
|
||||
.build();
|
||||
byte[] pt1;
|
||||
try (InputStream in = dccb.getStream()) {
|
||||
pt1 = readAll(in);
|
||||
}
|
||||
/*
|
||||
* AesSpec spec =
|
||||
* AesSpec.builder().mode(Mode.GCM).tagLenBits(128).header(null).build();
|
||||
* EncryptionContext dec1 = CryptoAlgorithms.create("AES", KeyUsage.DECRYPT,
|
||||
* key, spec); ((ContextAware) dec1).setContext(session); // same IV/AAD in ctx
|
||||
* byte[] pt1 = readAll(dec1.attach(new
|
||||
* ByteArrayInputStream(extractedEncryptedBytes))); dec1.close();
|
||||
*/
|
||||
String decrypted = new String(pt1, StandardCharsets.UTF_8);
|
||||
|
||||
assertEquals(inputText, decrypted);
|
||||
System.out.println("...ok");
|
||||
}
|
||||
|
||||
private Path getResourcePath(String resource) throws URISyntaxException {
|
||||
URL url = getClass().getClassLoader().getResource(resource);
|
||||
if (url == null) {
|
||||
throw new IllegalArgumentException("Missing resource: " + resource);
|
||||
}
|
||||
return Paths.get(url.toURI());
|
||||
}
|
||||
}
|
||||
@@ -1,88 +0,0 @@
|
||||
/*******************************************************************************
|
||||
* Copyright (C) 2026, Leo Galambos
|
||||
* All rights reserved.
|
||||
*
|
||||
* Redistribution and use in source and binary forms, with or without modification,
|
||||
* are permitted provided that the following conditions are met:
|
||||
*
|
||||
* 1. Redistributions of source code must retain the above copyright notice, this
|
||||
* list of conditions and the following disclaimer.
|
||||
*
|
||||
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
* this list of conditions and the following disclaimer in the documentation
|
||||
* and/or other materials provided with the distribution.
|
||||
*
|
||||
* 3. All advertising materials mentioning features or use of this software must
|
||||
* display the following acknowledgement:
|
||||
* This product includes software developed by the Egothor project.
|
||||
*
|
||||
* 4. Neither the name of the copyright holder nor the names of its contributors
|
||||
* may be used to endorse or promote products derived from this software without
|
||||
* specific prior written permission.
|
||||
*
|
||||
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR
|
||||
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
|
||||
* ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
******************************************************************************/
|
||||
package zeroecho.sdk.integrations.stegano;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.StandardOpenOption;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
public class LSBSteganographyMethodTest {
|
||||
|
||||
@Test
|
||||
void testEmbedAndExtract() throws Exception {
|
||||
SteganographyMethod method = new LSBSteganographyMethod();
|
||||
String message = "Hello from LSB!";
|
||||
InputStream imageIn = getClass().getResourceAsStream("/test.jpg");
|
||||
InputStream msgIn = new ByteArrayInputStream(message.getBytes());
|
||||
|
||||
// Embed
|
||||
InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn);
|
||||
|
||||
// Extract
|
||||
InputStream extracted = method.extract(embedded);
|
||||
String result = new String(extracted.readAllBytes());
|
||||
|
||||
assertEquals(message, result);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testMetadata() {
|
||||
SteganographyMethod method = new LSBSteganographyMethod();
|
||||
StegoMetadata meta = method.getMetadata();
|
||||
|
||||
assertEquals("LSB", meta.name());
|
||||
assertTrue(meta.fullName().contains("Least Significant"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void createSample() throws Exception {
|
||||
// for verification purposes
|
||||
SteganographyMethod method = new LSBSteganographyMethod();
|
||||
String message = "Hello from LSB!";
|
||||
InputStream imageIn = getClass().getResourceAsStream("/test.jpg");
|
||||
InputStream msgIn = new ByteArrayInputStream(message.getBytes());
|
||||
|
||||
// Embed
|
||||
InputStream embedded = method.embed(imageIn, ImageFormat.PNG, msgIn);
|
||||
|
||||
embedded.transferTo(Files.newOutputStream(Paths.get("/tmp/lsb-test.png"), StandardOpenOption.CREATE));
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 230 KiB |
Reference in New Issue
Block a user