diff --git a/CMakeLists.txt b/CMakeLists.txt index e11858a3..1208a093 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -42,6 +42,7 @@ endif() include(GNUInstallDirs) option(LIBCDOC_WITH_DOCS "Generate documentation with Doxygen" ON) +option(SKIP_JAVA_TESTS "Skip Java tests (useful when cross-compiling)" OFF) find_package(OpenSSL 3.0.0 REQUIRED) find_package(ZLIB REQUIRED) @@ -69,6 +70,13 @@ if(LIBCDOC_WITH_DOCS) endif() add_subdirectory(cdoc) +# Binary dir of the cdoc sub-build; used by examples/CMakeLists.txt to locate +# the SWIG-generated Java bindings. Correct also when libcdoc is included as +# a subproject of a larger build. +set(CDOC_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR}/cdoc) +if(NOT SKIP_JAVA_TESTS) + add_subdirectory(examples) +endif() find_package(Boost COMPONENTS unit_test_framework QUIET) if (TARGET Boost::unit_test_framework) diff --git a/CMakePresets.json b/CMakePresets.json index 744a600e..01c9a7c2 100644 --- a/CMakePresets.json +++ b/CMakePresets.json @@ -84,7 +84,8 @@ "cacheVariables": { "CMAKE_SYSTEM_NAME": "Android", "CMAKE_SYSTEM_VERSION": "30", - "BUILD_SHARED_LIBS": "NO" + "BUILD_SHARED_LIBS": "NO", + "SKIP_JAVA_TESTS": "YES" } }, { diff --git a/cdoc/CDocCipher.cpp b/cdoc/CDocCipher.cpp index b98b938e..1b19617e 100644 --- a/cdoc/CDocCipher.cpp +++ b/cdoc/CDocCipher.cpp @@ -315,7 +315,9 @@ int CDocCipher::writer_push(CDocWriter& writer, const vector& rcpts, int64_t result = writer.beginEncryption(); if (result != libcdoc::OK) return result; for (const std::string& file : files) { - std::filesystem::path path(file); + // The string is UTF-8; construct the path from u8string so that + // Windows interprets it as UTF-8 rather than the active code page. + std::filesystem::path path(std::u8string(reinterpret_cast(file.data()), file.size())); if (!std::filesystem::exists(path)) { LOG_ERROR("File does not exist: {}", file); return 1; @@ -519,7 +521,7 @@ int CDocCipher::Decrypt(const unique_ptr& rdr, unsigned int lock_idx LOG_ERROR("Error on extracting FMK: {} {}", result, rdr->getLastErrorStr()); return 1; } - filesystem::path base_path(base_pathname); + filesystem::path base_path(std::u8string(reinterpret_cast(base_pathname.data()), base_pathname.size())); /* Do pull */ result = rdr->beginDecryption(fmk); diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt new file mode 100644 index 00000000..f776322e --- /dev/null +++ b/examples/CMakeLists.txt @@ -0,0 +1,74 @@ +# Java example (CDocTool) built with the Gradle wrapper in examples/java. +# +# The example compiles against the SWIG-generated Java bindings produced by +# the cdoc_java target. It is added to the default build only when: +# * SWIG was found and the cdoc_java target exists, and +# * a Java Development Kit was found (Java_Development_FOUND), and +# * the Gradle wrapper script is present in the source tree, and +# * LIBCDOC_BUILD_JAVA_EXAMPLE is ON (default). +# +# The Gradle invocation is given absolute paths to the SWIG Java sources and +# the JNI library of the current build, so no manual configuration is needed. + +set(JAVA_EXAMPLE_DIR ${CMAKE_CURRENT_SOURCE_DIR}/java) + +if(WIN32) + set(GRADLE_WRAPPER ${JAVA_EXAMPLE_DIR}/gradlew.bat) +else() + set(GRADLE_WRAPPER ${JAVA_EXAMPLE_DIR}/gradlew) +endif() + +if(NOT EXISTS ${GRADLE_WRAPPER}) + message(STATUS "Gradle wrapper not found in ${JAVA_EXAMPLE_DIR} - skipping Java example") + return() +endif() + +if(NOT TARGET cdoc_java) + message(STATUS "cdoc_java target not available - skipping Java example") + return() +endif() + +if(NOT Java_Development_FOUND) + message(STATUS "Java Development Kit not found - skipping Java example") + return() +endif() + +option(LIBCDOC_BUILD_JAVA_EXAMPLE "Build the Java CDocTool example with Gradle" ON) +if(NOT LIBCDOC_BUILD_JAVA_EXAMPLE) + return() +endif() + +# Directory containing the SWIG-generated .java files (OUTPUT_DIR of +# swig_add_library in cdoc/CMakeLists.txt). CDOC_BINARY_DIR is set by the +# parent CMakeLists.txt right after add_subdirectory(cdoc); fall back to the +# conventional location if this file is used standalone. +if(NOT CDOC_BINARY_DIR) + set(CDOC_BINARY_DIR ${CMAKE_BINARY_DIR}/cdoc) +endif() +set(JAVA_EXAMPLE_SWIG_DIR ${CDOC_BINARY_DIR}/java) +set(JAVA_EXAMPLE_JAR ${JAVA_EXAMPLE_DIR}/build/libs/CDocTool.jar) + +# When cross-compiling (e.g. Android), the JNI library targets a different +# architecture than the build host, so Java tests cannot load it. Build the +# example jar but skip the tests. +if(SKIP_JAVA_TESTS) + set(GRADLE_SKIP_TESTS -x test) +endif() + +add_custom_command( + OUTPUT ${JAVA_EXAMPLE_JAR} + COMMENT "Building Java example (CDocTool.jar) with Gradle" + COMMAND ${GRADLE_WRAPPER} + --info + --no-daemon + -PswigJavaDir=${JAVA_EXAMPLE_SWIG_DIR} + -PjniLibDir=$ + build ${GRADLE_SKIP_TESTS} + WORKING_DIRECTORY ${JAVA_EXAMPLE_DIR} + DEPENDS cdoc_java + VERBATIM +) + +add_custom_target(cdoc_java_example ALL + DEPENDS ${JAVA_EXAMPLE_JAR} +) diff --git a/examples/java/README.md b/examples/java/README.md index ee8c206a..8368ad14 100644 --- a/examples/java/README.md +++ b/examples/java/README.md @@ -1,9 +1,62 @@ # Build instructions for Java -## Build +The Java example (`CDocTool`) compiles against the SWIG-generated Java +bindings produced by the libcdoc CMake build. + +## Automatic build (recommended) + +When the libcdoc project is configured with SWIG and a JDK available, the +example is built automatically as part of the default CMake build: + + cmake --preset macos + cmake --build build/macos + +This produces `examples/java/build/libs/CDocTool.jar`. Set +`-DLIBCDOC_BUILD_JAVA_EXAMPLE=OFF` to disable it. + +## Manual build + +First build the main CMake project so that the SWIG Java bindings and the +JNI library exist, then run the Gradle wrapper: ./gradlew jar +`build.gradle` locates the SWIG-generated sources automatically by scanning +the well-known CMake build directories (`build//cdoc/java`). If your +build tree lives elsewhere, point the build at it explicitly: + + ./gradlew jar -PswigJavaDir=/path/to/build/cdoc/java -PjniLibDir=/path/to/build/cdoc + +or with environment variables: + + LIBCDOC_SWIG_JAVA_DIR=/path/to/build/cdoc/java LIBCDOC_JNI_LIB_DIR=/path/to/build/cdoc ./gradlew jar + +## Test + +The example has JUnit 5 tests (in `src/test/java`) that run against the +compiled JNI library: + + ./gradlew test + +The CMake-driven build runs the tests automatically (it invokes +`gradlew build`, which includes the `test` task). + ## Run - java -Djava.library.path=/some/path/lib -jar build/libs/CDocTool.jar \ No newline at end of file + ./gradlew run + +or directly: + + java -jar build/libs/CDocTool.jar + +The JNI library built by the CMake project is located automatically. The +resolution order is: + +1. `--library ` command line argument +2. `-Dcdoc.library=` system property +3. `ee/ria/cdoc/jni.properties` baked into the jar by Gradle (records the + JNI library directory at build time) +4. Well-known CMake build directories (`build//cdoc`) relative to + the working directory +5. `java.library.path` (set by `./gradlew run`, or pass + `-Djava.library.path=/path/to/build/cdoc` to `java`) diff --git a/examples/java/build.gradle b/examples/java/build.gradle index 4acef996..117ef1f1 100755 --- a/examples/java/build.gradle +++ b/examples/java/build.gradle @@ -1,17 +1,215 @@ plugins { id 'java' } + group 'ee.ria' -sourceSets.main.java.srcDirs += ['../../build/macos/cdoc/java', '../../../../build/client/libcdoc/cdoc/java'] + +/* + * Robust build for the CDocTool Java example. + * + * The SWIG-generated Java bindings are produced by the CMake build of the + * parent project and are not checked into the repository. This build script + * locates them using, in order of precedence: + * + * 1. -PswigJavaDir= (Gradle project property) + * 2. LIBCDOC_SWIG_JAVA_DIR env (environment variable) + * 3. Auto-detection (scans well-known CMake build directories) + * + * The native JNI library directory (used by the 'run' task) can be set with: + * + * 1. -PjniLibDir= (Gradle project property) + * 2. LIBCDOC_JNI_LIB_DIR env (environment variable) + * 3. Auto-detection (same scan as above) + */ + +def projectDir = layout.projectDirectory.asFile + +static File findSwigJavaDir(File root) { + // Scan well-known CMake build directory layouts for the SWIG output. + // Paths are relative to examples/java, i.e. two levels below the + // libcdoc project root. + def candidates = [ + '../../build/macos/cdoc/java', + '../../build/macos-debug/cdoc/java', + '../../build/ninja/cdoc/java', + '../../build/linux/cdoc/java', + '../../build/cdoc/java', + '../../../build/cdoc/java', + '../../../build/client/libcdoc/cdoc/java', + ] + for (String rel : candidates) { + File dir = new File(root, rel) + if (new File(dir, 'CDoc.java').isFile()) { + return dir.canonicalFile + } + } + // Fall back to glob over /build/*/cdoc/java + def buildRoot = new File(root, '../../build') + if (buildRoot.isDirectory()) { + def found = buildRoot.listFiles()?.findAll { b -> + new File(b, 'cdoc/java/CDoc.java').isFile() + } + if (found && !found.isEmpty()) { + return new File(found.sort().first(), 'cdoc/java').canonicalFile + } + } + return null +} + +static File findJniLibDir(File root) { + def names = ['libcdoc_java.jnilib', 'libcdoc_javad.jnilib', + 'libcdoc_java.so', 'libcdoc_javad.so', + 'cdoc_java.dll', 'cdoc_javad.dll'] + def candidates = [ + '../../build/macos/cdoc', + '../../build/macos-debug/cdoc', + '../../build/ninja/cdoc', + '../../build/linux/cdoc', + '../../build/cdoc', + '../../../build/cdoc', + ] + for (String rel : candidates) { + for (String name : names) { + if (new File(root, "$rel/$name").isFile()) { + return new File(root, rel).canonicalFile + } + } + } + def buildRoot = new File(root, '../../build') + if (buildRoot.isDirectory()) { + def found = buildRoot.listFiles()?.findAll { b -> + names.any { new File(b, "cdoc/$it").isFile() } + } + if (found && !found.isEmpty()) { + return new File(found.sort().first(), 'cdoc').canonicalFile + } + } + return null +} + +File swigJavaDir = findProperty('swigJavaDir')?.with { file(it) } + ?: System.getenv('LIBCDOC_SWIG_JAVA_DIR')?.with { file(it) } + ?: findSwigJavaDir(projectDir) + +File jniLibDir = findProperty('jniLibDir')?.with { file(it) } + ?: System.getenv('LIBCDOC_JNI_LIB_DIR')?.with { file(it) } + ?: findJniLibDir(projectDir) + +if (swigJavaDir == null) { + throw new GradleException(""" + |Could not locate SWIG-generated Java bindings. + | + |Build the libcdoc CMake project first (it must be configured with + |SWIG and Java/JNI available), then either: + | * pass -PswigJavaDir=/cdoc/java, or + | * set LIBCDOC_SWIG_JAVA_DIR=/cdoc/java + | + |Searched under: $projectDir + """.stripMargin()) +} + +if (!new File(swigJavaDir, 'CDoc.java').isFile()) { + throw new GradleException( + "swigJavaDir '$swigJavaDir' does not contain CDoc.java. " + + "Make sure the CMake build with SWIG/Java has been run.") +} + +logger.lifecycle("Using SWIG Java sources: $swigJavaDir") +if (jniLibDir != null) { + logger.lifecycle("Using JNI library directory: $jniLibDir") +} else { + logger.lifecycle("JNI library directory not found - 'run' task will need -PjniLibDir") +} + +// Generate ee/ria/cdoc/jni.properties with the build-time JNI library +// directory. CDocTool reads this resource at runtime and loads the project's +// compiled JNI library from there, so no --library flag is needed. +def jniResourcesDir = layout.buildDirectory.dir('generated/resources/jni') + +tasks.register('generateJniProperties') { + def propsFile = jniResourcesDir.map { it.file('ee/ria/cdoc/jni.properties') } + inputs.property('jniLibDir', jniLibDir?.absolutePath ?: '') + outputs.file(propsFile) + doLast { + File f = propsFile.get().asFile + f.parentFile.mkdirs() + Properties props = new Properties() + if (jniLibDir != null) { + props.setProperty('jniLibDir', jniLibDir.absolutePath) + } + f.withOutputStream { props.store(it, 'Generated by build.gradle') } + } +} + +sourceSets { + main { + java { + srcDirs = ['src/main/java', swigJavaDir.absolutePath] + } + output.dir(jniResourcesDir.get().asFile, builtBy: 'generateJniProperties') + } +} + java { targetCompatibility JavaVersion.VERSION_17 sourceCompatibility JavaVersion.VERSION_17 } + +tasks.withType(JavaCompile).configureEach { + options.encoding = 'UTF-8' +} + base { archivesName = 'CDocTool' } + +repositories { + mavenCentral() +} + +dependencies { + testImplementation platform('org.junit:junit-bom:5.10.2') + testImplementation 'org.junit.jupiter:junit-jupiter' + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +// Resolve the actual JNI library file (with debug postfix and platform +// suffix) for the benefit of tests, which load it via -Dcdoc.library. +def jniLibFile = jniLibDir == null ? null : ( + ['libcdoc_java.jnilib', 'libcdoc_javad.jnilib', + 'libcdoc_java.dylib', 'libcdoc_javad.dylib', + 'libcdoc_java.so', 'libcdoc_javad.so', + 'cdoc_java.dll', 'cdoc_javad.dll'] + .collect { new File(jniLibDir, it) } + .find { it.isFile() }) + jar { manifest { attributes 'Main-Class': 'ee.ria.cdoc.CDocTool' } } + +tasks.named('test', Test) { + useJUnitPlatform() + if (jniLibFile != null) { + systemProperty 'cdoc.library', jniLibFile.absolutePath + } else if (jniLibDir != null) { + systemProperty 'java.library.path', jniLibDir.absolutePath + } + testLogging { + events 'passed', 'failed', 'skipped' + showStandardStreams true + showStackTraces true + exceptionFormat 'full' + } +} + +tasks.register('run', JavaExec) { + description = 'Runs CDocTool (requires the JNI library to be built by CMake)' + group = 'application' + mainClass = 'ee.ria.cdoc.CDocTool' + classpath = sourceSets.main.runtimeClasspath + if (jniLibDir != null) { + systemProperty 'java.library.path', jniLibDir.absolutePath + } +} diff --git a/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java b/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java index 2b0cf6be..5c04b15e 100644 --- a/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java +++ b/examples/java/src/main/java/ee/ria/cdoc/CDocTool.java @@ -3,311 +3,497 @@ import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; -import java.io.InputStream; import java.io.IOException; +import java.io.InputStream; import java.io.OutputStream; +import java.io.PrintStream; +import java.math.BigInteger; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.PublicKey; import java.security.SecureRandom; +import java.security.interfaces.ECPrivateKey; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.MGF1ParameterSpec; +import java.security.spec.PKCS8EncodedKeySpec; import java.util.ArrayList; -import java.util.HashMap; -import java.util.Collection; +import java.util.Arrays; import java.util.HexFormat; -import java.util.concurrent.locks.Lock; +import java.util.List; +import javax.crypto.KeyAgreement; +import javax.crypto.spec.OAEPParameterSpec; +import javax.crypto.spec.PSource; public class CDocTool { private enum Action { INVALID, ENCRYPT, DECRYPT, - LOCKS, - TEST + LOCKS } - private static HexFormat hex = HexFormat.of(); + private static final HexFormat hex = HexFormat.of(); public static String getArg(int arg_idx, String[] args) { arg_idx += 1; if (arg_idx >= args.length) { - System.err.println("Invalid arguments"); - System.exit(1); + failUsage("Missing argument"); } return args[arg_idx]; } // Make logger static to ensure that it is not garbage-collected as long as it is attached to library private static Logger logger; - + + private static void printUsage(PrintStream ofs) { + ofs.print(""" + Usage: + CDocTool [--library JNI_LIBRARY] ACTION ARGUMENTS FILE(S) + + --library before ACTION is the path to the libcdoc JNI library. + Everywhere else --library refers to the PKCS#11 module. + + Actions: + encrypt Encrypt files + decrypt Decrypt files + locks List locks in a CDoc file + + Encryption arguments: + --rcpt RECIPIENT Recipient info, where recipient is one of the following: +