Programme pour creer les fichiers ePub chiffrés, pour les tests.

This commit is contained in:
2025-10-14 04:57:34 -07:00
parent 1ce0ddc97c
commit a8c91283dc
3 changed files with 658 additions and 4 deletions

View File

@@ -3,7 +3,7 @@ plugins {
}
group 'epub.jgourour' // Replace with your group ID
version '1.0-SNAPSHOT' // Replace with your version
version '0.1-SNAPSHOT' // Replace with your version
repositories {
mavenCentral()
@@ -14,9 +14,7 @@ dependencies {
implementation 'net.java.dev.jna:jna-platform:5.17.0'
}
test {
useJUnitPlatform()
}
jar.enabled = false
sourceSets {
libraries {
@@ -29,3 +27,26 @@ sourceSets {
}
}
tasks.register('EncryptEPub', Jar) {
archiveFileName = 'EncryptEPub.jar'
manifest {
attributes 'Main-Class': 'EncryptEPub' // Replace with your main class
}
from {sourceSets.main.output.classesDirs}
dependsOn configurations.runtimeClasspath
from {
configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) }
}
duplicatesStrategy = DuplicatesStrategy.INCLUDE // Or INCLUDE, EXCLUDE, WARN, INHERIT
}
jar.enabled = false // instead build serverJar, fullJar, i18neditorJar
artifacts {
archives EncryptEPub
}
test {
useJUnitPlatform()
}

View File

@@ -0,0 +1,532 @@
import java.nio.file.*;
import java.util.Iterator;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.xml.namespace.NamespaceContext;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.xpath.XPath;
import javax.xml.xpath.XPathConstants;
import javax.xml.xpath.XPathExpressionException;
import javax.xml.xpath.XPathFactory;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.cert.CertificateException;
import java.security.cert.CertificateFactory;
import java.security.cert.X509Certificate;
import java.util.Base64;
import java.util.Enumeration;
import java.util.zip.*;
import picocli.CommandLine;
import picocli.CommandLine.Command;
import picocli.CommandLine.Option;
import picocli.CommandLine.Parameters;
import static java.lang.System.arraycopy;
import static java.util.zip.ZipEntry.STORED;
@Command(name = "EncryptEPub", mixinStandardHelpOptions = true,
version = "0.1",
description = "Adds encryption to a TPM-free epub file." )
public class EncryptEPub implements Runnable
{
@Parameters(index = "0",
description = "Name and path of the ePub file to process.")
private String inputFile = null;
@Option(names = {"-a", "--activation-file"},
description = "an xml file containing the user's UUID and public key certificate",
defaultValue = "activation.xml")
private String activationFileName = "activation.xml";
@Option(names = {"-D", "--adept-directory"},
description = "directory that includes the activation file (default: current working directory)",
defaultValue = ".")
private String adeptDir = null;
@Option(names = {"--win"}, description = "Read license certificate from the Windows registry in lieu of the activation file" )
private boolean isWin = false;
private final Logger LOGGER = Logger.getLogger( EncryptEPub.class.getName() );
Path activationPath;
private final String resourceId = "urn:uuid:82919621-81bd-419a-880b-c52eabc22bf1";
private final byte[] sessionKey;
private String uuid;
private String cert;
public static class AdeptNamespaceContext implements NamespaceContext
{
private final Document adeptDoc;
public AdeptNamespaceContext( Document doc )
{
adeptDoc = doc;
}
@Override
public String getNamespaceURI( String s )
{
return "http://ns.adobe.com/adept";
}
@Override
public String getPrefix( String s )
{
return adeptDoc.lookupPrefix( s );
}
@Override
public Iterator<String> getPrefixes( String s )
{
return null;
}
}
/**
* CONSTRUCTOR
*/
EncryptEPub()
{
sessionKey = new byte[16];
new SecureRandom().nextBytes( sessionKey );
}
private String readValue( BufferedReader in )
throws IOException
{
String line;
do
{
line = in.readLine();
}
while (line != null && !line.contains( "value" ));
if (null == line)
{
return null;
}
return line.substring( line.lastIndexOf( "REG_SZ" ) + 7 ).trim();
}
private String getRegistryEntry( String key )
throws IOException
{
// 0001\0002 LicenseCertificate
// 0001\0000 uuid
String command = "reg query HKEY_CURRENT_USER\\Software\\Adobe\\Adept\\Activation\\" + key + " /s /v *";
Process process = Runtime.getRuntime().exec( command );
BufferedReader reader = new BufferedReader( new InputStreamReader( process.getInputStream() ) );
return readValue( reader );
}
@Override
public void run()
{
try
{
// an existing input file is a requirement
Path inputPath = Path.of( inputFile );
if (!Files.exists( inputPath ))
{
String errMsg = "Error: cannot find" + inputPath.toFile().getCanonicalPath();
System.out.println( errMsg );
throw new RuntimeException( errMsg );
}
String osName = System.getProperty( "os.name" );
if (!osName.toLowerCase().contains( "windows" ))
{
isWin = false;
}
if (isWin)
{
try
{
uuid = getRegistryEntry( "0001\\0000" );
uuid = uuid.substring( 9 );
cert = getRegistryEntry( "0001\\0002" );
}
catch (IOException ioe)
{
System.out.println( "Error: Adobe adept registry keys not found" );
throw ioe;
}
}
else
{
if (null == adeptDir)
{
adeptDir = ".";
}
if (null == activationFileName)
activationFileName = "activation.xml";
activationPath = Paths.get( adeptDir, activationFileName );
// An activation file is required! If it doesn't exist we should throw an error here
if (!Files.exists( activationPath ))
{
String errMsg = "Error: cannot find" + activationPath.toFile().getCanonicalPath();
System.out.println( errMsg );
throw new RuntimeException( errMsg );
}
Document activationDoc = parseActivationFile(); // Will throw a handled exception on failure.
try
{
//noinspection unused -- for debugging
String xmlString = docToString( activationDoc, true );
}
catch (TransformerException ignore) {}
XPathFactory xpf = XPathFactory.newInstance();
XPath xpath = xpf.newXPath();
xpath.setNamespaceContext( new AdeptNamespaceContext( activationDoc ) );
Node node = activationDoc.getDocumentElement();
cert = (String) xpath.evaluate("//adept:licenseCertificate", node, XPathConstants.STRING);
uuid = (String) xpath.evaluate( "//adept:user", node, XPathConstants.STRING);
}
encrypt( inputPath );
}
catch (ParserConfigurationException | SAXException | IOException | CertificateException |
XPathExpressionException e)
{
throw new RuntimeException( e );
}
}
/**
* Compresses data using the Deflate algorithm.
*
* @param data The data to compress.
* @return The compressed data.
*/
public static byte[] deflate( byte[] data )
throws IOException
{
Deflater deflater = new Deflater( Deflater.DEFAULT_COMPRESSION, true ); // true for no headers
deflater.setInput( data );
deflater.finish();
ByteArrayOutputStream baos = new ByteArrayOutputStream( data.length );
byte[] buffer = new byte[1024];
while (!deflater.finished())
{
int count = deflater.deflate( buffer );
baos.write( buffer, 0, count );
}
baos.close();
return baos.toByteArray();
}
private Document parseActivationFile()
throws ParserConfigurationException, IOException, SAXException
{
LOGGER.log( Level.FINE, "DEBUG: Parse activation file " + activationPath );
DocumentBuilderFactory docFactory = DocumentBuilderFactory.newInstance();
docFactory.setNamespaceAware( true ); // Important for XPath with namespaces
DocumentBuilder docBuilder;
docBuilder = docFactory.newDocumentBuilder();
Document doc = docBuilder.parse( activationPath.toFile() );
doc.getDocumentElement().normalize();
return doc;
}
private Element createEncryptedDataNode( Document encryption, String URI )
{
Element dataNode = encryption.createElement( "EncryptedData" );
dataNode.setAttribute( "xmlns", "http://www.w3.org/2001/04/xmlenc#" );
Element el = encryption.createElement( "EncryptionMethod" );
el.setAttribute( "Algorithm", "http://www.w3.org/2001/04/xmlenc#aes128-cbc" );
dataNode.appendChild( el );
el = encryption.createElement( "KeyInfo" );
el.setAttribute( "xmlns", "http://www.w3.org/2000/09/xmldsig#" );
dataNode.appendChild( el );
Element child = encryption.createElement( "resource" );
child.setAttribute( "xmlns", "http://ns.adobe.com/adept" );
child.setTextContent( resourceId );
el.appendChild( child );
el = encryption.createElement( "CipherData" );
dataNode.appendChild( el );
child = encryption.createElement( "CipherReference" );
child.setAttribute( "URI", URI );
el.appendChild( child );
return dataNode;
}
private boolean isInSpine( String idref, NodeList spine )
{
if (null == idref || idref.isBlank())
return false;
for (int i = 0; i < spine.getLength(); i++)
{
String attr = ((Element) spine.item( i )).getAttribute( "idref" );
if (attr.equals( idref )) // attr will never be null
return true;
}
return false;
}
private String getManifestedId( String name, NodeList manifest )
{
for (int i = 0; i < manifest.getLength(); i++)
{
Node node = manifest.item( i );
if (node instanceof Element)
{
String attr = ((Element) node).getAttribute( "href" );
if (name.equals( attr ))
{
return ((Element) node).getAttribute( "id" );
}
}
}
return null;
}
private Document createRights()
throws ParserConfigurationException, NoSuchPaddingException, IllegalBlockSizeException,
CertificateException, NoSuchAlgorithmException, BadPaddingException, InvalidKeyException
{
DocumentBuilder documentBuilder = DocumentBuilderFactory.newInstance().newDocumentBuilder();
Document rights = documentBuilder.newDocument();
Element docRoot = rights.createElement( "adept:rights" );
docRoot.setAttribute( "xmlns:adept", "http://ns.adobe.com/adept" );
rights.appendChild( docRoot );
Element token = rights.createElement( "licenseToken" );
token.setAttribute( "xmlns", "http://ns.adobe.com/adept" );
docRoot.appendChild( token );
Element el = rights.createElement( "user" );
el.setTextContent( uuid );
token.appendChild( el );
el = rights.createElement( "resource" );
el.setTextContent( resourceId );
token.appendChild( el );
el = rights.createElement( "encryptedKey" );
token.appendChild( el );
// Encrypt the session key with the user's public key certificate
String encoded = encryptSessionKey( sessionKey, cert );
el.setTextContent( encoded );
el.setAttribute( "keyInfo", "user" );
return rights;
}
private void encrypt( Path inputFile )
throws ParserConfigurationException, SAXException, IOException, CertificateException,
XPathExpressionException
{
String extPattern = "(?<!^)[.].*"; // + (removeAllExtensions ? ".*" : "[^.]*$");
String outFileName = inputFile.toString().replaceAll(extPattern, "");
File outputFile = new File( outFileName + "-enc.epub" );
Files.deleteIfExists( outputFile.toPath() );
System.out.println("Attempting to convert " + inputFile + " to " + outputFile );
try (ZipFile zf = new ZipFile( inputFile.toFile() );
FileOutputStream fos = new FileOutputStream( outputFile );
ZipOutputStream zipOut = new ZipOutputStream( fos ))
{
// get the container and parse it.
ZipEntry entry = zf.getEntry( "META-INF/container.xml" );
DocumentBuilderFactory docFac = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = docFac.newDocumentBuilder();
Document container = documentBuilder.parse( zf.getInputStream( entry ) );
String xmlString; // = XmlUtils.docToString( container, true );
NodeList roots = container.getElementsByTagName( "rootfile" );
String opfFile = ((Element) roots.item( 0 )).getAttribute( "full-path" );
entry = zf.getEntry( opfFile );
container = documentBuilder.parse( zf.getInputStream( entry ) );
Element manifest = (Element) container.getElementsByTagName( "manifest" ).item( 0 );
NodeList spines = container.getElementsByTagName( "spine" ); // there can be only one
NodeList refList = ((Element) spines.item( 0 )).getElementsByTagName( "itemref" );
NodeList itemList = manifest.getElementsByTagName( "item" );
// Read entries from the epub file, encrypting the elements referenced in the spine
CRC32 checksum = new CRC32();
ZipEntry newEntry;
Document encryption = null;
Document rights = null;
int bufSize = 1024;
byte[] buffer = new byte[bufSize];
final Enumeration<? extends ZipEntry> entries = zf.entries();
while (entries.hasMoreElements())
{
entry = entries.nextElement();
// find the file in the manifest, get its id then check that id in the spine
String idref = getManifestedId( entry.getName(), itemList );
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] uncompressed;
try (InputStream is = zf.getInputStream( entry ))
{
int length;
while ((length = is.read( buffer )) >= 0)
{
os.write( buffer, 0, length );
}
uncompressed = os.toByteArray();
}
boolean shouldEncrypt = isInSpine( idref, refList );
if (!shouldEncrypt)
{
// not an encrypted file, just copy it to the output
newEntry = new ZipEntry( entry.getName() );
zipOut.putNextEntry( newEntry );
for (int length = uncompressed.length, offset = 0;
length > offset;
offset += 1024)
{
zipOut.write( uncompressed, offset, Math.min( 1024, length - offset ) );
}
}
else // Encrypt the entry and STORE it in the new zip file
{
// Build the encryption and rights XML documents if they do not yet exist
if (null == encryption)
{
encryption = documentBuilder.newDocument();
Element docRoot = encryption.createElement( "encryption" );
docRoot.setAttribute( "xmlns", "urn:oasis:names:tc:opendocument:xmlns:container" );
encryption.appendChild( docRoot );
// create rights document.
rights = createRights();
}
byte[] compressedData = deflate( uncompressed );
// now encrypt the data with the aes session key
Cipher cipher = Cipher.getInstance( "AES/CBC/PKCS5Padding" );
SecretKeySpec secretKeySpec = new SecretKeySpec( sessionKey, "AES" );
cipher.init( Cipher.ENCRYPT_MODE, secretKeySpec ); // let cipher create the initialization vector for us
byte[] encryptedPayload = cipher.doFinal( compressedData );
byte[] initVector = cipher.getIV();
// Combine IV and the actual encrypted payload
int encryptedLength = encryptedPayload.length;
byte[] result = new byte[16 + encryptedLength];
arraycopy( initVector, 0, result, 0, 16 ); // Copy IV
arraycopy( encryptedPayload, 0, result, 16, encryptedLength ); // Copy encrypted payload
// store the encrypted payload for testing purposes
String parent = outputFile.getCanonicalFile().getParent();
Path file = Paths.get(null == parent ? "" : parent , entry.getName() + ".b64" );
Files.write( file, Base64.getEncoder().encode( result ) );
// Store the result in the zip file
newEntry = new ZipEntry( entry.getName() );
newEntry.setMethod( STORED );
checksum.reset();
checksum.update( result, 0, result.length );
newEntry.setCrc( checksum.getValue() );
newEntry.setSize( result.length );
newEntry.setCompressedSize( result.length );
zipOut.putNextEntry( newEntry );
zipOut.write( result, 0, result.length );
// Add this entry to encryption.xml
Element dataNode = createEncryptedDataNode( encryption, newEntry.getName() );
encryption.getDocumentElement().appendChild( dataNode );
}
} // end while (entries.hasMoreElements())
// if there's an encryption document, add it to the output file
if (null != encryption)
{
xmlString = docToString( encryption, true );
newEntry = new ZipEntry( "META-INF/encryption.xml" );
zipOut.putNextEntry( newEntry );
zipOut.write( xmlString.getBytes( StandardCharsets.UTF_8 ) );
}
// do the same thing with the abbreviated rights.xml.
if (null != rights)
{
xmlString = docToString( rights, false );
newEntry = new ZipEntry( "META-INF/rights.xml" );
zipOut.putNextEntry( newEntry );
zipOut.write( xmlString.getBytes( StandardCharsets.UTF_8 ));
}
} // end try with resources.
catch (IOException | NoSuchAlgorithmException | NoSuchPaddingException | InvalidKeyException |
IllegalBlockSizeException | BadPaddingException | TransformerException e)
{
throw new RuntimeException( e );
}
System.out.println( inputFile + " encrypted.");
}
/**
* Converts an XML Document to a formatted String.
*
* @param doc The XML Document to convert.
* @param omitXmlDeclaration a flag to suppress the xml declaration at the beginning of the document
* @return A string representation of the XML.
*/
private static String docToString( Document doc, boolean omitXmlDeclaration )
throws TransformerException
{
System.setProperty( "line.separator", "\n" );
TransformerFactory transformerFactory = TransformerFactory.newInstance();
Transformer transformer = transformerFactory.newTransformer();
transformer.setOutputProperty( OutputKeys.INDENT, "yes" );
transformer.setOutputProperty( "{http://xml.apache.org/xslt}indent-amount", "2" );
transformer.setOutputProperty( OutputKeys.ENCODING, "UTF-8" ); // Ensure UTF-8 output
if (omitXmlDeclaration)
transformer.setOutputProperty( OutputKeys.OMIT_XML_DECLARATION, "yes" );
StringWriter writer = new StringWriter();
transformer.transform( new DOMSource( doc ), new StreamResult( writer ) );
@SuppressWarnings("UnnecessaryLocalVariable")
String xmlStr = writer.toString();
// Java's XML Transformer has a tendency to create unnecessary blonk lines; use a regex function to remove them.
// return collapseLines( xmlStr );
return xmlStr;
}
String encryptSessionKey( byte[] sessionKey, String cert )
throws NoSuchPaddingException, NoSuchAlgorithmException, CertificateException, InvalidKeyException,
IllegalBlockSizeException, BadPaddingException
{
byte[] certBytes = Base64.getDecoder().decode( cert ); // my public key certificate
CertificateFactory cf = CertificateFactory.getInstance( "X.509" );
X509Certificate x509AuthCertificate = (X509Certificate) cf.generateCertificate(
new ByteArrayInputStream( certBytes ) );
// encrypt with the certificate
Cipher cipher = Cipher.getInstance( "RSA/ECB/PKCS1Padding" );
cipher.init( Cipher.ENCRYPT_MODE, x509AuthCertificate );
byte[] enc = cipher.doFinal( sessionKey ); // The session key encrypted with the public key. Should be 128 bytes
return Base64.getEncoder().encodeToString( enc );
}
public static void main( String[] args )
{
int exitCode = new CommandLine( new EncryptEPub() ).execute( args );
System.exit( exitCode );
}
}

View File

@@ -0,0 +1,101 @@
#### Aperçu
Pendant la traduction de libjourou du C++ vers Java, j'ai réalisé que j'avais
besoin d'un ou de plusieurs fichiers ePub chiffrés pour mes tests. J'ai donc
écrit EncryptEPub pour accomplir cette tâche. EncryptEPub.jar prend un
fichier ePub non chiffré et le transforme en un fichier ePub chiffré.
Il commence par créer une clé symétrique AES de 128 bits (16 octets). Ensuite,
en utilisant soit un fichier "activation.xml" compatible avec libgourou, soit
les données du registre Windows, il chiffre cette clé symétrique avec la clé
publique provenant du certificat de licence X509 de l'utilisateur
("adept:licenseCertificate"). Il encode ensuite cette clé en base64 et, avec
l'UUID de l'utilisateur et une UUID constante, il crée un document XML minimal
"rights".
Il lit ensuite l'entrée '.opf' du fichier ePub original, telle qu'enregistrée
dans l'entrée ZIP "META-INF/container.xml", et analyse son nœud XML &lt;spine>.
Il parcourt ensuite l'intégralité du fichier ePub original, copiant les entrées
du fichier d'origine vers un nouveau fichier d'archive ZIP de sortie. Il
commence par extraire chaque entrée et la décompresse en utilisant la méthode
'Inflate'. Si l'entrée n'est pas référencée dans le nœud <spine> du fichier
'.opf', il l'ajoute à la nouvelle archive en utilisant la méthode 'Deflate'
(compression). Si elle est référencée, il la compresse en utilisant la méthode
'Deflate' sans l'ajouter, chiffre les données compressées avec la clé symétrique
AES, puis ajoute les données chiffrées à la nouvelle archive ePub en utilisant
la méthode 'Store' (sans compression). Pour chaque fichier chiffré, il crée
également un élément <EncryptedData> dans un document XML d'encryption,
référençant le nom de l'entrée et spécifiant l'algorithme de chiffrement
(AES-128-CBC).
#### Par example:
`<EncryptedData xmlns="http://www.w3.org/2001/04/xmlenc#">`
&emsp;`<EncryptionMethod Algorithm="http://www.w3.org/2001/04/xmlenc#aes128-cbc"/>`
&emsp;`<KeyInfo xmlns="http://www.w3.org/2000/09/xmldsig#">`
&emsp;&emsp;`<resource xmlns="http://ns.adobe.com/adept">urn:uuid:82919621-81bd-419a-880b-c52eabc22bf1&lt;/resource>`
&emsp;`</KeyInfo>`
&emsp;`<CipherData>`
&emsp;&emsp;`<CipherReference URI="toc.html"/>`
&emsp;`</CipherData>`
`</EncryptedData>`
Enfin, les documents XML "rights" et "encryption" sont convertis en fichiers
XML et ajoutés à l'ePub de sortie. Cette archive obtenue est le fichier ePub
fortement chiffré, complet avec les métadonnées TPM nécessaire, le rendant
prêt pour les tests.
#### Format d'utilisation:
> java -jar EncryptEPub.jar \[-hV\] \[--win\] \[-a=&lt;nomFichierActivation>\]
> \[-D=&lt;dossierAdept>\] &lt;fichierEPub>
#### Options de Ligne de Commande
> ##### "-h" ou "--help"
>
> Affiche l'aide du programme (instructions d'utilisation) puis se termine.
>
> ##### "-V" ou "--version"
>
> Affiche les informations de version puis se termine.
>
> ##### "&lt;fichierEPub>"
>
> Le nom et le chemin du fichier ePub à traiter. S'il n'est pas spécifié, ou
> s'il n'existe pas, le programme affichera un message d'erreur et se terminera.
>
> ##### "-a" ou "--activation-file" \[&lt;nomFichierActivation>\]
>
> Spécifie le nom du fichier XML contenant l'UUID de l'utilisateur et son
> certificat de clé publique, dans le même format que celui utilisé par le
> fichier activation.xml de libgourou. Seuls les éléments &lt;adept:user> et
> &lt;adept:privateLicenseKey> sont requis. Si ce paramètre n'est pas fourni,
> le nom "activation.xml" sera utilisé par défaut.
>
> ##### "-D" ou "--adept-directory" \[&lt;cheminDossier>\]
>
> Spécifie le répertoire qui contient le fichier d'activation. S'il n'est pas
> spécifié, le répertoire de travail actuel sera utilisé.
>
> ##### "--win"
>
> Lit le certificat de licence et l'UUID de l'utilisateur directement du
> Registre Windows au lieu d'utiliser le fichier d'activation. Si cette option
> est spécifiée _et_ que le programme est exécuté sur Windows, il tentera de
> lire l'UUID de l'utilisateur et le certificat de clé publique X509 à partir
> des clés de Registre :
>
> HKEY_CURRENT_USER\Software\Adobe\Adept\Activation\0001\0000, et
> HKEY_CURRENT_USER\Software\Adobe\Adept\Activation\0001\0002
>
> Ces clés doivent exister si Adobe Digital Editions a été installé et activé
> sur la même machine que celle où EncryptEPub est exécuté.
> ##### Gestion des erreurs
>
> Si l'option "--win" est spécifiée, et que les clés de Registre ne peuvent
> être trouvées, le programme affichera un message d'erreur et se terminera.
>
> Si l'option "--win" n'est pas spécifiée, et que le fichier d'activation ne
> peut être trouvé, le programme affichera un message d'erreur et se
> terminera.