Basic support for decryption of imported DCPs.
authorCarl Hetherington <cth@carlh.net>
Sun, 20 Jul 2014 20:22:52 +0000 (21:22 +0100)
committerCarl Hetherington <cth@carlh.net>
Sun, 20 Jul 2014 20:22:52 +0000 (21:22 +0100)
14 files changed:
src/lib/dcp_content.cc
src/lib/dcp_content.h
src/lib/dcp_decoder.cc
src/lib/dcp_examiner.cc
src/lib/dcp_examiner.h
src/lib/film.cc
src/lib/film.h
src/lib/player.cc
src/lib/video_content.cc
src/wx/config_dialog.cc
src/wx/content_menu.cc
src/wx/content_menu.h
src/wx/content_panel.cc
src/wx/film_viewer.cc

index 0eef075d7c0b9eddcb58c7dfd544af212e90b21c..9d4ee63887e9e5d4aecc7d8d167639cef314b226 100644 (file)
 */
 
 #include <dcp/dcp.h>
+#include <dcp/exceptions.h>
 #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<const Film> f, boost::filesystem::path p)
        : Content (f)
@@ -35,7 +41,9 @@ DCPContent::DCPContent (shared_ptr<const Film> 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<const Film> 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> 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> 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;
 }
index 60b7142de1fe655dc0a6e4826a2841fb3d611334..da78e6d72a5cc0bd22f7f111c875013d41609ee3 100644 (file)
 
 */
 
+#ifndef DCPOMATIC_DCP_CONTENT_H
+#define DCPOMATIC_DCP_CONTENT_H
+
 /** @file  src/lib/dcp_content.h
  *  @brief DCPContent class.
  */
 
 #include <libcxml/cxml.h>
+#include <dcp/encrypted_kdm.h>
+#include <dcp/decrypted_kdm.h>
 #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<dcp::EncryptedKDM> 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<dcp::EncryptedKDM> _kdm;
+       /** true if _kdm successfully decrypts the first frame of our DCP */
+       bool _kdm_valid;
 };
+
+#endif
index d0642d8b6ca9f6fe06edd76a6f1b3611aa7a5496..bf016ef877bb11990bccf83a01821459cecb5a71 100644 (file)
@@ -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<const DCPContent> c, shared_ptr<Log> 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<const DCPContent> c, shared_ptr<Log> log)
 bool
 DCPDecoder::pass ()
 {
-       if (_reel == _reels.end ()) {
+       if (_reel == _reels.end () || !_dcp_content->can_be_played ()) {
                return true;
        }
 
index 625276e180de13f54a92a05b16f5e453f27e8d08..1e4cc899df96df614fd6a9b1413d3101245247a4 100644 (file)
 #include <dcp/reel.h>
 #include <dcp/reel_picture_asset.h>
 #include <dcp/reel_sound_asset.h>
+#include <dcp/mono_picture_mxf.h>
+#include <dcp/mono_picture_frame.h>
+#include <dcp/stereo_picture_mxf.h>
+#include <dcp/stereo_picture_frame.h>
 #include <dcp/sound_mxf.h>
 #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<const DCPContent> 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<const DCPContent> 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<dcp::PictureMXF> mxf = dcp.cpls().front()->reels().front()->main_picture()->mxf ();
+                       shared_ptr<dcp::MonoPictureMXF> mono = dynamic_pointer_cast<dcp::MonoPictureMXF> (mxf);
+                       shared_ptr<dcp::StereoPictureMXF> stereo = dynamic_pointer_cast<dcp::StereoPictureMXF> (mxf);
+                       
+                       shared_ptr<Image> 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"));
+               }
+       }
 }
index 5b510743b73c8ce8ba2a876c84e8fe5ae4d71b11..03d43d0f6c9f7a270ab2f2b16d147c4389ec289d 100644 (file)
@@ -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<float> _video_frame_rate;
        boost::optional<dcp::Size> _video_size;
@@ -68,4 +76,6 @@ private:
        ContentTime _audio_length;
        std::string _name;
        bool _has_subtitles;
+       bool _encrypted;
+       bool _kdm_valid;
 };
index c3194eca80b95a0636331eccf44b41a969ca7abd..99a668e373b2567a9ac24151fb91db13d3266f5f 100644 (file)
@@ -939,6 +939,13 @@ Film::content () const
        return _playlist->content ();
 }
 
+void
+Film::examine_content (shared_ptr<Content> c)
+{
+       shared_ptr<Job> j (new ExamineContentJob (shared_from_this(), c));
+       JobManager::instance()->add (j);
+}
+
 void
 Film::examine_and_add_content (shared_ptr<Content> 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);
 }
 
index 6c3f78895791221297b500ea64b1b4ea730777af..dea669d981f9088faa6b223ad7ccbc7ffe785f3e 100644 (file)
@@ -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<Content>);
        void examine_and_add_content (boost::shared_ptr<Content>);
        void add_content (boost::shared_ptr<Content>);
        void remove_content (boost::shared_ptr<Content>);
index c8ac591a7b2919a36a6f77080dad538417c08982..06f9e1365df3525d4807814bccdf8ec0fa348b54 100644 (file)
@@ -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<Content> 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;
index a8590ce55b4a70f23c3d6ec7519e59f3f9ee5219..0d9a8fc45c491c24b664751daf3f4a4df73f15ab 100644 (file)
@@ -192,7 +192,7 @@ VideoContent::take_from_video_examiner (shared_ptr<VideoExaminer> 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;
index ee660832f441561a244f0881cf7e895aeab56bcd..3775ae1bbd57fd9fc0cce79a3b0f3119cedda3c6 100644 (file)
@@ -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<dcp::Signer> _signer;
 };
 
index b91c82ab154c99e4bb2860bb16e7272888c50992..b396ceb41d211b65cad48075b2b0dabde183d040 100644 (file)
@@ -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<Film> 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<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_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<Job> j, weak_ptr<Content> oc, weak_pt
 
        old_content->set_path (new_content->path (0));
 }
+
+void
+ContentMenu::kdm ()
+{
+       assert (!_content.empty ());
+       shared_ptr<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (_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 = _film.lock ();
+               assert (film);
+               film->examine_content (dcp);
+       }
+       
+       d->Destroy ();
+}
index f2ad3aa75621ac04a114b46badd931b6bd92a4b1..fccd5f38a4b714070f7c2db7f6f4822be446313c 100644 (file)
@@ -39,6 +39,7 @@ private:
        void repeat ();
        void join ();
        void find_missing ();
+       void kdm ();
        void remove ();
        void maybe_found_missing (boost::weak_ptr<Job>, boost::weak_ptr<Content>, boost::weak_ptr<Content>);
        
@@ -50,6 +51,7 @@ private:
        wxMenuItem* _repeat;
        wxMenuItem* _join;
        wxMenuItem* _find_missing;
+       wxMenuItem* _kdm;
        wxMenuItem* _remove;
 };
 
index 13ae2d88ae6d9abd8e3bd5d0d00c9dff608daab0..5ef2d8356bbf6a4a259dccb954046e0aeeb61aef 100644 (file)
@@ -399,7 +399,7 @@ ContentPanel::set_selection (weak_ptr<Content> 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<DCPContent> dcp = dynamic_pointer_cast<DCPContent> (*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);
                }
        }
index 74e3b81ed82df4126c5ea1b5bee202b2feb3621b..80b12bf7627e797702799bd48a6ac7b3e2898ed8 100644 (file)
@@ -24,6 +24,7 @@
 #include <iostream>
 #include <iomanip>
 #include <wx/tglbtn.h>
+#include <dcp/exceptions.h>
 #include "lib/film.h"
 #include "lib/ratio.h"
 #include "lib/util.h"
@@ -153,9 +154,24 @@ FilmViewer::get (DCPTime p, bool accurate)
 
        list<shared_ptr<PlayerVideo> > 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;