aurabx / jmix
PHP library for JMIX (JSON Medical Interchange) format - secure medical data exchange with cryptographic features
Installs: 0
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 0
Forks: 0
Open Issues: 0
pkg:composer/aurabx/jmix
Requires
- php: ^8.2
- ext-json: *
- ext-openssl: *
- ext-sodium: *
- justinrainbow/json-schema: ^5.2
- ramsey/uuid: ^4.7
Requires (Dev)
- phpstan/phpstan: ^1.10
- phpunit/phpunit: ^10.0
- squizlabs/php_codesniffer: ^3.7
- vimeo/psalm: ^5.0
README
A PHP library for creating and processing JMIX (JSON Medical Interchange) envelopes from DICOM files. JMIX is a secure data format for exchanging medical/healthcare information with strong cryptographic features including AES-256-GCM encryption.
Features
- Convert DICOM folders to complete JMIX envelopes
- Automatic metadata extraction from DICOM files
- JSON Schema validation for all components
- Simple, array-based configuration
- AES-256-GCM payload encryption with ECDH key exchange (Curve25519)
- Payload decryption and envelope extraction
- SHA-256 payload hash verification for data integrity
- Built-in audit trail generation
- CLI tools for building, analyzing, and decrypting envelopes
- Ephemeral keys for forward secrecy
Requirements
In order to extract file information to build the files list, DCMTK is required. Without this, the library will not extract DICOM data.
# Install DCMTK on macOS brew install dcmtk # Install DCMTK on Ubuntu apt-get install dcmtk
The library will automatically use dcmdump
if available.
Installation
composer require aurabx/jmix
Quick Start
<?php require_once 'vendor/autoload.php'; use AuraBox\Jmix\JmixBuilder; // Configuration array $config = [ 'sender' => [ 'name' => 'Radiology Clinic A', 'id' => 'org:au.gov.health.123456', 'contact' => 'imaging@clinica.org.au', ], 'requester' => [ 'name' => 'Dr John Smith', 'id' => 'org:au.gov.health.55555', 'contact' => 'smith@clinicb.org.au', ], 'receivers' => [[ 'name' => 'Radiology Clinic B', 'id' => 'org:au.gov.health.987654', 'contact' => ['system' => 'phone', 'value' => '+61049555555'], ]], 'patient' => [ 'name' => 'Jane Doe', 'dob' => '1975-02-14', 'sex' => 'F', 'ihi' => '8003608166690503', ], ]; // Build JMIX envelope $jmixBuilder = new JmixBuilder(); $envelope = $jmixBuilder->buildFromDicom('/path/to/dicom/files', $config); // Save to JMIX envelope directory $envelopePath = $jmixBuilder->saveToFiles($envelope, '/path/to/output', $config); echo "Envelope created at: {$envelopePath}\n"; // Example with encryption (optional) // Generate keypair for encryption use AuraBox\Jmix\Encryption\PayloadEncryptor; $keypair = PayloadEncryptor::generateKeypair(); // Add encryption to config $config['encryption'] = [ 'recipient_public_key' => $keypair['public_key'] ]; // Build encrypted envelope $encryptedEnvelope = $jmixBuilder->buildFromDicom('/path/to/dicom/files', $config); $encryptedPath = $jmixBuilder->saveToFiles($encryptedEnvelope, '/path/to/output', $config); echo "🔒 Encrypted envelope created at: {$encryptedPath}\n";
Encryption and Decryption
The library supports enterprise-grade AES-256-GCM encryption with ECDH key exchange for secure medical data transmission.
Generating Keys
use AuraBox\Jmix\Encryption\PayloadEncryptor; // Generate a keypair for testing/development $keypair = PayloadEncryptor::generateKeypair(); $publicKey = $keypair['public_key']; // For encryption (share with sender) $privateKey = $keypair['private_key']; // For decryption (keep secure!) echo "Public Key: " . $publicKey . "\n"; echo "Private Key: " . $privateKey . "\n";
Creating Encrypted Envelopes
use AuraBox\Jmix\JmixBuilder; use AuraBox\Jmix\Encryption\PayloadEncryptor; // Your regular configuration $config = [ 'sender' => ['name' => 'Clinic A', 'id' => 'org:clinic.a', 'contact' => 'info@clinica.com'], 'requester' => ['name' => 'Dr Smith', 'id' => 'doc:smith', 'contact' => 'smith@clinic.com'], 'receivers' => [['name' => 'Clinic B', 'id' => 'org:clinic.b', 'contact' => 'info@clinicb.com']], 'patient' => ['name' => 'Jane Doe', 'dob' => '1975-02-14', 'sex' => 'F'], // ... other config ]; // Add encryption $config['encryption'] = [ 'recipient_public_key' => $recipientPublicKey // Recipient's public key ]; // Build encrypted envelope $jmixBuilder = new JmixBuilder(); $envelope = $jmixBuilder->buildFromDicom('/path/to/dicom', $config); $envelopePath = $jmixBuilder->saveToFiles($envelope, '/path/to/output', $config); echo "🔒 Encrypted envelope created at: {$envelopePath}\n";
Analyzing and Decrypting Envelopes
use AuraBox\Jmix\JmixDecryptor; $decryptor = new JmixDecryptor(); // 1. Analyze envelope (without extracting) $analysis = $decryptor->analyzeEnvelope('/path/to/envelope.JMIX'); echo "Envelope ID: " . $analysis['envelope_id'] . "\n"; echo "Encrypted: " . ($analysis['is_encrypted'] ? 'Yes' : 'No') . "\n"; echo "Patient: " . $analysis['sender']['name'] . "\n"; if ($analysis['is_encrypted']) { echo "Encryption: " . $analysis['encryption']['algorithm'] . "\n"; } // 2. Decrypt encrypted envelope if ($analysis['is_encrypted']) { $envelope = $decryptor->decryptEnvelope( '/path/to/encrypted.JMIX', $privateKey, // Your private key '/path/to/output' ); echo "🔓 Decrypted envelope contents:\n"; echo "Patient: " . $envelope['metadata']['patient']['name']['text'] . "\n"; echo "DICOM files: " . $envelope['payload_path'] . '/dicom/' . "\n"; } // 3. Extract unencrypted envelope else { $envelope = $decryptor->extractEnvelope( '/path/to/unencrypted.JMIX', '/path/to/output' ); echo "Extracted envelope contents:\n"; echo "Patient: " . $envelope['metadata']['patient']['name']['text'] . "\n"; echo "DICOM files: " . $envelope['payload_path'] . '/dicom/' . "\n"; }
CLI Tools
The library includes command-line tools for building and processing envelopes:
Building Envelopes
# Create unencrypted envelope jmix-build /path/to/dicom config.json /path/to/output # Create unencrypted envelope with a custom schema directory jmix-build /path/to/dicom config.json /path/to/output /absolute/or/relative/path/to/schemas # Create encrypted envelope (add encryption config to config.json) jmix-build /path/to/dicom encrypted-config.json /path/to/output
Analyzing Envelopes
# Analyze any envelope (shows encryption status, metadata, etc.) jmix-decrypt analyze /path/to/envelope.JMIX # Analyze and verify cryptographic assertions jmix-decrypt analyze /path/to/envelope.JMIX --verify-assertions
Output:
Envelope Analysis
ID: a1b2c3d4-5678-90ab-cdef-123456789abc
Timestamp: 2025-09-27T06:32:05Z
Encrypted: 🔒 Yes
Has Payload Hash: ✓ Yes
Sender:
Name: Test Healthcare Organization
ID: org:test.health.123
Encryption Details:
Algorithm: AES-256-GCM
Ephemeral Public Key: Y12JovXD3Hjc/mMk...
Extracting Unencrypted Envelopes
# Extract unencrypted envelope
jmix-decrypt extract /path/to/envelope.JMIX /path/to/output
Decrypting Encrypted Envelopes
# Decrypt encrypted envelope jmix-decrypt decrypt /path/to/encrypted.JMIX /path/to/output <private-key-base64>
Output:
✓ Envelope decrypted successfully!
🔓 Decrypted Envelope Contents:
ID: a1b2c3d4-5678-90ab-cdef-123456789abc
Patient: Jane Doe
Study: CT Pulmonary Angiogram
Payload Path: /path/to/output/payload
📁 Extracted Files:
- DICOM files in: /path/to/output/payload/dicom/
- Attachment files in: /path/to/output/payload/files/
Configuration
Schema Validation and Schema Path
- All generated JMIX components are validated against JSON Schemas: manifest.json, metadata.json, audit.json, and files.json (when present).
- Default schema directory resolves to a sibling repository path: ../jmix/schemas (relative to this package).
- You can override the schema path in both library and CLI:
- Library: new JmixBuilder('/path/to/schemas') and new JmixDecryptor('/path/to/schemas')
- CLI: jmix-build /dicom config.json /output /path/to/schemas
If you keep the JMIX schema repository checked out one directory up, the defaults will work out of the box.
Required Configuration
$config = [ 'sender' => [ 'name' => 'Clinic Name', 'id' => 'org:identifier', 'contact' => 'email@clinic.com', ], 'requester' => [ 'name' => 'Doctor Name', 'id' => 'org:doctor.id', 'contact' => 'doctor@clinic.com', ], 'receivers' => [ [ 'name' => 'Receiving Clinic', 'id' => 'org:receiver.id', 'contact' => 'receiver@clinic.com', ], ], 'patient' => [ 'name' => 'Patient Name', 'dob' => '1975-02-14', 'sex' => 'F', 'ihi' => '8003608166690503', // Australian Individual Healthcare Identifier ], ];
Optional Configuration
$config = [ // ... required fields above 'custom_tags' => ['teaching', 'priority-review'], 'security' => [ 'classification' => 'confidential', // or 'restricted', 'public' ], // Encryption (optional) 'encryption' => [ 'recipient_public_key' => '<base64-encoded-public-key>', // Recipient's Curve25519 public key ], 'report' => [ 'file' => 'files/report.pdf', 'url' => 'https://example.com/report', ], 'files' => [ 'file' => 'files/images.zip', 'url' => 'https://example.com/images', ], 'consent' => [ 'status' => 'granted', 'scope' => ['treatment', 'research'], 'method' => 'digital-signature', ], 'deid_keys' => ['PatientName', 'PatientID', 'IssuerOfPatientID'], // Cryptographic assertions (for production use) 'sender' => [ // ... other fields 'assertion' => [ 'alg' => 'Ed25519', 'public_key' => '<base64_encoded_public_key>', 'fingerprint' => 'SHA256:<hex_fingerprint>', 'key_reference' => 'aurabox://org/clinic#key-ed25519', 'signature' => '<base64_signature>', 'expires_at' => '2025-12-31T23:59:59Z', ], ], ];
Output Structure
After processing, you'll have a JMIX envelope directory. The structure depends on whether encryption was used:
Unencrypted Envelope
<envelope-id>.JMIX/
├── manifest.json # Security & routing metadata (includes payload_hash)
├── audit.json # Audit trail
└── payload/
├── metadata.json # Medical data & patient info
├── dicom/ # DICOM files (copied from source)
│ ├── series_1/
│ │ ├── CT.1.1.dcm
│ │ └── ...
│ └── series_2/
│ └── ...
├── files/ # Optional: report files and attachments
│ └── report.pdf
└── files.json # File manifest (when files/ present)
Encrypted Envelope
<envelope-id>.JMIX/
├── manifest.json # Security & routing metadata + encryption parameters
├── audit.json # Audit trail
└── payload.encrypted # AES-256-GCM encrypted TAR archive of payload/
The manifest.json
in encrypted envelopes includes encryption details:
{ "security": { "payload_hash": "sha256:abc123...", "encryption": { "algorithm": "AES-256-GCM", "ephemeral_public_key": "Y12JovXD3Hjc...", "iv": "uTv8nI6Wi/a/O7wc", "auth_tag": "5c8VZzxSuWM3RMqA..." } } }
Each file is validated against its respective JSON schema before being saved.
Error Handling
The library provides specific exception types:
use AuraBox\Jmix\Exceptions\{JmixException, ValidationException, CryptographyException}; try { $envelope = $jmixBuilder->buildFromDicom($dicomPath, $config); } catch (ValidationException $e) { echo "Validation failed: " . $e->getMessage() . "\n"; foreach ($e->getErrors() as $error) { echo " - $error\n"; } } catch (JmixException $e) { echo "JMIX error: " . $e->getMessage() . "\n"; }
Development
Requirements
- PHP 8.1+
- ext-json
- ext-openssl
- ext-sodium (for encryption/decryption)
- Composer
- Optional: DCMTK's
dcmdump
for enhanced DICOM metadata extraction
Setup
git clone https://github.com/aurabx/jmix-php
cd jmix-php/php-library
composer install
Testing
Samples are available under ./samples for demos and tests. Prefer writing to ./tmp per examples.
# Run all tests composer test # Run specific test files vendor/bin/phpunit tests/JmixBuilderTest.php vendor/bin/phpunit tests/JmixDecryptorTest.php vendor/bin/phpunit tests/Encryption/PayloadEncryptorTest.php # Run tests with coverage (requires Xdebug) XDEBUG_MODE=coverage vendor/bin/phpunit # Test CLI tools bin/jmix-build ./samples/study_1 ./examples/sample-config.json ./tmp/test-output bin/jmix-decrypt analyze ./tmp/test-output/*.JMIX
Code Quality
composer cs-check # Check coding standards composer cs-fix # Fix coding standards composer phpstan # Static analysis composer psalm # Additional static analysis
Security Considerations
JWS Manifest Signing (optional)
You can sign manifest.json with a compact JWS (EdDSA/Ed25519) for integrity and authenticity.
- Add jws_signing_key (base64 Ed25519 private key) to your build configuration
- The builder will add a security.jws reference and emit a manifest.jws file when saving
- Verify with JwsHandler::verifyJws(jws, publicKey)
Example (library):
use AuraBox\Jmix\JmixBuilder; $config = [ // ... your existing config 'jws_signing_key' => '<base64-ed25519-private-key>', ]; $builder = new JmixBuilder(); $envelope = $builder->buildFromDicom('/path/to/dicom', $config); $path = $builder->saveToFiles($envelope, './tmp/output', $config); // Outputs manifest.json and manifest.jws
✅ Production-Ready Security Features
This library includes enterprise-grade encryption that is production-ready:
- AES-256-GCM encryption with authenticated encryption
- ECDH key exchange using Curve25519 elliptic curve
- HKDF key derivation with SHA-256
- Forward secrecy through ephemeral keypairs
- Payload integrity verification with SHA-256 hashing
- Memory safety with secure key clearing
⚠️ Additional Production Considerations
For production use, you should also consider:
-
Key Management:
- Use hardware security modules (HSMs) for key storage
- Implement proper key rotation and expiration handling
- Secure key distribution mechanisms
-
Digital Signatures (currently placeholders):
- Integrate with
web-token/jwt-framework
for JWT/JWS signatures - Use
paragonie/constant_time_encoding
for secure encoding
- Integrate with
-
Certificate Authority Integration:
- Integrate with a certificate authority for directory attestations
- Implement certificate validation and revocation checking
-
Compliance:
- Ensure compliance with healthcare data regulations (HIPAA, GDPR, etc.)
- Implement audit logging for all cryptographic operations
- Regular security assessments and penetration testing
Contributing
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
License
This project is licensed under the MIT License - see the LICENSE file for details.
Support
- Issues: GitHub Issues
See CHANGELOG.md for the full history, including 0.3.0, 0.2.0, and 0.1.0 entries.