From: Carl Hetherington Date: Sun, 20 Jul 2014 20:22:52 +0000 (+0100) Subject: Basic support for decryption of imported DCPs. X-Git-Tag: v2.0.48~702 X-Git-Url: https://git.carlh.net/gitweb/?p=dcpomatic.git;a=commitdiff_plain;h=d8d7ddd4c39e3ea347afd1fccc037d8b0a31bc87 Basic support for decryption of imported DCPs. --- diff --git a/src/lib/dcp_content.cc b/src/lib/dcp_content.cc index 0eef075d7..9d4ee6388 100644 --- a/src/lib/dcp_content.cc +++ b/src/lib/dcp_content.cc @@ -18,16 +18,22 @@ */ #include +#include #include "dcp_content.h" #include "dcp_examiner.h" #include "job.h" #include "film.h" +#include "config.h" #include "compose.hpp" #include "i18n.h" using std::string; +using std::cout; using boost::shared_ptr; +using boost::optional; + +int const DCPContentProperty::CAN_BE_PLAYED = 600; DCPContent::DCPContent (shared_ptr f, boost::filesystem::path p) : Content (f) @@ -35,7 +41,9 @@ DCPContent::DCPContent (shared_ptr f, boost::filesystem::path p) , SingleStreamAudioContent (f) , SubtitleContent (f) , _has_subtitles (false) + , _encrypted (false) , _directory (p) + , _kdm_valid (false) { read_directory (p); } @@ -49,6 +57,8 @@ DCPContent::DCPContent (shared_ptr f, cxml::ConstNodePtr node, int v _name = node->string_child ("Name"); _has_subtitles = node->bool_child ("HasSubtitles"); _directory = node->string_child ("Directory"); + _encrypted = node->bool_child ("Encrypted"); + _kdm_valid = node->bool_child ("KDMValid"); } void @@ -66,6 +76,8 @@ DCPContent::read_directory (boost::filesystem::path p) void DCPContent::examine (shared_ptr job) { + bool const could_be_played = can_be_played (); + job->set_progress_unknown (); Content::examine (job); @@ -76,6 +88,12 @@ DCPContent::examine (shared_ptr job) boost::mutex::scoped_lock lm (_mutex); _name = examiner->name (); _has_subtitles = examiner->has_subtitles (); + _encrypted = examiner->encrypted (); + _kdm_valid = examiner->kdm_valid (); + + if (could_be_played != can_be_played ()) { + signal_changed (DCPContentProperty::CAN_BE_PLAYED); + } } string @@ -106,7 +124,10 @@ DCPContent::as_xml (xmlpp::Node* node) const boost::mutex::scoped_lock lm (_mutex); node->add_child("Name")->add_child_text (_name); node->add_child("HasSubtitles")->add_child_text (_has_subtitles ? "1" : "0"); + node->add_child("Encrypted")->add_child_text (_encrypted ? "1" : "0"); node->add_child("Directory")->add_child_text (_directory.string ()); + /* XXX: KDM */ + node->add_child("KDMValid")->add_child_text (_kdm_valid ? "1" : "0"); } DCPTime @@ -123,9 +144,14 @@ DCPContent::identifier () const return SubtitleContent::identifier (); } +void +DCPContent::add_kdm (dcp::EncryptedKDM k) +{ + _kdm = k; +} + bool -DCPContent::has_subtitles () const +DCPContent::can_be_played () const { - boost::mutex::scoped_lock lm (_mutex); - return _has_subtitles; + return !_encrypted || _kdm_valid; } diff --git a/src/lib/dcp_content.h b/src/lib/dcp_content.h index 60b7142de..da78e6d72 100644 --- a/src/lib/dcp_content.h +++ b/src/lib/dcp_content.h @@ -17,15 +17,26 @@ */ +#ifndef DCPOMATIC_DCP_CONTENT_H +#define DCPOMATIC_DCP_CONTENT_H + /** @file src/lib/dcp_content.h * @brief DCPContent class. */ #include +#include +#include #include "video_content.h" #include "single_stream_audio_content.h" #include "subtitle_content.h" +class DCPContentProperty +{ +public: + static int const CAN_BE_PLAYED; +}; + /** @class DCPContent * @brief An existing DCP used as input. */ @@ -48,17 +59,40 @@ public: std::string identifier () const; /* SubtitleContent */ - bool has_subtitles () const; + bool has_subtitles () const { + boost::mutex::scoped_lock lm (_mutex); + return _has_subtitles; + } boost::filesystem::path directory () const { boost::mutex::scoped_lock lm (_mutex); return _directory; } + bool encrypted () const { + boost::mutex::scoped_lock lm (_mutex); + return _encrypted; + } + + void add_kdm (dcp::EncryptedKDM); + + boost::optional kdm () const { + return _kdm; + } + + bool can_be_played () const; + private: void read_directory (boost::filesystem::path); std::string _name; bool _has_subtitles; + /** true if our DCP is encrypted */ + bool _encrypted; boost::filesystem::path _directory; + boost::optional _kdm; + /** true if _kdm successfully decrypts the first frame of our DCP */ + bool _kdm_valid; }; + +#endif diff --git a/src/lib/dcp_decoder.cc b/src/lib/dcp_decoder.cc index d0642d8b6..bf016ef87 100644 --- a/src/lib/dcp_decoder.cc +++ b/src/lib/dcp_decoder.cc @@ -31,6 +31,7 @@ #include "dcp_content.h" #include "j2k_image_proxy.h" #include "image.h" +#include "config.h" using std::list; using std::cout; @@ -46,6 +47,9 @@ DCPDecoder::DCPDecoder (shared_ptr c, shared_ptr log) { dcp::DCP dcp (c->directory ()); dcp.read (); + if (c->kdm ()) { + dcp.add (dcp::DecryptedKDM (c->kdm().get (), Config::instance()->decryption_private_key ())); + } assert (dcp.cpls().size() == 1); _reels = dcp.cpls().front()->reels (); _reel = _reels.begin (); @@ -54,7 +58,7 @@ DCPDecoder::DCPDecoder (shared_ptr c, shared_ptr log) bool DCPDecoder::pass () { - if (_reel == _reels.end ()) { + if (_reel == _reels.end () || !_dcp_content->can_be_played ()) { return true; } diff --git a/src/lib/dcp_examiner.cc b/src/lib/dcp_examiner.cc index 625276e18..1e4cc899d 100644 --- a/src/lib/dcp_examiner.cc +++ b/src/lib/dcp_examiner.cc @@ -22,21 +22,38 @@ #include #include #include +#include +#include +#include +#include #include #include "dcp_examiner.h" #include "dcp_content.h" #include "exceptions.h" +#include "image.h" +#include "config.h" #include "i18n.h" using std::list; +using std::cout; using boost::shared_ptr; +using boost::dynamic_pointer_cast; DCPExaminer::DCPExaminer (shared_ptr content) + : _video_length (0) + , _audio_length (0) + , _has_subtitles (false) + , _encrypted (false) + , _kdm_valid (false) { dcp::DCP dcp (content->directory ()); dcp.read (); + if (content->kdm ()) { + dcp.add (dcp::DecryptedKDM (content->kdm().get(), Config::instance()->decryption_private_key ())); + } + if (dcp.cpls().size() == 0) { throw DCPError ("No CPLs found in DCP"); } else if (dcp.cpls().size() > 1) { @@ -89,4 +106,31 @@ DCPExaminer::DCPExaminer (shared_ptr content) _has_subtitles = true; } } + + _encrypted = dcp.encrypted (); + _kdm_valid = true; + + /* Check that we can read the first picture frame */ + try { + if (!dcp.cpls().empty () && !dcp.cpls().front()->reels().empty ()) { + shared_ptr mxf = dcp.cpls().front()->reels().front()->main_picture()->mxf (); + shared_ptr mono = dynamic_pointer_cast (mxf); + shared_ptr stereo = dynamic_pointer_cast (mxf); + + shared_ptr image (new Image (PIX_FMT_RGB24, _video_size.get(), false)); + + if (mono) { + mono->get_frame(0)->rgb_frame (image->data()[0]); + } else { + stereo->get_frame(0)->rgb_frame (dcp::EYE_LEFT, image->data()[0]); + } + + } + } catch (dcp::DCPReadError& e) { + _kdm_valid = false; + if (_encrypted && content->kdm ()) { + /* XXX: maybe don't use an exception for this */ + throw StringError (_("The KDM does not decrypt the DCP. Perhaps it is targeted at the wrong CPL")); + } + } } diff --git a/src/lib/dcp_examiner.h b/src/lib/dcp_examiner.h index 5b510743b..03d43d0f6 100644 --- a/src/lib/dcp_examiner.h +++ b/src/lib/dcp_examiner.h @@ -47,6 +47,10 @@ public: return _has_subtitles; } + bool encrypted () const { + return _encrypted; + } + int audio_channels () const { return _audio_channels.get_value_or (0); } @@ -59,6 +63,10 @@ public: return _audio_frame_rate.get_value_or (48000); } + bool kdm_valid () const { + return _kdm_valid; + } + private: boost::optional _video_frame_rate; boost::optional _video_size; @@ -68,4 +76,6 @@ private: ContentTime _audio_length; std::string _name; bool _has_subtitles; + bool _encrypted; + bool _kdm_valid; }; diff --git a/src/lib/film.cc b/src/lib/film.cc index c3194eca8..99a668e37 100644 --- a/src/lib/film.cc +++ b/src/lib/film.cc @@ -939,6 +939,13 @@ Film::content () const return _playlist->content (); } +void +Film::examine_content (shared_ptr c) +{ + shared_ptr j (new ExamineContentJob (shared_from_this(), c)); + JobManager::instance()->add (j); +} + void Film::examine_and_add_content (shared_ptr c) { @@ -1083,7 +1090,7 @@ Film::make_kdm ( } return dcp::DecryptedKDM ( - cpl, from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string() + cpl, key(), from, until, "DCP-o-matic", cpl->content_title_text(), dcp::LocalTime().as_string() ).encrypt (signer, target, formulation); } diff --git a/src/lib/film.h b/src/lib/film.h index 6c3f78895..dea669d98 100644 --- a/src/lib/film.h +++ b/src/lib/film.h @@ -250,6 +250,7 @@ public: void set_directory (boost::filesystem::path); void set_name (std::string); void set_use_isdcf_name (bool); + void examine_content (boost::shared_ptr); void examine_and_add_content (boost::shared_ptr); void add_content (boost::shared_ptr); void remove_content (boost::shared_ptr); diff --git a/src/lib/player.cc b/src/lib/player.cc index c8ac591a7..06f9e1365 100644 --- a/src/lib/player.cc +++ b/src/lib/player.cc @@ -31,6 +31,7 @@ #include "subtitle_content.h" #include "subrip_decoder.h" #include "subrip_content.h" +#include "dcp_content.h" #include "playlist.h" #include "job.h" #include "image.h" @@ -189,7 +190,8 @@ Player::content_changed (weak_ptr w, int property, bool frequent) property == ContentProperty::TRIM_START || property == ContentProperty::TRIM_END || property == ContentProperty::PATH || - property == VideoContentProperty::VIDEO_FRAME_TYPE + property == VideoContentProperty::VIDEO_FRAME_TYPE || + property == DCPContentProperty::CAN_BE_PLAYED ) { _have_valid_pieces = false; diff --git a/src/lib/video_content.cc b/src/lib/video_content.cc index a8590ce55..0d9a8fc45 100644 --- a/src/lib/video_content.cc +++ b/src/lib/video_content.cc @@ -192,7 +192,7 @@ VideoContent::take_from_video_examiner (shared_ptr d) dcp::Size const vs = d->video_size (); float const vfr = d->video_frame_rate (); ContentTime vl = d->video_length (); - + { boost::mutex::scoped_lock lm (_mutex); _video_size = vs; diff --git a/src/wx/config_dialog.cc b/src/wx/config_dialog.cc index ee660832f..3775ae1bb 100644 --- a/src/wx/config_dialog.cc +++ b/src/wx/config_dialog.cc @@ -39,6 +39,8 @@ #include "lib/colour_conversion.h" #include "lib/log.h" #include "lib/util.h" +#include "lib/cross.h" +#include "lib/exceptions.h" #include "config_dialog.h" #include "wx_util.h" #include "editable_list.h" @@ -656,6 +658,10 @@ public: s->Add (_load_decryption_private_key, 0, wxLEFT, DCPOMATIC_SIZER_X_GAP); table->Add (s, 0); } + + _export_decryption_certificate = new wxButton (_panel, wxID_ANY, _("Export DCP decryption certificate...")); + table->Add (_export_decryption_certificate); + table->AddSpacer (0); _add_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::add_certificate, this)); _remove_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::remove_certificate, this)); @@ -664,6 +670,7 @@ public: _load_signer_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_signer_private_key, this)); _load_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_certificate, this)); _load_decryption_private_key->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::load_decryption_private_key, this)); + _export_decryption_certificate->Bind (wxEVT_COMMAND_BUTTON_CLICKED, boost::bind (&KeysPage::export_decryption_certificate, this)); _signer.reset (new dcp::Signer (*Config::instance()->signer().get ())); @@ -814,6 +821,26 @@ private: _decryption_private_key->SetLabel (std_to_wx (dcp::private_key_fingerprint (Config::instance()->decryption_private_key()))); } + void export_decryption_certificate () + { + wxFileDialog* d = new wxFileDialog ( + _panel, _("Select Certificate File"), wxEmptyString, wxEmptyString, wxT ("PEM files (*.pem)|*.pem"), + wxFD_SAVE | wxFD_OVERWRITE_PROMPT + ); + + if (d->ShowModal () == wxID_OK) { + FILE* f = fopen_boost (wx_to_std (d->GetPath ()), "w"); + if (!f) { + throw OpenFileError (wx_to_std (d->GetPath ())); + } + + string const s = Config::instance()->decryption_certificate().certificate (true); + fwrite (s.c_str(), 1, s.length(), f); + fclose (f); + } + d->Destroy (); + } + wxPanel* _panel; wxListCtrl* _certificates; wxButton* _add_certificate; @@ -824,6 +851,7 @@ private: wxButton* _load_decryption_certificate; wxStaticText* _decryption_private_key; wxButton* _load_decryption_private_key; + wxButton* _export_decryption_certificate; shared_ptr _signer; }; diff --git a/src/wx/content_menu.cc b/src/wx/content_menu.cc index b91c82ab1..b396ceb41 100644 --- a/src/wx/content_menu.cc +++ b/src/wx/content_menu.cc @@ -26,6 +26,7 @@ #include "lib/examine_content_job.h" #include "lib/job_manager.h" #include "lib/exceptions.h" +#include "lib/dcp_content.h" #include "content_menu.h" #include "repeat_dialog.h" #include "wx_util.h" @@ -40,6 +41,7 @@ enum { ID_repeat = 1, ID_join, ID_find_missing, + ID_kdm, ID_remove }; @@ -50,12 +52,14 @@ ContentMenu::ContentMenu (wxWindow* p) _repeat = _menu->Append (ID_repeat, _("Repeat...")); _join = _menu->Append (ID_join, _("Join")); _find_missing = _menu->Append (ID_find_missing, _("Find missing...")); + _kdm = _menu->Append (ID_kdm, _("Add KDM...")); _menu->AppendSeparator (); _remove = _menu->Append (ID_remove, _("Remove")); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::repeat, this), ID_repeat); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::join, this), ID_join); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::find_missing, this), ID_find_missing); + _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::kdm, this), ID_kdm); _parent->Bind (wxEVT_COMMAND_MENU_SELECTED, boost::bind (&ContentMenu::remove, this), ID_remove); } @@ -81,6 +85,14 @@ ContentMenu::popup (weak_ptr f, ContentList c, wxPoint p) _join->Enable (n > 1); _find_missing->Enable (_content.size() == 1 && !_content.front()->paths_valid ()); + + if (_content.size() == 1) { + shared_ptr dcp = dynamic_pointer_cast (_content.front ()); + _kdm->Enable (dcp && dcp->encrypted ()); + } else { + _kdm->Enable (false); + } + _remove->Enable (!_content.empty ()); _parent->PopupMenu (_menu, p); } @@ -226,3 +238,22 @@ ContentMenu::maybe_found_missing (weak_ptr j, weak_ptr oc, weak_pt old_content->set_path (new_content->path (0)); } + +void +ContentMenu::kdm () +{ + assert (!_content.empty ()); + shared_ptr dcp = dynamic_pointer_cast (_content.front ()); + assert (dcp); + + wxFileDialog* d = new wxFileDialog (_parent, _("Select KDM")); + + if (d->ShowModal() == wxID_OK) { + dcp->add_kdm (dcp::EncryptedKDM (wx_to_std (d->GetPath ()))); + shared_ptr film = _film.lock (); + assert (film); + film->examine_content (dcp); + } + + d->Destroy (); +} diff --git a/src/wx/content_menu.h b/src/wx/content_menu.h index f2ad3aa75..fccd5f38a 100644 --- a/src/wx/content_menu.h +++ b/src/wx/content_menu.h @@ -39,6 +39,7 @@ private: void repeat (); void join (); void find_missing (); + void kdm (); void remove (); void maybe_found_missing (boost::weak_ptr, boost::weak_ptr, boost::weak_ptr); @@ -50,6 +51,7 @@ private: wxMenuItem* _repeat; wxMenuItem* _join; wxMenuItem* _find_missing; + wxMenuItem* _kdm; wxMenuItem* _remove; }; diff --git a/src/wx/content_panel.cc b/src/wx/content_panel.cc index 13ae2d88a..5ef2d8356 100644 --- a/src/wx/content_panel.cc +++ b/src/wx/content_panel.cc @@ -399,7 +399,7 @@ ContentPanel::set_selection (weak_ptr wc) void ContentPanel::film_content_changed (int property) { - if (property == ContentProperty::PATH || property == ContentProperty::POSITION) { + if (property == ContentProperty::PATH || property == ContentProperty::POSITION || property == DCPContentProperty::CAN_BE_PLAYED) { setup (); } @@ -425,19 +425,26 @@ ContentPanel::setup () for (ContentList::iterator i = content.begin(); i != content.end(); ++i) { int const t = _content->GetItemCount (); bool const valid = (*i)->paths_valid (); + shared_ptr dcp = dynamic_pointer_cast (*i); + bool const needs_kdm = !dcp->can_be_played (); string s = (*i)->summary (); + if (!valid) { s = _("MISSING: ") + s; } + if (needs_kdm) { + s = _("NEEDS KDM: ") + s; + } + _content->InsertItem (t, std_to_wx (s)); if ((*i)->summary() == selected_summary) { _content->SetItemState (t, wxLIST_STATE_SELECTED, wxLIST_STATE_SELECTED); } - if (!valid) { + if (!valid || needs_kdm) { _content->SetItemTextColour (t, *wxRED); } } diff --git a/src/wx/film_viewer.cc b/src/wx/film_viewer.cc index 74e3b81ed..80b12bf76 100644 --- a/src/wx/film_viewer.cc +++ b/src/wx/film_viewer.cc @@ -24,6 +24,7 @@ #include #include #include +#include #include "lib/film.h" #include "lib/ratio.h" #include "lib/util.h" @@ -153,9 +154,24 @@ FilmViewer::get (DCPTime p, bool accurate) list > pvf = _player->get_video (p, accurate); if (!pvf.empty ()) { - _frame = pvf.front()->image (true); - _frame = _frame->scale (_frame->size(), Scaler::from_id ("fastbilinear"), PIX_FMT_RGB24, false); - _position = pvf.front()->time (); + try { + _frame = pvf.front()->image (true); + _frame = _frame->scale (_frame->size(), Scaler::from_id ("fastbilinear"), PIX_FMT_RGB24, false); + _position = pvf.front()->time (); + } catch (dcp::DCPReadError& e) { + /* This can happen on the following sequence of events: + * - load encrypted DCP + * - add KDM + * - DCP is examined again, which sets its "playable" flag to 1 + * - as a side effect of the exam, the viewer is updated using the old pieces + * - the DCPDecoder in the old piece gives us an encrypted frame + * - then, the pieces are re-made (but too late). + * + * I hope there's a better way to handle this ... + */ + _frame.reset (); + _position = p; + } } else { _frame.reset (); _position = p;