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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,30 @@ class AndroidSignerTests : SignerTests() {
assertTrue(result.success, "StrongBox Availability test failed: ${result.message}")
}

@Test
fun runTestSignWithContextFromSettings() = runBlocking {
val result = testSignWithContextFromSettings()
assertTrue(result.success, "Sign With Context (settings signer) test failed: ${result.message}")
}

@Test
fun runTestSignUnsupportedFormat() = runBlocking {
val result = testSignUnsupportedFormat()
assertTrue(result.success, "Sign Unsupported Format test failed: ${result.message}")
}

@Test
fun runTestSignWithContextFromJson() = runBlocking {
val result = testSignWithContextFromJson()
assertTrue(result.success, "Sign With Context (fromJson builder) test failed: ${result.message}")
}

@Test
fun runTestSignWithContextWithoutSigner() = runBlocking {
val result = testSignWithContextWithoutSigner()
assertTrue(result.success, "Sign With Context (no signer) test failed: ${result.message}")
}

@Test
fun runTestSignerFromSettingsToml() = runBlocking {
val result = testSignerFromSettingsToml()
Expand Down
104 changes: 70 additions & 34 deletions library/src/main/jni/c2pa_jni.c
Original file line number Diff line number Diff line change
Expand Up @@ -1034,34 +1034,9 @@ JNIEXPORT jint JNICALL Java_org_contentauth_c2pa_Builder_writeIngredientArchiveN
return result;
}

JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *env, jobject obj, jlong builderPtr, jstring format, jlong sourceStreamPtr, jlong destStreamPtr, jlong signerPtr) {
if (builderPtr == 0 || format == NULL || sourceStreamPtr == 0 || destStreamPtr == 0 || signerPtr == 0) {
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"),
"Builder, format, streams, and signer cannot be null");
return NULL;
}

struct C2paBuilder *builder = (struct C2paBuilder*)(uintptr_t)builderPtr;
const char *cformat = jstring_to_cstring(env, format);
if (cformat == NULL) {
return NULL;
}

struct C2paStream *source = (struct C2paStream*)(uintptr_t)sourceStreamPtr;
struct C2paStream *dest = (struct C2paStream*)(uintptr_t)destStreamPtr;
struct C2paSigner *signer = (struct C2paSigner*)(uintptr_t)signerPtr;

const unsigned char *manifestBytes = NULL;
int64_t size = c2pa_builder_sign(builder, cformat, source, dest, signer, &manifestBytes);

release_cstring(env, format, cformat);

if (size < 0) {
throw_c2pa_exception(env, "Failed to sign builder");
return NULL;
}

// Create result object
// Builds a Builder.SignResult from a sign call's outputs. Frees manifestBytes on every
// path; returns NULL (with the pending exception cleared) if construction fails.
static jobject build_sign_result(JNIEnv *env, int64_t size, const unsigned char *manifestBytes) {
jclass resultClass = g_signResultClass;
if (resultClass == NULL) {
resultClass = (*env)->FindClass(env, "org/contentauth/c2pa/Builder$SignResult");
Expand All @@ -1073,7 +1048,7 @@ JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *e
return NULL;
}
}

jmethodID constructor = (*env)->GetMethodID(env, resultClass, "<init>", "(J[B)V");
if (constructor == NULL) {
check_exception(env);
Expand All @@ -1082,32 +1057,93 @@ JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *e
}
return NULL;
}

jbyteArray jmanifestBytes = NULL;
if (manifestBytes != NULL && size > 0) {
jmanifestBytes = safe_new_byte_array(env, size);
if (jmanifestBytes == NULL) {
c2pa_free(manifestBytes);
return NULL;
}

(*env)->SetByteArrayRegion(env, jmanifestBytes, 0, size, (const jbyte*)manifestBytes);
if (check_exception(env)) {
c2pa_free(manifestBytes);
return NULL;
}

}
if (manifestBytes != NULL) {
c2pa_free(manifestBytes);
}

jobject result = (*env)->NewObject(env, resultClass, constructor, (jlong)size, jmanifestBytes);
if (result == NULL) {
check_exception(env);
}

return result;
}

JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signNative(JNIEnv *env, jobject obj, jlong builderPtr, jstring format, jlong sourceStreamPtr, jlong destStreamPtr, jlong signerPtr) {
if (builderPtr == 0 || format == NULL || sourceStreamPtr == 0 || destStreamPtr == 0 || signerPtr == 0) {
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"),
"Builder, format, streams, and signer cannot be null");
return NULL;
}

struct C2paBuilder *builder = (struct C2paBuilder*)(uintptr_t)builderPtr;
const char *cformat = jstring_to_cstring(env, format);
if (cformat == NULL) {
return NULL;
}

struct C2paStream *source = (struct C2paStream*)(uintptr_t)sourceStreamPtr;
struct C2paStream *dest = (struct C2paStream*)(uintptr_t)destStreamPtr;
struct C2paSigner *signer = (struct C2paSigner*)(uintptr_t)signerPtr;

const unsigned char *manifestBytes = NULL;
int64_t size = c2pa_builder_sign(builder, cformat, source, dest, signer, &manifestBytes);

release_cstring(env, format, cformat);

// On failure, return NULL and let the Kotlin wrapper raise C2PAError from c2pa_error().
if (size < 0) {
return NULL;
}

return build_sign_result(env, size, manifestBytes);
}

JNIEXPORT jobject JNICALL Java_org_contentauth_c2pa_Builder_signWithContextNative(JNIEnv *env, jobject obj, jlong builderPtr, jstring format, jlong sourceStreamPtr, jlong destStreamPtr) {
if (builderPtr == 0 || format == NULL || sourceStreamPtr == 0 || destStreamPtr == 0) {
(*env)->ThrowNew(env, (*env)->FindClass(env, "java/lang/IllegalArgumentException"),
"Builder, format, and streams cannot be null");
return NULL;
}

struct C2paBuilder *builder = (struct C2paBuilder*)(uintptr_t)builderPtr;
const char *cformat = jstring_to_cstring(env, format);
if (cformat == NULL) {
return NULL;
}

struct C2paStream *source = (struct C2paStream*)(uintptr_t)sourceStreamPtr;
struct C2paStream *dest = (struct C2paStream*)(uintptr_t)destStreamPtr;

// Signer comes from the builder's context (programmatic or from settings).
const unsigned char *manifestBytes = NULL;
int64_t size = c2pa_builder_sign_context(builder, cformat, source, dest, &manifestBytes);

release_cstring(env, format, cformat);

// On failure, return NULL and let the Kotlin wrapper raise C2PAError from c2pa_error().
if (size < 0) {
return NULL;
}

return build_sign_result(env, size, manifestBytes);
}

// New Builder methods
JNIEXPORT jbyteArray JNICALL Java_org_contentauth_c2pa_Builder_dataHashedPlaceholderNative(JNIEnv *env, jobject obj, jlong builderPtr, jlong reservedSize, jstring format) {
if (builderPtr == 0 || format == NULL || reservedSize <= 0) {
Expand Down
39 changes: 31 additions & 8 deletions library/src/main/kotlin/org/contentauth/c2pa/Builder.kt
Original file line number Diff line number Diff line change
Expand Up @@ -576,13 +576,30 @@ class Builder internal constructor(private var ptr: Long) : Closeable {
* @throws C2PAError.Api if signing fails
*/
@Throws(C2PAError::class)
fun sign(format: String, source: Stream, dest: Stream, signer: Signer): SignResult {
val result = signNative(ptr, format, source.rawPtr, dest.rawPtr, signer.ptr)
if (result.size < 0) {
throw C2PAError.Api(C2PA.getError() ?: "Failed to sign")
}
return result
}
fun sign(format: String, source: Stream, dest: Stream, signer: Signer): SignResult =
signNative(ptr, format, source.rawPtr, dest.rawPtr, signer.ptr)
?: throw C2PAError.Api(C2PA.getError() ?: "Failed to sign")

/**
* Signs the manifest using the signer configured on the builder's [C2PAContext] — either set
* programmatically (`C2PAContextBuilder.setSigner`) or supplied via settings (`[signer.local]`
* / `[cawg_x509_signer]`) — and writes the signed asset to [dest].
*
* Unlike [sign], no explicit [Signer] is passed; the builder's context must have a signer
* configured, or signing fails. Every builder has a context ([fromJson] creates one
* internally), so this also works for builders created with [fromJson] and a [C2PASettings]
* that carries a signer. Use this instead of the deprecated settings-based [Signer] factories.
*
* @param format The MIME type of the asset (e.g. "image/jpeg")
* @param source The input stream containing the original asset
* @param dest The output stream for the signed asset
* @return A [SignResult] containing the manifest size and optional manifest bytes
* @throws C2PAError.Api if signing fails (e.g. the context has no signer)
*/
@Throws(C2PAError::class)
fun signWithContext(format: String, source: Stream, dest: Stream): SignResult =
signWithContextNative(ptr, format, source.rawPtr, dest.rawPtr)
?: throw C2PAError.Api(C2PA.getError() ?: "Failed to sign with context")

/**
* Creates a data-hashed placeholder for deferred signing workflows.
Expand Down Expand Up @@ -815,7 +832,13 @@ class Builder internal constructor(private var ptr: Long) : Closeable {
sourceHandle: Long,
destHandle: Long,
signerHandle: Long,
): SignResult
): SignResult?
private external fun signWithContextNative(
handle: Long,
format: String,
sourceHandle: Long,
destHandle: Long,
): SignResult?
private external fun dataHashedPlaceholderNative(handle: Long, reservedSize: Long, format: String): ByteArray?
private external fun signDataHashedEmbeddableNative(
handle: Long,
Expand Down
8 changes: 8 additions & 0 deletions library/src/main/kotlin/org/contentauth/c2pa/C2PA.kt
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,20 @@ object C2PA {
* Load settings from a string.
* Returns the result code from the native call (0 for success).
*/
@Deprecated(
"Global settings apply relies on the deprecated c2pa_load_settings. Configure settings " +
"in C2PASettings and build a C2PAContext instead.",
)
@JvmStatic
fun loadSettingsResult(settings: String, format: String): Int = loadSettingsNative(settings, format)

/**
* Load settings from a string
*/
@Deprecated(
"Global settings apply relies on the deprecated c2pa_load_settings. Configure settings " +
"in C2PASettings and build a C2PAContext instead.",
)
@Throws(C2PAError::class)
fun loadSettings(settings: String, format: String) {
executeC2PAOperation("Failed to load settings") {
Expand Down
16 changes: 16 additions & 0 deletions library/src/main/kotlin/org/contentauth/c2pa/Signer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,11 @@ class Signer internal constructor(internal var ptr: Long) : Closeable {
* }
* ```
*/
@Deprecated(
"Settings-based signers rely on deprecated core APIs (c2pa_load_settings / " +
"c2pa_signer_from_settings). Configure the signer in C2PASettings, build a " +
"C2PAContext, and sign via Builder.signWithContext().",
)
@JvmStatic
@Throws(C2PAError::class)
fun fromSettingsJson(settingsJson: String): Signer = fromSettings(settingsJson, "json")
Expand Down Expand Up @@ -161,6 +166,11 @@ class Signer internal constructor(internal var ptr: Long) : Closeable {
* referenced_assertions = ["cawg.training-mining"]
* ```
*/
@Deprecated(
"Settings-based signers rely on deprecated core APIs (c2pa_load_settings / " +
"c2pa_signer_from_settings). Configure the signer in C2PASettings, build a " +
"C2PAContext, and sign via Builder.signWithContext().",
)
@JvmStatic
@Throws(C2PAError::class)
fun fromSettingsToml(settingsToml: String): Signer = fromSettings(settingsToml, "toml")
Expand All @@ -173,6 +183,7 @@ class Signer internal constructor(internal var ptr: Long) : Closeable {
* @return A new [Signer] instance configured according to the settings.
* @throws C2PAError if the settings are invalid or the signer cannot be created.
*/
@Suppress("DEPRECATION")
@JvmStatic
@Throws(C2PAError::class)
private fun fromSettings(settings: String, format: String): Signer =
Expand All @@ -195,6 +206,11 @@ class Signer internal constructor(internal var ptr: Long) : Closeable {
* @param format The format of the settings string ("json" or "toml").
* @throws C2PAError if the settings are invalid.
*/
@Deprecated(
"Global settings apply relies on the deprecated c2pa_load_settings. Configure settings " +
"in C2PASettings and build a C2PAContext instead.",
)
@Suppress("DEPRECATION")
@JvmStatic
@Throws(C2PAError::class)
fun loadSettings(settings: String, format: String) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,10 @@ private suspend fun runAllTests(context: Context): List<TestResult> = withContex
results.add(signerTests.testStrongBoxSignerIntegration())
results.add(signerTests.testKeyStoreSignerKeyManagement())
results.add(signerTests.testStrongBoxAvailability())
results.add(signerTests.testSignUnsupportedFormat())
results.add(signerTests.testSignWithContextFromSettings())
results.add(signerTests.testSignWithContextFromJson())
results.add(signerTests.testSignWithContextWithoutSigner())
results.add(signerTests.testSignerFromSettingsToml())
results.add(signerTests.testSignerFromSettingsJson())
results.add(signerTests.testCawgCombinedPemSigner())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,7 @@ abstract class CoreTests : TestBase() {
}
}

@Suppress("DEPRECATION")
suspend fun testLoadSettings(): TestResult = withContext(Dispatchers.IO) {
runTest("Load Settings") {
val settingsJson =
Expand Down
Loading
Loading