Skip to content

Commit 9a60f0b

Browse files
joewizclaude
andcommitted
[feature] EXPath Cryptographic Module for eXist-db
Native implementation of the EXPath Cryptographic Module using Java JCE. Replaces the legacy expath-crypto-module (ro.kuberam) with zero external dependencies. Functions: crypto:hash, crypto:hmac, crypto:encrypt, crypto:decrypt, crypto:generate-signature, crypto:validate-signature Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
0 parents  commit 9a60f0b

File tree

18 files changed

+2318
-0
lines changed

18 files changed

+2318
-0
lines changed

.github/workflows/exist.yml

Lines changed: 153 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,153 @@
1+
# This workflow builds a xar archive, deploys it into exist and runs a smoke test.
2+
3+
name: exist-db CI
4+
5+
on: [push, pull_request]
6+
7+
jobs:
8+
build:
9+
runs-on: ubuntu-latest
10+
strategy:
11+
fail-fast: false
12+
matrix:
13+
exist-version: [latest]
14+
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
# Build with Maven
19+
- name: Set up JDK 21
20+
uses: actions/setup-java@v5
21+
with:
22+
java-version: '21'
23+
distribution: 'temurin'
24+
cache: maven
25+
26+
# Extract exist-core JAR from Docker image for compilation,
27+
# since the published 7.0.0-SNAPSHOT in the Maven repo is stale.
28+
- name: Pull eXist-db image
29+
run: docker pull existdb/existdb:${{ matrix.exist-version }}
30+
31+
- name: Install exist-core from Docker image into local Maven repo
32+
run: |
33+
id=$(docker create existdb/existdb:${{ matrix.exist-version }})
34+
docker cp "$id:/exist/lib/exist.uber.jar" /tmp/exist.uber.jar
35+
docker rm "$id"
36+
mvn install:install-file -Dfile=/tmp/exist.uber.jar \
37+
-DgroupId=org.exist-db -DartifactId=exist-core \
38+
-Dversion=7.0.0-SNAPSHOT -Dpackaging=jar -q
39+
40+
- name: Build with Maven
41+
run: mvn clean package -DskipTests -q
42+
43+
# Deploy XAR in Container
44+
- name: Start eXist-db container
45+
run: |
46+
docker run -dit -p 8080:8080 \
47+
-v ${{ github.workspace }}/target:/exist/autodeploy \
48+
--name exist --rm --health-interval=1s --health-start-period=1s \
49+
existdb/existdb:${{ matrix.exist-version }}
50+
51+
- name: Wait for eXist-db to start and deploy packages
52+
timeout-minutes: 5
53+
run: |
54+
while ! docker logs exist 2>&1 | grep -q "Server has started"; \
55+
do sleep 6s; \
56+
done
57+
echo "Server started, waiting for autodeploy..."
58+
sleep 15
59+
echo "=== eXist-db logs (tail) ==="
60+
docker logs exist 2>&1 | tail -15
61+
62+
- name: Check XAR is installed
63+
run: |
64+
result=$(curl -s -u admin: -H "Content-Type: application/xml" --data '
65+
<query xmlns="http://exist.sourceforge.net/NS/exist">
66+
<text><![CDATA[
67+
import module namespace crypto = "http://expath.org/ns/crypto";
68+
"crypto module loaded"
69+
]]></text>
70+
</query>' "http://localhost:8080/exist/rest/db")
71+
echo "$result"
72+
echo "$result" | grep -q "crypto module loaded" || (echo "FAIL: crypto module not loaded" && exit 1)
73+
74+
# Smoke tests: verify functions work
75+
- name: Test crypto:hash with known SHA-256 vector
76+
run: |
77+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
78+
<query xmlns="http://exist.sourceforge.net/NS/exist">
79+
<text><![CDATA[
80+
import module namespace crypto = "http://expath.org/ns/crypto";
81+
crypto:hash("test", "SHA-256", "hex")
82+
]]></text>
83+
</query>' "http://localhost:8080/exist/rest/db")
84+
echo "$result"
85+
echo "$result" | grep -q "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" || (echo "FAIL: SHA-256 hash mismatch" && exit 1)
86+
87+
- name: Test crypto:hmac with SHA256 hex output
88+
run: |
89+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
90+
<query xmlns="http://exist.sourceforge.net/NS/exist">
91+
<text><![CDATA[
92+
import module namespace crypto = "http://expath.org/ns/crypto";
93+
string-length(crypto:hmac("test", "key", "SHA256", "hex"))
94+
]]></text>
95+
</query>' "http://localhost:8080/exist/rest/db")
96+
echo "$result"
97+
echo "$result" | grep -q ">64<" || (echo "FAIL: expected 64 hex chars" && exit 1)
98+
99+
- name: Test crypto:hmac with base64 output
100+
run: |
101+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
102+
<query xmlns="http://exist.sourceforge.net/NS/exist">
103+
<text><![CDATA[
104+
import module namespace crypto = "http://expath.org/ns/crypto";
105+
string-length(crypto:hmac("test", "key", "SHA256")) > 0
106+
]]></text>
107+
</query>' "http://localhost:8080/exist/rest/db")
108+
echo "$result"
109+
echo "$result" | grep -q "true" || (echo "FAIL: expected non-empty base64" && exit 1)
110+
111+
- name: Test crypto:encrypt and crypto:decrypt round-trip
112+
run: |
113+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
114+
<query xmlns="http://exist.sourceforge.net/NS/exist">
115+
<text><![CDATA[
116+
import module namespace crypto = "http://expath.org/ns/crypto";
117+
let $key := "0123456789abcdef"
118+
let $enc := crypto:encrypt("secret message", "symmetric", $key, "AES")
119+
return crypto:decrypt($enc, "symmetric", $key, "AES")
120+
]]></text>
121+
</query>' "http://localhost:8080/exist/rest/db")
122+
echo "$result"
123+
echo "$result" | grep -q "secret message" || (echo "FAIL: decrypt did not return original" && exit 1)
124+
125+
- name: Test crypto:generate-signature produces Signature element
126+
run: |
127+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
128+
<query xmlns="http://exist.sourceforge.net/NS/exist">
129+
<text><![CDATA[
130+
import module namespace crypto = "http://expath.org/ns/crypto";
131+
let $doc := <root><data>test</data></root>
132+
let $signed := crypto:generate-signature($doc, "inclusive", "SHA256", "RSA_SHA256", "dsig", "enveloped")
133+
return exists($signed//*[local-name() = "Signature"])
134+
]]></text>
135+
</query>' "http://localhost:8080/exist/rest/db")
136+
echo "$result"
137+
echo "$result" | grep -q "true" || (echo "FAIL: expected Signature element" && exit 1)
138+
139+
- name: Test multiple HMAC algorithms
140+
run: |
141+
result=$(curl -sf -u admin: -H "Content-Type: application/xml" --data '
142+
<query xmlns="http://exist.sourceforge.net/NS/exist">
143+
<text><![CDATA[
144+
import module namespace crypto = "http://expath.org/ns/crypto";
145+
string-join(
146+
for $alg in ("MD5", "SHA1", "SHA256", "SHA384", "SHA512")
147+
return string-length(crypto:hmac("test", "key", $alg, "hex")),
148+
","
149+
)
150+
]]></text>
151+
</query>' "http://localhost:8080/exist/rest/db")
152+
echo "$result"
153+
echo "$result" | grep -q "32,40,64,96,128" || (echo "FAIL: expected 32,40,64,96,128" && exit 1)

.gitignore

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
target/
2+
.mvn/
3+
.m2-repo/
4+
.env
5+
*.iml
6+
.idea/
7+
.classpath
8+
.project
9+
.settings/

LICENSE

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
GNU LESSER GENERAL PUBLIC LICENSE
2+
Version 2.1, February 1999
3+
4+
Copyright (C) 1991, 1999 Free Software Foundation, Inc.
5+
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
6+
Everyone is permitted to copy and distribute verbatim copies
7+
of this license document, but changing it is not allowed.
8+
9+
[This is the first released version of the Lesser GPL. It also counts
10+
as the successor of the GNU Library Public License, version 2, hence
11+
the version number 2.1.]

README.md

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
# EXPath Crypto Module for eXist-db
2+
3+
A standalone XAR package implementing the [EXPath Cryptographic Module](http://expath.org/spec/crypto) for eXist-db 7.0+. Provides cryptographic hashing, HMAC authentication, symmetric encryption/decryption, and XML digital signatures using Java's built-in JCE (Java Cryptography Extension) — no external dependencies.
4+
5+
## Install
6+
7+
Download the `.xar` from [Releases](https://github.com/joewiz/exist-crypto/releases) and install with the eXist-db Package Manager or the `xst` CLI:
8+
9+
```bash
10+
xst package install exist-crypto-0.9.0-SNAPSHOT.xar
11+
```
12+
13+
## Functions
14+
15+
| Function | Description |
16+
|----------|-------------|
17+
| `crypto:hash($data, $algorithm)` | Cryptographic hash with Base64 output |
18+
| `crypto:hash($data, $algorithm, $encoding)` | Cryptographic hash with specified encoding (`base64` or `hex`) |
19+
| `crypto:hmac($data, $key, $algorithm)` | HMAC with Base64 output |
20+
| `crypto:hmac($data, $key, $algorithm, $encoding)` | HMAC with specified encoding (`base64` or `hex`) |
21+
| `crypto:encrypt($data, $type, $key, $algorithm)` | Symmetric encryption (AES or DES) |
22+
| `crypto:decrypt($data, $type, $key, $algorithm)` | Symmetric decryption |
23+
| `crypto:generate-signature($data, $c14n, $digest, $sig, $prefix, $type)` | Generate XML digital signature |
24+
| `crypto:validate-signature($data)` | Validate XML digital signature |
25+
26+
**Module namespace:** `http://expath.org/ns/crypto`
27+
28+
### Hash
29+
30+
```xquery
31+
import module namespace crypto = "http://expath.org/ns/crypto";
32+
33+
(: Base64 output (default) :)
34+
crypto:hash("data", "SHA-256")
35+
36+
(: Hex output :)
37+
crypto:hash("data", "SHA-256", "hex")
38+
```
39+
40+
**Algorithms:** MD5, SHA-1, SHA-256, SHA-384, SHA-512
41+
42+
For new code, prefer `fn:hash()` (XQuery 4.0) or `util:hash()`. `crypto:hash` is provided for backward compatibility with the EXPath spec and cross-engine portability (BaseX also implements it).
43+
44+
### HMAC
45+
46+
```xquery
47+
import module namespace crypto = "http://expath.org/ns/crypto";
48+
49+
(: Base64 output (default) :)
50+
crypto:hmac("data", "secret-key", "SHA256")
51+
52+
(: Hex output :)
53+
crypto:hmac("data", "secret-key", "SHA256", "hex")
54+
```
55+
56+
**Algorithms:** MD5, SHA1, SHA256, SHA384, SHA512
57+
58+
### Symmetric Encryption
59+
60+
```xquery
61+
import module namespace crypto = "http://expath.org/ns/crypto";
62+
63+
let $key := "0123456789abcdef" (: 16 bytes = AES-128 :)
64+
let $encrypted := crypto:encrypt("secret message", "symmetric", $key, "AES")
65+
return crypto:decrypt($encrypted, "symmetric", $key, "AES")
66+
```
67+
68+
**Algorithms:** AES (16/24/32-byte key), DES (8-byte key)
69+
70+
A random initialization vector (IV) is automatically generated and prepended to the ciphertext.
71+
72+
### XML Digital Signatures
73+
74+
```xquery
75+
import module namespace crypto = "http://expath.org/ns/crypto";
76+
77+
let $doc := <order id="123"><total>99.99</total></order>
78+
let $signed := crypto:generate-signature(
79+
$doc, "inclusive", "SHA256", "RSA_SHA256", "dsig", "enveloped")
80+
return crypto:validate-signature($signed)
81+
```
82+
83+
## Relationship to expath-crypto-module
84+
85+
This package is the **successor** to [`expath-crypto-module`](https://github.com/eXist-db/expath-crypto-module) (originally by Claudius Teodorescu, last updated 2020). Both implement the same [EXPath Cryptographic Module](http://expath.org/spec/crypto) specification and share the same module namespace and `crypto:` prefix — so existing XQuery code using `crypto:` functions will work with this package. However, there are some behavior differences (see migration guide below).
86+
87+
The two packages **cannot be installed simultaneously** because they register the same module namespace (`http://expath.org/ns/crypto`). A `pre-install.xq` script automatically detects and removes the old package when you install this one.
88+
89+
### Why switch?
90+
91+
| | Predecessor (`expath-crypto-module`) | Successor (`exist-crypto`) |
92+
|---|---|---|
93+
| **eXist-db compatibility** | 5.x–6.x (broken on 7.x due to Type constant renumbering) | 7.0+ |
94+
| **Java version** | Java 8+ | Java 21+ |
95+
| **Dependencies** | External `crypto-java` library (ro.kuberam) | Zero — uses Java's built-in JCE |
96+
| **Parent POM** | `exist-apps-parent` 1.11.0 | `exist-apps-parent` 2.0.0 |
97+
| **`crypto:hash`** | Yes (2–3 arity) | Yes (2–3 arity, compatible) |
98+
99+
### Upgrade steps
100+
101+
1. **Install the new package** — the `pre-install.xq` script automatically detects and removes the old `expath-crypto-module`:
102+
103+
```bash
104+
xst package install exist-crypto-0.9.0-SNAPSHOT.xar
105+
```
106+
107+
To remove the old package manually beforehand:
108+
109+
```bash
110+
xst package remove http://expath.org/ns/crypto
111+
```
112+
113+
2. **Review your XQuery code** — see the migration guide below. Most code will work without changes.
114+
115+
### Migration guide
116+
117+
#### No changes needed
118+
119+
The module namespace is identical, so **import statements require no changes**:
120+
121+
```xquery
122+
import module namespace crypto = "http://expath.org/ns/crypto";
123+
```
124+
125+
Calls to `crypto:hash`, `crypto:hmac`, and `crypto:validate-signature` are compatible as-is for the most common usage patterns.
126+
127+
#### `crypto:hmac` — return type changed
128+
129+
| | Predecessor | Successor |
130+
|---|---|---|
131+
| **Parameter types** | `$data as xs:atomic*`, `$key as xs:atomic*` | `$data as xs:string`, `$key as xs:string` |
132+
| **Return type** | `xs:byte*` | `xs:string` |
133+
134+
The predecessor accepted binary types and returned a byte sequence. The successor accepts strings and returns a string (Base64 or hex). If your code passes `xs:string` arguments (the common case), no changes are needed.
135+
136+
**If you passed binary data:** Convert to string first with `util:binary-to-string()`.
137+
138+
#### `crypto:encrypt` / `crypto:decrypt` — IV handling changed
139+
140+
| | Predecessor | Successor |
141+
|---|---|---|
142+
| **IV parameter** | Optional 5th argument (`xs:base64Binary`) | Automatic (random IV prepended to ciphertext) |
143+
| **Provider parameter** | Optional 6th argument | Not supported |
144+
| **Arity** | 4–6 | 4 |
145+
146+
The successor always generates a random IV and prepends it to the ciphertext. The decrypt function extracts the IV automatically. This is more secure than reusing IVs but means **ciphertext from the predecessor cannot be decrypted by the successor** (and vice versa) unless the IV handling is compatible.
147+
148+
**If you stored encrypted data:** You will need to re-encrypt data during migration. Decrypt with the predecessor first, then re-encrypt with this package.
149+
150+
**If you used a custom provider:** Remove the provider argument. Java's default JCE provider is used.
151+
152+
#### `crypto:generate-signature` — subset of arities
153+
154+
| | Predecessor | Successor |
155+
|---|---|---|
156+
| **Arities** | 6, 7 (XPath), 8 (certificate), 3 (private key) | 6 |
157+
| **Signature algorithms** | RSA\_SHA1, DSA\_SHA1 | RSA\_SHA1, DSA\_SHA1, RSA\_SHA256 |
158+
159+
The 6-argument form is compatible. The XPath subsetting (7-arg), certificate (8-arg), and private key (3-arg) variants are not yet implemented.
160+
161+
**If you used the 6-argument form:** No changes needed.
162+
163+
**If you used XPath subsetting or certificates:** Not yet available. File an issue if you need them.
164+
165+
#### `crypto:hash` — return type changed
166+
167+
| | Predecessor | Successor |
168+
|---|---|---|
169+
| **Parameter types** | `$data as item()*` | `$data as item()` |
170+
| **Return type** | `xs:byte*` | `xs:string` |
171+
172+
The predecessor returned a byte sequence. The successor returns a string (Base64 or hex encoded). The function signatures and algorithm names are otherwise compatible.
173+
174+
For new code, prefer `fn:hash()` (XQuery 4.0) or `util:hash()`. `crypto:hash` is provided for EXPath spec conformance and backward compatibility (BaseX also implements it).
175+
176+
### Package identity
177+
178+
| Property | Predecessor | Successor |
179+
|---|---|---|
180+
| Package name (URI) | `http://expath.org/ns/crypto` | `http://exist-db.org/pkg/crypto` |
181+
| Package abbreviation | `crypto` | `exist-crypto` |
182+
| Module namespace | `http://expath.org/ns/crypto` | `http://expath.org/ns/crypto` |
183+
| XQuery prefix | `crypto:` | `crypto:` |
184+
| Maven groupId | `org.exist-db.xquery.extensions.expath` | `org.exist-db` |
185+
| Maven artifactId | `expath-crypto-module` | `exist-crypto` |
186+
| Java package | `org.expath.exist.crypto` | `org.exist.xquery.modules.crypto` |
187+
188+
The module namespace and `crypto:` prefix are identical — no XQuery import changes needed. The package abbreviations differ (`crypto` vs `exist-crypto`), but the `pre-install.xq` script handles the transition automatically.
189+
190+
## Build
191+
192+
```bash
193+
JAVA_HOME=/path/to/java-21 mvn clean package -DskipTests
194+
```
195+
196+
Run integration tests (requires exist-core 7.0.0-SNAPSHOT in your local Maven repo):
197+
198+
```bash
199+
mvn test -Pintegration-tests
200+
```
201+
202+
## License
203+
204+
[GNU Lesser General Public License v2.1](https://opensource.org/licenses/LGPL-2.1)

0 commit comments

Comments
 (0)