Add --decryption-key option to KDM CLI (#2790).
[dcpomatic.git] / src / lib / kdm_cli.cc
1 /*
2     Copyright (C) 2013-2022 Carl Hetherington <cth@carlh.net>
3
4     This file is part of DCP-o-matic.
5
6     DCP-o-matic is free software; you can redistribute it and/or modify
7     it under the terms of the GNU General Public License as published by
8     the Free Software Foundation; either version 2 of the License, or
9     (at your option) any later version.
10
11     DCP-o-matic is distributed in the hope that it will be useful,
12     but WITHOUT ANY WARRANTY; without even the implied warranty of
13     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14     GNU General Public License for more details.
15
16     You should have received a copy of the GNU General Public License
17     along with DCP-o-matic.  If not, see <http://www.gnu.org/licenses/>.
18
19 */
20
21
22 /** @file  src/tools/dcpomatic_kdm_cli.cc
23  *  @brief Command-line program to generate KDMs.
24  */
25
26
27 #include "cinema.h"
28 #include "config.h"
29 #include "dkdm_wrapper.h"
30 #include "email.h"
31 #include "exceptions.h"
32 #include "film.h"
33 #include "kdm_with_metadata.h"
34 #include "screen.h"
35 #include <dcp/certificate.h>
36 #include <dcp/decrypted_kdm.h>
37 #include <dcp/encrypted_kdm.h>
38 #include <dcp/filesystem.h>
39 #include <getopt.h>
40
41
42 using std::dynamic_pointer_cast;
43 using std::list;
44 using std::make_shared;
45 using std::runtime_error;
46 using std::shared_ptr;
47 using std::string;
48 using std::vector;
49 using boost::optional;
50 using boost::bind;
51 #if BOOST_VERSION >= 106100
52 using namespace boost::placeholders;
53 #endif
54 using namespace dcpomatic;
55
56
57 static void
58 help (std::function<void (string)> out)
59 {
60         out (String::compose("Syntax: %1 [OPTION] <FILM|CPL-ID|DKDM>", program_name));
61         out ("  -h, --help                               show this help");
62         out ("  -o, --output <path>                      output file or directory");
63         out ("  -K, --filename-format <format>           filename format for KDMs");
64         out ("  -Z, --container-name-format <format>     filename format for ZIP containers");
65         out ("  -f, --valid-from <time>                  valid from time (e.g. \"2013-09-28T01:41:51+04:00\", \"2018-01-01T12:00:30\") or \"now\"");
66         out ("  -t, --valid-to <time>                    valid to time (e.g. \"2014-09-28T01:41:51\")");
67         out ("  -d, --valid-duration <duration>          valid duration (e.g. \"1 day\", \"4 hours\", \"2 weeks\")");
68         out ("  -F, --formulation <formulation>          modified-transitional-1, multiple-modified-transitional-1, dci-any or dci-specific [default modified-transitional-1]");
69         out ("  -p, --disable-forensic-marking-picture   disable forensic marking of pictures essences");
70         out ("  -a, --disable-forensic-marking-audio     disable forensic marking of audio essences (optionally above a given channel, e.g 12)");
71         out ("  -e, --email                              email KDMs to cinemas");
72         out ("  -z, --zip                                ZIP each cinema's KDMs into its own file");
73         out ("  -v, --verbose                            be verbose");
74         out ("  -c, --cinema <name|email>                cinema name (when using -C) or name/email (to filter cinemas)");
75         out ("  -S, --screen <name>                      screen name (when using -C) or screen name (to filter screens when using -c)");
76         out ("  -C, --projector-certificate <file>       file containing projector certificate");
77         out ("  -T, --trusted-device-certificate <file>  file containing a trusted device's certificate");
78         out ("      --decryption-key <file>              file containing the private key which can decrypt the given DKDM");
79         out ("                                           (DCP-o-matic's configured private key will be used otherwise)");
80         out ("      --cinemas-file <file>                use the given file as a list of cinemas instead of the current configuration");
81         out ("      --list-cinemas                       list known cinemas from the DCP-o-matic settings");
82         out ("      --list-dkdm-cpls                     list CPLs for which DCP-o-matic has DKDMs");
83         out ("");
84         out ("CPL-ID must be the ID of a CPL that is mentioned in DCP-o-matic's DKDM list.");
85         out ("");
86         out ("For example:");
87         out ("");
88         out ("Create KDMs for my_great_movie to play in all of Fred's Cinema's screens for the next two weeks and zip them up.");
89         out ("(Fred's Cinema must have been set up in DCP-o-matic's KDM window)");
90         out ("");
91         out (String::compose("\t%1 -c \"Fred's Cinema\" -f now -d \"2 weeks\" -z my_great_movie", program_name));
92 }
93
94
95 class KDMCLIError : public std::runtime_error
96 {
97 public:
98         KDMCLIError (std::string message)
99                 : std::runtime_error (String::compose("%1: %2", program_name, message).c_str())
100         {}
101 };
102
103
104 static boost::posix_time::time_duration
105 duration_from_string (string d)
106 {
107         int N;
108         char unit_buf[64] = "\0";
109         sscanf (d.c_str(), "%d %63s", &N, unit_buf);
110         string const unit (unit_buf);
111
112         if (N == 0) {
113                 throw KDMCLIError (String::compose("could not understand duration \"%1\"", d));
114         }
115
116         if (unit == "year" || unit == "years") {
117                 return boost::posix_time::time_duration (N * 24 * 365, 0, 0, 0);
118         } else if (unit == "week" || unit == "weeks") {
119                 return boost::posix_time::time_duration (N * 24 * 7, 0, 0, 0);
120         } else if (unit == "day" || unit == "days") {
121                 return boost::posix_time::time_duration (N * 24, 0, 0, 0);
122         } else if (unit == "hour" || unit == "hours") {
123                 return boost::posix_time::time_duration (N, 0, 0, 0);
124         }
125
126         throw KDMCLIError (String::compose("could not understand duration \"%1\"", d));
127 }
128
129
130 static bool
131 always_overwrite ()
132 {
133         return true;
134 }
135
136
137 static
138 void
139 write_files (
140         list<KDMWithMetadataPtr> kdms,
141         bool zip,
142         boost::filesystem::path output,
143         dcp::NameFormat container_name_format,
144         dcp::NameFormat filename_format,
145         bool verbose,
146         std::function<void (string)> out
147         )
148 {
149         if (zip) {
150                 int const N = write_zip_files (
151                         collect (kdms),
152                         output,
153                         container_name_format,
154                         filename_format,
155                         bind (&always_overwrite)
156                         );
157
158                 if (verbose) {
159                         out (String::compose("Wrote %1 ZIP files to %2", N, output));
160                 }
161         } else {
162                 int const N = write_files (
163                         kdms, output, filename_format,
164                         bind (&always_overwrite)
165                         );
166
167                 if (verbose) {
168                         out (String::compose("Wrote %1 KDM files to %2", N, output));
169                 }
170         }
171 }
172
173
174 static
175 shared_ptr<Cinema>
176 find_cinema (string cinema_name)
177 {
178         auto cinemas = Config::instance()->cinemas ();
179         auto i = cinemas.begin();
180         while (
181                 i != cinemas.end() &&
182                 (*i)->name != cinema_name &&
183                 find ((*i)->emails.begin(), (*i)->emails.end(), cinema_name) == (*i)->emails.end()) {
184
185                 ++i;
186         }
187
188         if (i == cinemas.end ()) {
189                 throw KDMCLIError (String::compose("could not find cinema \"%1\"", cinema_name));
190         }
191
192         return *i;
193 }
194
195
196 static
197 void
198 from_film (
199         vector<shared_ptr<Screen>> screens,
200         boost::filesystem::path film_dir,
201         bool verbose,
202         boost::filesystem::path output,
203         dcp::NameFormat container_name_format,
204         dcp::NameFormat filename_format,
205         dcp::LocalTime valid_from,
206         dcp::LocalTime valid_to,
207         dcp::Formulation formulation,
208         bool disable_forensic_marking_picture,
209         optional<int> disable_forensic_marking_audio,
210         bool email,
211         bool zip,
212         std::function<void (string)> out
213         )
214 {
215         shared_ptr<Film> film;
216         try {
217                 film = make_shared<Film>(film_dir);
218                 film->read_metadata ();
219                 if (verbose) {
220                         out (String::compose("Read film %1", film->name()));
221                 }
222         } catch (std::exception& e) {
223                 throw KDMCLIError (String::compose("error reading film \"%1\" (%2)", film_dir.string(), e.what()));
224         }
225
226         /* XXX: allow specification of this */
227         vector<CPLSummary> cpls = film->cpls ();
228         if (cpls.empty ()) {
229                 throw KDMCLIError ("no CPLs found in film");
230         } else if (cpls.size() > 1) {
231                 throw KDMCLIError ("more than one CPL found in film");
232         }
233
234         auto cpl = cpls.front().cpl_file;
235
236         std::vector<KDMCertificatePeriod> period_checks;
237
238         try {
239                 list<KDMWithMetadataPtr> kdms;
240                 for (auto i: screens) {
241                         std::function<dcp::DecryptedKDM (dcp::LocalTime, dcp::LocalTime)> make_kdm = [film, cpl](dcp::LocalTime begin, dcp::LocalTime end) {
242                                 return film->make_kdm(cpl, begin, end);
243                         };
244                         auto p = kdm_for_screen(make_kdm, i, valid_from, valid_to, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio, period_checks);
245                         if (p) {
246                                 kdms.push_back (p);
247                         }
248                 }
249
250                 if (find_if(
251                         period_checks.begin(),
252                         period_checks.end(),
253                         [](KDMCertificatePeriod const& p) { return p.overlap == KDMCertificateOverlap::KDM_OUTSIDE_CERTIFICATE; }
254                    ) != period_checks.end()) {
255                         throw KDMCLIError(
256                                 "Some KDMs would have validity periods which are completely outside the recipient certificate periods.  Such KDMs are very unlikely to work, so will not be created."
257                                 );
258                 }
259
260                 if (find_if(
261                         period_checks.begin(),
262                         period_checks.end(),
263                         [](KDMCertificatePeriod const& p) { return p.overlap == KDMCertificateOverlap::KDM_OVERLAPS_CERTIFICATE; }
264                    ) != period_checks.end()) {
265                         out("For some of these KDMs the recipient certificate's validity period will not cover the whole of the KDM validity period.  This might cause problems with the KDMs.");
266                 }
267
268                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
269                 if (email) {
270                         send_emails ({kdms}, container_name_format, filename_format, film->dcp_name(), {});
271                 }
272         } catch (FileError& e) {
273                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
274         }
275 }
276
277
278 static
279 optional<dcp::EncryptedKDM>
280 sub_find_dkdm (shared_ptr<DKDMGroup> group, string cpl_id)
281 {
282         for (auto i: group->children()) {
283                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
284                 if (g) {
285                         auto dkdm = sub_find_dkdm (g, cpl_id);
286                         if (dkdm) {
287                                 return dkdm;
288                         }
289                 } else {
290                         auto d = dynamic_pointer_cast<DKDM>(i);
291                         assert (d);
292                         if (d->dkdm().cpl_id() == cpl_id) {
293                                 return d->dkdm();
294                         }
295                 }
296         }
297
298         return {};
299 }
300
301
302 static
303 optional<dcp::EncryptedKDM>
304 find_dkdm (string cpl_id)
305 {
306         return sub_find_dkdm (Config::instance()->dkdms(), cpl_id);
307 }
308
309
310 static
311 dcp::EncryptedKDM
312 kdm_from_dkdm (
313         dcp::DecryptedKDM dkdm,
314         dcp::Certificate target,
315         vector<string> trusted_devices,
316         dcp::LocalTime valid_from,
317         dcp::LocalTime valid_to,
318         dcp::Formulation formulation,
319         bool disable_forensic_marking_picture,
320         optional<int> disable_forensic_marking_audio
321         )
322 {
323         /* Signer for new KDM */
324         auto signer = Config::instance()->signer_chain ();
325         if (!signer->valid ()) {
326                 throw KDMCLIError ("signing certificate chain is invalid.");
327         }
328
329         /* Make a new empty KDM and add the keys from the DKDM to it */
330         dcp::DecryptedKDM kdm (
331                 valid_from,
332                 valid_to,
333                 dkdm.annotation_text().get_value_or(""),
334                 dkdm.content_title_text(),
335                 dcp::LocalTime().as_string()
336                 );
337
338         for (auto const& j: dkdm.keys()) {
339                 kdm.add_key(j);
340         }
341
342         return kdm.encrypt (signer, target, trusted_devices, formulation, disable_forensic_marking_picture, disable_forensic_marking_audio);
343 }
344
345
346 static
347 void
348 from_dkdm (
349         vector<shared_ptr<Screen>> screens,
350         dcp::DecryptedKDM dkdm,
351         bool verbose,
352         boost::filesystem::path output,
353         dcp::NameFormat container_name_format,
354         dcp::NameFormat filename_format,
355         dcp::LocalTime valid_from,
356         dcp::LocalTime valid_to,
357         dcp::Formulation formulation,
358         bool disable_forensic_marking_picture,
359         optional<int> disable_forensic_marking_audio,
360         bool email,
361         bool zip,
362         std::function<void (string)> out
363         )
364 {
365         dcp::NameFormat::Map values;
366
367         try {
368                 list<KDMWithMetadataPtr> kdms;
369                 for (auto i: screens) {
370                         if (!i->recipient) {
371                                 continue;
372                         }
373
374                         auto const kdm = kdm_from_dkdm(
375                                                         dkdm,
376                                                         i->recipient.get(),
377                                                         i->trusted_device_thumbprints(),
378                                                         valid_from,
379                                                         valid_to,
380                                                         formulation,
381                                                         disable_forensic_marking_picture,
382                                                         disable_forensic_marking_audio
383                                                         );
384
385                         dcp::NameFormat::Map name_values;
386                         name_values['c'] = i->cinema ? i->cinema->name : "";
387                         name_values['s'] = i->name;
388                         name_values['f'] = kdm.content_title_text();
389                         name_values['b'] = valid_from.date() + " " + valid_from.time_of_day(true, false);
390                         name_values['e'] = valid_to.date() + " " + valid_to.time_of_day(true, false);
391                         name_values['i'] = kdm.cpl_id();
392
393                         kdms.push_back(make_shared<KDMWithMetadata>(name_values, i->cinema.get(), i->cinema ? i->cinema->emails : vector<string>(), kdm));
394                 }
395                 write_files (kdms, zip, output, container_name_format, filename_format, verbose, out);
396                 if (email) {
397                         send_emails ({kdms}, container_name_format, filename_format, dkdm.annotation_text().get_value_or(""), {});
398                 }
399         } catch (FileError& e) {
400                 throw KDMCLIError (String::compose("%1 (%2)", e.what(), e.file().string()));
401         }
402 }
403
404
405 static
406 void
407 dump_dkdm_group (shared_ptr<DKDMGroup> group, int indent, std::function<void (string)> out)
408 {
409         auto const indent_string = string(indent, ' ');
410
411         if (indent > 0) {
412                 out (indent_string + group->name());
413         }
414         for (auto i: group->children()) {
415                 auto g = dynamic_pointer_cast<DKDMGroup>(i);
416                 if (g) {
417                         dump_dkdm_group (g, indent + 2, out);
418                 } else {
419                         auto d = dynamic_pointer_cast<DKDM>(i);
420                         assert(d);
421                         out (indent_string + d->dkdm().cpl_id());
422                 }
423         }
424 }
425
426
427 static
428 dcp::LocalTime
429 time_from_string(string time)
430 {
431         if (time == "now") {
432                 return {};
433         }
434
435         if (time.length() > 10 && time[10] == ' ') {
436                 time[10] = 'T';
437         }
438
439         return dcp::LocalTime(time);
440 }
441
442
443 optional<string>
444 kdm_cli (int argc, char* argv[], std::function<void (string)> out)
445 try
446 {
447         boost::filesystem::path output = dcp::filesystem::current_path();
448         auto container_name_format = Config::instance()->kdm_container_name_format();
449         auto filename_format = Config::instance()->kdm_filename_format();
450         optional<string> cinema_name;
451         shared_ptr<Cinema> cinema;
452         optional<boost::filesystem::path> projector_certificate;
453         optional<boost::filesystem::path> decryption_key;
454         optional<string> screen;
455         vector<shared_ptr<Screen>> screens;
456         optional<dcp::EncryptedKDM> dkdm;
457         optional<dcp::LocalTime> valid_from;
458         optional<dcp::LocalTime> valid_to;
459         bool zip = false;
460         bool list_cinemas = false;
461         bool list_dkdm_cpls = false;
462         optional<string> duration_string;
463         bool verbose = false;
464         dcp::Formulation formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
465         bool disable_forensic_marking_picture = false;
466         optional<int> disable_forensic_marking_audio;
467         bool email = false;
468         optional<boost::filesystem::path> cinemas_file;
469
470         program_name = argv[0];
471
472         /* Reset getopt() so we can call this method several times in one test process */
473         optind = 1;
474
475         int option_index = 0;
476         while (true) {
477                 static struct option long_options[] = {
478                         { "help", no_argument, 0, 'h'},
479                         { "output", required_argument, 0, 'o'},
480                         { "filename-format", required_argument, 0, 'K'},
481                         { "container-name-format", required_argument, 0, 'Z'},
482                         { "valid-from", required_argument, 0, 'f'},
483                         { "valid-to", required_argument, 0, 't'},
484                         { "valid-duration", required_argument, 0, 'd'},
485                         { "formulation", required_argument, 0, 'F' },
486                         { "disable-forensic-marking-picture", no_argument, 0, 'p' },
487                         { "disable-forensic-marking-audio", optional_argument, 0, 'a' },
488                         { "email", no_argument, 0, 'e' },
489                         { "zip", no_argument, 0, 'z' },
490                         { "verbose", no_argument, 0, 'v' },
491                         { "cinema", required_argument, 0, 'c' },
492                         { "screen", required_argument, 0, 'S' },
493                         { "projector-certificate", required_argument, 0, 'C' },
494                         { "trusted-device-certificate", required_argument, 0, 'T' },
495                         { "decryption-key", required_argument, 0, 'G' },
496                         { "list-cinemas", no_argument, 0, 'B' },
497                         { "list-dkdm-cpls", no_argument, 0, 'D' },
498                         { "cinemas-file", required_argument, 0, 'E' },
499                         { 0, 0, 0, 0 }
500                 };
501
502                 int c = getopt_long (argc, argv, "ho:K:Z:f:t:d:F:pae::zvc:S:C:T:BDE:G:", long_options, &option_index);
503
504                 if (c == -1) {
505                         break;
506                 }
507
508                 switch (c) {
509                 case 'h':
510                         help (out);
511                         return {};
512                 case 'o':
513                         output = optarg;
514                         break;
515                 case 'K':
516                         filename_format = dcp::NameFormat (optarg);
517                         break;
518                 case 'Z':
519                         container_name_format = dcp::NameFormat (optarg);
520                         break;
521                 case 'f':
522                         valid_from = time_from_string(optarg);
523                         break;
524                 case 't':
525                         valid_to = dcp::LocalTime(optarg);
526                         break;
527                 case 'd':
528                         duration_string = optarg;
529                         break;
530                 case 'F':
531                         if (string(optarg) == "modified-transitional-1") {
532                                 formulation = dcp::Formulation::MODIFIED_TRANSITIONAL_1;
533                         } else if (string(optarg) == "multiple-modified-transitional-1") {
534                                 formulation = dcp::Formulation::MULTIPLE_MODIFIED_TRANSITIONAL_1;
535                         } else if (string(optarg) == "dci-any") {
536                                 formulation = dcp::Formulation::DCI_ANY;
537                         } else if (string(optarg) == "dci-specific") {
538                                 formulation = dcp::Formulation::DCI_SPECIFIC;
539                         } else {
540                                 throw KDMCLIError ("unrecognised KDM formulation " + string (optarg));
541                         }
542                         break;
543                 case 'p':
544                         disable_forensic_marking_picture = true;
545                         break;
546                 case 'a':
547                         disable_forensic_marking_audio = 0;
548                         if (optarg == 0 && argv[optind] != 0 && argv[optind][0] != '-') {
549                                 disable_forensic_marking_audio = atoi (argv[optind++]);
550                         } else if (optarg) {
551                                 disable_forensic_marking_audio = atoi (optarg);
552                         }
553                         break;
554                 case 'e':
555                         email = true;
556                         break;
557                 case 'z':
558                         zip = true;
559                         break;
560                 case 'v':
561                         verbose = true;
562                         break;
563                 case 'c':
564                         /* This could be a cinema to search for in the configured list or the name of a cinema being
565                            built up on-the-fly in the option.  Cater for both possilibities here by storing the name
566                            (for lookup) and by creating a Cinema which the next Screen will be added to.
567                         */
568                         cinema_name = optarg;
569                         cinema = make_shared<Cinema>(optarg, vector<string>(), "");
570                         break;
571                 case 'S':
572                         /* Similarly, this could be the name of a new (temporary) screen or the name of a screen
573                          * to search for.
574                          */
575                         screen = optarg;
576                         break;
577                 case 'C':
578                         projector_certificate = optarg;
579                         break;
580                 case 'T':
581                         /* A trusted device ends up in the last screen we made */
582                         if (!screens.empty ()) {
583                                 screens.back()->trusted_devices.push_back(TrustedDevice(dcp::Certificate(dcp::file_to_string(optarg))));
584                         }
585                         break;
586                 case 'G':
587                         decryption_key = optarg;
588                         break;
589                 case 'B':
590                         list_cinemas = true;
591                         break;
592                 case 'D':
593                         list_dkdm_cpls = true;
594                         break;
595                 case 'E':
596                         cinemas_file = optarg;
597                         break;
598                 }
599         }
600
601         if (cinemas_file) {
602                 Config::instance()->set_cinemas_file(*cinemas_file);
603         }
604
605         if (projector_certificate) {
606                 /* Make a new screen and add it to the current cinema */
607                 dcp::CertificateChain chain(dcp::file_to_string(*projector_certificate));
608                 auto screen_to_add = std::make_shared<Screen>(screen.get_value_or(""), "", chain.leaf(), boost::none, vector<TrustedDevice>());
609                 if (cinema) {
610                         cinema->add_screen(screen_to_add);
611                 }
612                 screens.push_back(screen_to_add);
613         }
614
615         if (list_cinemas) {
616                 auto cinemas = Config::instance()->cinemas ();
617                 for (auto i: cinemas) {
618                         out (String::compose("%1 (%2)", i->name, Email::address_list(i->emails)));
619                 }
620                 return {};
621         }
622
623         if (list_dkdm_cpls) {
624                 dump_dkdm_group (Config::instance()->dkdms(), 0, out);
625                 return {};
626         }
627
628         if (!duration_string && !valid_to) {
629                 throw KDMCLIError ("you must specify a --valid-duration or --valid-to");
630         }
631
632         if (!valid_from) {
633                 throw KDMCLIError ("you must specify --valid-from");
634         }
635
636         if (optind >= argc) {
637                 throw KDMCLIError ("no film, CPL ID or DKDM specified");
638         }
639
640         if (screens.empty()) {
641                 if (!cinema_name) {
642                         throw KDMCLIError ("you must specify either a cinema or one or more screens using certificate files");
643                 }
644
645                 screens = find_cinema (*cinema_name)->screens ();
646                 if (screen) {
647                         screens.erase(std::remove_if(screens.begin(), screens.end(), [&screen](shared_ptr<Screen> s) { return s->name != *screen; }), screens.end());
648                 }
649         }
650
651         if (duration_string) {
652                 valid_to = valid_from.get();
653                 valid_to->add(duration_from_string(*duration_string));
654         }
655
656         if (verbose) {
657                 out(String::compose("Making KDMs valid from %1 to %2", valid_from->as_string(), valid_to->as_string()));
658         }
659
660         string const thing = argv[optind];
661         if (dcp::filesystem::is_directory(thing) && dcp::filesystem::is_regular_file(boost::filesystem::path(thing) / "metadata.xml")) {
662                 from_film (
663                         screens,
664                         thing,
665                         verbose,
666                         output,
667                         container_name_format,
668                         filename_format,
669                         *valid_from,
670                         *valid_to,
671                         formulation,
672                         disable_forensic_marking_picture,
673                         disable_forensic_marking_audio,
674                         email,
675                         zip,
676                         out
677                         );
678         } else {
679                 if (dcp::filesystem::is_regular_file(thing)) {
680                         dkdm = dcp::EncryptedKDM (dcp::file_to_string (thing));
681                 } else {
682                         dkdm = find_dkdm (thing);
683                 }
684
685                 if (!dkdm) {
686                         throw KDMCLIError ("could not find film or CPL ID corresponding to " + thing);
687                 }
688
689                 string const key = decryption_key ? dcp::file_to_string(*decryption_key) : Config::instance()->decryption_chain()->key().get();
690
691                 from_dkdm (
692                         screens,
693                         dcp::DecryptedKDM(*dkdm, key),
694                         verbose,
695                         output,
696                         container_name_format,
697                         filename_format,
698                         *valid_from,
699                         *valid_to,
700                         formulation,
701                         disable_forensic_marking_picture,
702                         disable_forensic_marking_audio,
703                         email,
704                         zip,
705                         out
706                         );
707         }
708
709         return {};
710 } catch (std::exception& e) {
711         return string(e.what());
712 }
713