Homemade decrypt submission data

Hello,

i use odk collect just on smartphone side and on server side i use my own code on php to intercept and process data.
This system complete our web application with a powerfull smartphone collect solution.
It's working prefectly for several years.
Actually i test cypher on client side, everything works client side, i try collect for a simple form. I recup all transfert file, it's ok
submission.xml.enc
and the cyper dataform myform.xml

now i would like to decrypt manually the data form submission.xml.enc

I try to decrypt the symmetric encryption key find between tag "base64EncryptedKey" with my Private RSA key,
but i don't find the good expression to use on shell with openssl

i paste the base64SymmetricKey in a file and base64 decode it, base64SymmetricKey contains the text between tag "base64EncryptedKey"
cat SymmetricKey64.txt| base64 -d > SymmetricKey.txt

And i try to recup the SymmetricKey

openssl rsautl -decrypt -in SymmetricKey.txt -out SymmetricKeyClear.txt -inkey MyPrivateKey.pem

but it does not work.

Has anyone already performed this operation ?
with openssl command on linux shell or in php

Thanks

J'ai résolu mon test 5 ans plus tard à l'aide d'un LLM,
qui portait sur le déchiffrement d'un formulaire chiffré sur un serveur homemade odk en php.

Cause :
PHP (via OpenSSL) utilise SHA1 pour OAEP (mgf1_md:sha1)
ODK utilise SHA256 dans son chiffrement OAEP : mgf1_md:sha256
source : collect-2025.3.0-beta.4\collect_app\src\main\java\org\odk\collect\android\utilities\EncryptionUtils.java
lignes :

public class EncryptionUtils {
    public static final String RSA_ALGORITHM = "RSA";
    // the symmetric key we are encrypting with RSA is only 256 bits... use SHA-256
    public static final String ASYMMETRIC_ALGORITHM = "RSA/NONE/OAEPWithSHA256AndMGF1Padding";
    public static final String SYMMETRIC_ALGORITHM = "AES/CFB/PKCS5Padding";

Problème :
PHP ne permet pas actuellement (sans passer par une librairie C ou OpenSSL CLI) de spécifier ce paramètre dans openssl_private_decrypt().
On peut faire :
openssl_private_decrypt($data, $output, $privateKey, OPENSSL_PKCS1_OAEP_PADDING);
Mais on ne peux pas dire : utilise OAEP avec SHA256 au lieu de SHA1.

ODK Collect
│
│ Formulaire rempli
â–Ľ
Serveur
│ 1. Fichier XML avec clé RSA chiffrée
│ 2. Fichier chiffré AES (submission.xml.enc)
â–Ľ
PHP/Bash
│
├─ Déchiffrement RSA (OAEP-SHA256) -> clé AES
└─ Déchiffrement AES -> XML final

Rappel :
on crée côté serveur une clé privée et une clé publique.

-- Générer une clé privée RSA (format PEM)
openssl genpkey -algorithm RSA -out private.pem -pkeyopt rsa_keygen_bits:2048
-- En extraire la clé publique au format PEM
openssl rsa -in private.pem -pubout -out public.pem
-- Format base64 en une ligne pour ODK Collect
openssl rsa -in private.pem -pubout -outform DER | base64 -w 0 > public.der.base64

On utilise cette clé publique "public.der.base64" dans la balise de notre formulaire vierge : <submission base64RsaPublicKey=

<?xml version="1.0"?>
<h:html xmlns="http://www.w3.org/2002/xforms"
        xmlns:h="http://www.w3.org/1999/xhtml"
        xmlns:ev="http://www.w3.org/2001/xml-events"
        xmlns:xsd="http://www.w3.org/2001/XMLSchema"
        xmlns:jr="http://openrosa.org/javarosa"
        xmlns:orx="http://openrosa.org/xforms/">
	<h:head>
		<h:title>ControleBateau</h:title>
		<model>
			<instance>
				<data id="ControleBateau"
				      version='2025A1'>
					<orx:meta>
						<orx:instanceID/>
					</orx:meta>
					<Bateau/>
				</data>
			</instance>
			<submission base64RsaPublicKey="MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA4oauktY7DD4ztBVnAZjrNilpGJh9GHNZyJOPz9xfHzFFYoCqRqKQVWIqR0L4D5pJubQJ+e+aNvkESa9kPfKaaEcs6CiV4dDsuPWZH7G87MPDlf62IkmJG3jji2H/mnjHMCi0+xy/tIuqPuXMGucD8bFCdI59c/92nkJzFx8mp+I/chpC+n7BDDSsILxu8m3UGwccIvTr6Odf9svq7bDXln8QbsSt1Q23n5nxUgAwXyuLAVLORITZTRmvNJ+SDn5G4lH4J/QfI7OUcuKryhRVpbKTj9Kd9E8Ll9HPmt+W4HdJ2YaOGHrhO79nwYN+V4LAbYehghWhsavkkcDLPo3yJQIDAQAB"/>
			<bind nodeset="/data/meta/instanceID"
			      type="string"
			      readonly="true()"
			      calculate="concat('uuid:', uuid())"/>
			<bind nodeset="/data/Bateau"
			      type="string"
			      required="true()"/>
		</model>
	</h:head>
	<h:body>
		<input ref="/data/Bateau">
			<label>Saisir le nom du bateau</label>
		</input>
	</h:body>
</h:html>

Une fois le formulaire rempli et envoyé,
côté serveur je récupère 2 fichiers (si pas de média associé) :
le formulaire chiffré toujours nommé : submission.xml.enc
le fichier ControleBateau_2025-09-15_13-40-13.xml qui contient le hash et la clé de déchiffrement, chiffré avec la clé publique dans <base64EncryptedKey>

<data id="ControleBateau" version="2025A1" encrypted="yes" xmlns="http://www.opendatakit.org/xforms/encrypted">
<base64EncryptedKey>A1f1BzV4L7i2gwPbQs5YOPzWmv8IsSsxXJFNTm69Z1rGAExZkel8iG/HU4Blzo0mMWuahGFqwCjOdOKt3xy6z+t1bkXYdVwF8nYtBEw+nXpeU07JAJ6W/4ocpSMAK6P5YsZyAe2OqobfFO5V6qQc7LREPQYzKotOP80dwBHnIp+BtVQRoa8gkrXWmzcff09oyw8F56YSin4zH6NtJV0IDaOcOpOV9NDnpwQt5H1JZx/DGvVtZcVAIjLBLrHb2b5fA2w74j+cW/ZOhf9CUShy+5sUz98/gP6KDG9RQpur2Ly3EeOkVbvc0kMSDx6nfbydzdQV6aMxasMoNukjxrONMw==</base64EncryptedKey>
<orx:meta xmlns:orx="http://openrosa.org/xforms">
<orx:instanceID>uuid:20ebf70a-1b5c-4efc-b592-5c3c5315a9fa</orx:instanceID>
</orx:meta>
<encryptedXmlFile>submission.xml.enc</encryptedXmlFile>
<base64EncryptedElementSignature>PVJwfE0pYjd/51Y/0nAjpsPTuTpHh5bdz+C4lP6/Ldrdev6G8USBYCRrpPVMCQMV0d1maTahkNiE7LfPUYqCOnuy1QdDKOWl/VjPhyydx7QcyrYNETDHUHaodU1Zpt3FSLa2H8jRPBClyM6p7I1w5DTxDqQ0MhaZhJ/zSyCkqXkWsuLiqpjJCV5vXsLiQyO7uLXuQdzNhgUTm0CLUizxKoBo8PLnIiivNciaamzd93BmApBpdf4ie52HSEMlD72UnXEfnFmAaAVdptDI4rnTxZ71GgMPajDFJAF2OBN26UVKxi0Y8mfr2Znv9O8WJSrMNUoxSEYcQakxcZbBgR9RZw==</base64EncryptedElementSignature>
</data>

Pour déchiffrer le formulaire submission.xml.enc
avec des commandes bash :

# --- fichiers (adapter) ---
PRIVATE_KEY="./private.pem"
XML_SUBMISSION="./ControleBateau_2025-09-15_13-40-13.xml"
ENCRYPTED_FILE="./submission.xml.enc"

# --- 1) extraire <base64EncryptedKey> du XML (en binaire décodé) ---
# écrit dans enc_key.bin (binaire)
grep -oP '(?<=<base64EncryptedKey>)[^<]+' "$XML_SUBMISSION" | tr -d '\r\n' | base64 -d > enc_key.bin
# vérifier
if [ ! -s enc_key.bin ]; then echo "Erreur: enc_key.bin vide"; exit 1; fi

# --- 2) déchiffrer la clé AES avec openssl pkeyutl (RSA OAEP SHA256) ---
# sortie décodée dans dec_aes_key.bin (doit faire 32 octets)
openssl pkeyutl -decrypt \
  -inkey "$PRIVATE_KEY" \
  -in enc_key.bin \
  -out dec_aes_key.bin \
  -pkeyopt rsa_padding_mode:oaep \
  -pkeyopt rsa_oaep_md:sha256 \
  -pkeyopt rsa_mgf1_md:sha256

# vérifier taille (32 octets attendu)
[ "$(stat -c%s dec_aes_key.bin)" -eq 32 ] || { echo "Erreur: clé AES attendue 32 octets mais taille=$(stat -c%s dec_aes_key.bin)"; hexdump -C dec_aes_key.bin; exit 1; }

# --- 3) extraire IV (premiers 16 octets) et ciphertext (le reste) ---
# iv.bin (16 bytes), cipher.bin (reste)
dd if="$ENCRYPTED_FILE" bs=1 count=16 of=iv.bin status=none
dd if="$ENCRYPTED_FILE" bs=1 skip=16 of=cipher.bin status=none

# afficher hex pour debug
echo "IV (hex): $(xxd -p iv.bin)"
echo "Key (hex): $(xxd -p dec_aes_key.bin)"

# --- 4) openssl enc attend clé/iv en hex, on les convertit ---
KEY_HEX=$(xxd -p dec_aes_key.bin | tr -d '\n')
IV_HEX=$(xxd -p iv.bin | tr -d '\n')

# --- 5) déchiffrer AES-256-CFB (raw data) ---
# résultat brut dans plain.bin
openssl enc -d -aes-256-cfb -in cipher.bin -out plain.bin -K "$KEY_HEX" -iv "$IV_HEX"

# vérifier qu'on a obtenu quelque chose
if [ ! -s plain.bin ]; then echo "Erreur: déchiffrement AES a produit fichier vide"; exit 1; fi

# --- 6) corriger début & fin : garder uniquement la section <data ...>...</data> ---
# On traite le binaire comme texte (utile si le XML est UTF-8)
# Utiliser perl en mode slurp pour extraire la première occurrence <data...>...</data>
perl -0777 -ne 'if (/<data\b.*?<\/data>/s) { print $& }' plain.bin > data_only.xml

# vérifier
if [ ! -s data_only.xml ]; then echo "Erreur: balise <data> introuvable dans le flux déchiffré"; exit 1; fi

# ajouter l'en-tĂŞte XML proprement
printf "<?xml version='1.0' encoding='UTF-8'?>\n" > final.xml
cat data_only.xml >> final.xml

# --- 7) affichage / vérifications finales ---
echo "✅ Déchiffrement terminé. Fichier final : final.xml"
echo "Taille final.xml : $(stat -c%s final.xml) octets"
# facultatif : afficher un extrait
echo "---- début du fichier ----"
sed -n '1,80p' final.xml
echo "---- fin de l'extrait ----"

# --- 8) nettoyage (optionnel) ---
# rm -f enc_key.bin dec_aes_key.bin iv.bin cipher.bin plain.bin data_only.xml

En php :



// ----------------- CONFIG -----------------
$privateKeyFile   = './private.pem';
$xmlSubmission    = './ControleBateau_2025-09-15_13-40-13.xml';
$encryptedXmlFile = './submission.xml.enc';
// ------------------------------------------

// Nettoyage fichiers temporaires
function cleanupFiles(array $files) {
    foreach ($files as $f) {
        if (file_exists($f)) @unlink($f);
    }
}

// ----------------------------------------------------
// 1) Lecture du XML et extraction de la clé RSA chiffrée
// ----------------------------------------------------
if (!file_exists($xmlSubmission)) {
    die("Erreur : fichier XML $xmlSubmission introuvable<br/>");
}

$xmlContent = file_get_contents($xmlSubmission);
if (!preg_match('/<base64EncryptedKey>([^<]+)<\/base64EncryptedKey>/', $xmlContent, $matches)) {
    die("Erreur : balise <base64EncryptedKey> introuvable dans le XML<br/>");
}
$base64EncryptedKey = trim($matches[1]);

echo "✅ Clé RSA chiffrée extraite depuis le XML<br/>";

// ----------------------------------------------------
// 2) Décoder base64 -> binaire
// ----------------------------------------------------
$encryptedKeyBin = base64_decode($base64EncryptedKey, true);
if ($encryptedKeyBin === false) {
    die("Erreur : base64 invalide pour la clé chiffrée<br/>");
}

// ----------------------------------------------------
// 3) Déchiffrement de la clé AES avec RSA-OAEP-SHA256
// ----------------------------------------------------
$tmpDir = sys_get_temp_dir();
$encKeyFile = tempnam($tmpDir, 'odk_enckey_');
$decKeyFile = tempnam($tmpDir, 'odk_decakey_');

file_put_contents($encKeyFile, $encryptedKeyBin);


//PHP a bien une extension OpenSSL (openssl_private_decrypt()), mais Tu ne peux pas facilement spécifier mgf1_md:sha256.
$cmd = sprintf(
    'openssl pkeyutl -decrypt -inkey %s -in %s -out %s ' .
    '-pkeyopt rsa_padding_mode:oaep -pkeyopt rsa_oaep_md:sha256 -pkeyopt rsa_mgf1_md:sha256 2>&1',
    escapeshellarg($privateKeyFile),
    escapeshellarg($encKeyFile),
    escapeshellarg($decKeyFile)
);

exec($cmd, $cmdOut, $ret);
if ($ret !== 0) {
    cleanupFiles([$encKeyFile, $decKeyFile]);
    die("Erreur openssl pkeyutl (code $ret):<br/>" . implode("<br/>", $cmdOut) . "<br/>");
}

// Lire la clé AES déchiffrée
$decryptedAesKey = @file_get_contents($decKeyFile);
cleanupFiles([$encKeyFile, $decKeyFile]);

if ($decryptedAesKey === false || strlen($decryptedAesKey) !== 32) {
    echo "Clé brute hex : " . bin2hex($decryptedAesKey) . "<br/>";
    die("Erreur : clé AES invalide. Taille attendue : 32 octets, obtenu : " . strlen($decryptedAesKey) . "<br/>");
}

echo "✅ Clé AES déchiffrée : " . bin2hex($decryptedAesKey) . "<br/>";

// ----------------------------------------------------
// 4) Lecture du fichier chiffré et extraction IV + cipher
// ----------------------------------------------------
$encData = file_get_contents($encryptedXmlFile);
if ($encData === false || strlen($encData) <= 16) {
    die("Erreur : fichier chiffré invalide ou vide<br/>");
}

$iv = substr($encData, 0, 16);
$cipher = substr($encData, 16);

echo "IV (hex) : " . bin2hex($iv) . "<br/>";
echo "Cipher length : " . strlen($cipher) . " octets<br/>";

// ----------------------------------------------------
// 5) Déchiffrement AES-256-CFB
// ----------------------------------------------------
$plain = openssl_decrypt($cipher, 'aes-256-cfb', $decryptedAesKey, OPENSSL_RAW_DATA, $iv);

if ($plain === false) {
    echo "Erreur : déchiffrement AES échoué<br/>";
    echo "Erreur OpenSSL : " . openssl_error_string() . "<br/>";
    exit(1);
}


// ----------------------------------------------------
// 6) Nettoyage du flux
// ----------------------------------------------------

// Supprimer tout avant <data
$startPos = strpos($plain, '<data');
if ($startPos === false) {
    cleanupFiles([$encKeyFile, $decKeyFile]);
    die("Erreur : impossible de trouver la balise <data> dans le contenu déchiffré<br/>");
}

// Supprimer tout après </data>
$endPos = strrpos($plain, '</data>');
if ($endPos === false) {
    cleanupFiles([$encKeyFile, $decKeyFile]);
    die("Erreur : impossible de trouver la balise fermante </data> dans le contenu déchiffré<br/>");
}
$endPos += strlen('</data>');

// Garde uniquement la partie propre
$cleanXml = substr($plain, $startPos, $endPos - $startPos);

// Ajoute l'en-tĂŞte XML
$cleanXml = "<?xml version='1.0' encoding='UTF-8'?>\n" . $cleanXml;



echo "✅ Déchiffrement terminé ! Fichier généré : $cleanXml<br/>";


$xml = simplexml_load_string($cleanXml);

$Bateau = (isset($xml->Bateau) ? $xml->Bateau : '');

Avec cette méthode il y a du bruit au début et à la fin du fichier,
autour de la balise <data></data>,
du coup j'ai enlevé ce bruit dans mes scripts (étape : Nettoyage du flux).

Je partage ici cette petite expérience comme une démarche de compréhension pratique du chiffrement des formulaires.
Le but était de suivre le processus complet, depuis la génération des paires de clés RSA jusqu’au déchiffrement côté serveur des clés AES utilisées pour sécuriser les données du formulaire.

1 Like