3c950bdbf5e2b8086d5433d250f79d5b3630b237
[epubpypt.git] / src / Command / Decrypt.php
1 <?php
2 namespace Epubpypt\Command;
3
4 use Epubpypt\Crypt;
5 use Epubpypt\ZipWrapper;
6
7 /**
8  * Decrypt files inside the epub file
9  */
10 class Decrypt
11 {
12     /**
13      * Path of the .epub file
14      */
15     protected string $epubFile;
16
17     /**
18      * Paths of files to decrypt
19      */
20     protected array $paths;
21
22     /**
23      * Command line options
24      */
25     protected array $options = [];
26
27     /**
28      * Binary encryption key
29      */
30     protected ?string $encryptionKey = null;
31
32     public function __construct(string $epubFile, array $paths, array $options)
33     {
34         $this->epubFile = $epubFile;
35         $this->paths    = $paths;
36         $this->options  = $options;
37     }
38
39     public function run(): void
40     {
41         $this->validateOptions();
42         $this->loadEncryptionKey();
43
44         $zw = new ZipWrapper($this->epubFile, false);
45
46         $encryptionXml = $zw->getFromName('META-INF/encryption.xml');
47         if ($encryptionXml === false) {
48             $this->logger->info('No encryption information file found');
49             return;
50         }
51         $sxEnc = simplexml_load_string($encryptionXml);
52
53         $decryptedFiles = [];
54         $encInfoMap = $this->loadEncryptionInfo($zw, $sxEnc);
55         foreach ($encInfoMap as $filePath => $algoUrl) {
56             $content = $zw->getFromName($filePath);
57             if ($algoUrl == 'http://www.w3.org/2001/04/xmlenc#aes256-cbc') {
58                 $this->logger->info('Decrypting ' . $filePath);
59                 $decryptedContent = Crypt::decryptAES256CBC(
60                     $this->encryptionKey,
61                     $content
62                 );
63
64             } else {
65                 $this->logger->error('Unsupported encryption algorithm: ' . $algoUrl);
66                 continue;
67             }
68
69             if ($decryptedContent === null) {
70                 $this->logger->error('Decryption failed for ' . $filePath);
71                 continue;
72             }
73
74             $decryptedFiles[] = $filePath;
75             $zw->addFromString($filePath, $decryptedContent);
76         }
77
78         $this->removeDecryptedFromEncryptionXml($sxEnc, $decryptedFiles);
79         $zw->addFromString('META-INF/encryption.xml', $sxEnc->asXML());
80
81         if (!$zw->close()) {
82             $this->logger->error('Error while writing zip file contents');
83         }
84     }
85
86     protected function loadEncryptionKey(): void
87     {
88         if ($this->options['key'] !== null) {
89             $this->encryptionKey = @hex2bin($this->options['key']);
90             if ($this->encryptionKey == '') {
91                 throw new \InvalidArgumentException('Invalid encryption key');
92             }
93
94         } else {
95             if (!is_readable($this->options['keyfile'])) {
96                 throw new \InvalidArgumentException('Cannot open key file');
97             }
98             $this->encryptionKey = file_get_contents($this->options['keyfile']);
99         }
100     }
101
102     /**
103      * @return array Key is the file path, value the encryption algorithm URL.
104      */
105     protected function loadEncryptionInfo(ZipWrapper $zw, \SimpleXMLElement $sxEnc): array
106     {
107         $encInfoMap = [];
108         $invPaths = array_flip($this->paths);
109         foreach ($sxEnc as $sxEncData) {
110             $filePath = (string) $sxEncData->CipherData->CipherReference['URI'];
111             $algoUrl  = (string) $sxEncData->EncryptionMethod['Algorithm'];
112             if ($this->options['all'] || isset($invPaths[$filePath])) {
113                 $encInfoMap[$filePath] = $algoUrl;
114                 unset($invPaths[$filePath]);
115             }
116         }
117
118         if (!$this->options['all'] && count($invPaths)) {
119             foreach ($invPaths as $filePath => $dummy) {
120                 $this->logger->notice('File is not encrypted: ' . $filePath);
121             }
122         }
123
124         return $encInfoMap;
125     }
126
127     protected function removeDecryptedFromEncryptionXml(
128         \SimpleXMLElement $sxEnc, $decryptedFiles
129     ): void {
130         $decryptedFiles = array_flip($decryptedFiles);
131         $total = count($sxEnc->EncryptedData);
132         for ($n = $total - 1; $n >= 0; $n--) {
133             $filePath = (string) $sxEnc->EncryptedData[$n]->CipherData->CipherReference['URI'];
134             if (isset($decryptedFiles[$filePath])) {
135                 unset($sxEnc->EncryptedData[$n]);
136             }
137         }
138     }
139
140     protected function validateOptions(): void
141     {
142         if ($this->options['all'] && count($this->paths)) {
143             throw new \DomainException(
144                 'Do not specify file paths with using option "--all".'
145             );
146         }
147
148         if ($this->options['key'] !== null
149             && $this->options['keyfile'] !== null
150         ) {
151             throw new \DomainException(
152                 'Specify either --key or --keyfile option, but not both.'
153             );
154         }
155
156         if ($this->options['key'] === null
157             && $this->options['keyfile'] === null
158         ) {
159             throw new \DomainException(
160                 'Specify one of --key or --keyfile options.'
161             );
162         }
163     }
164 }