From e9dc5baf79e50f42a64a69832f67a6945b07819d Mon Sep 17 00:00:00 2001
From: alangsto <46360176+alangsto@users.noreply.github.com>
Date: Thu, 7 Jan 2021 09:05:33 -0500
Subject: [PATCH] added two endpoints for IDV decryption on stage (#25939)

simplified public keys

made migration

fixes for quality

added pylint fixes

fixed for pylint

added endpoint to retrieve user's receipt_ids

added tests for 404 with decryption error

fixed for quality

fixed for quality

updates for feedback

removed unnecessary method

fixed quality issue

updated tests
---
 lms/djangoapps/verify_student/models.py       |  82 ++++++-
 .../verify_student/tests/test_views.py        | 214 ++++++++++++++++--
 lms/djangoapps/verify_student/urls.py         |  12 +
 lms/djangoapps/verify_student/views.py        |  69 +++++-
 4 files changed, 350 insertions(+), 27 deletions(-)

diff --git a/lms/djangoapps/verify_student/models.py b/lms/djangoapps/verify_student/models.py
index 2d1f8160e3a..430ee98236f 100644
--- a/lms/djangoapps/verify_student/models.py
+++ b/lms/djangoapps/verify_student/models.py
@@ -37,14 +37,20 @@ from opaque_keys.edx.django.models import CourseKeyField
 
 from lms.djangoapps.verify_student.ssencrypt import (
     encrypt_and_encode,
+    decode_and_decrypt,
     generate_signed_message,
     random_aes_key,
-    rsa_encrypt
+    rsa_encrypt,
+    rsa_decrypt
 )
 from openedx.core.djangoapps.signals.signals import LEARNER_NOW_VERIFIED
 from openedx.core.storage import get_storage
 
-from .utils import auto_verify_for_testing_enabled, earliest_allowed_verification_date, submit_request_to_ss
+from .utils import (
+    auto_verify_for_testing_enabled,
+    earliest_allowed_verification_date,
+    submit_request_to_ss
+)
 
 log = logging.getLogger(__name__)
 
@@ -691,6 +697,14 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
         except cls.DoesNotExist:
             return None
 
+    def _save_image_to_storage(self, path, img_data):
+        """
+        Given a path and data, save to S3
+        Separated out for ease of mocking in testing
+        """
+        buff = ContentFile(img_data)
+        self._storage.save(path, buff)
+
     @status_before_must_be("created")
     def upload_face_image(self, img_data):
         """
@@ -716,9 +730,10 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
         else:
             aes_key = aes_key_str.decode("hex")
 
+        encrypted_data = encrypt_and_encode(img_data, aes_key)
+
         path = self._get_path("face")
-        buff = ContentFile(encrypt_and_encode(img_data, aes_key))
-        self._storage.save(path, buff)
+        self._save_image_to_storage(path, encrypted_data)
 
     @status_before_must_be("created")
     def upload_photo_id_image(self, img_data):
@@ -748,8 +763,8 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
 
         # Save this to the storage backend
         path = self._get_path("photo_id")
-        buff = ContentFile(encrypt_and_encode(img_data, aes_key))
-        self._storage.save(path, buff)
+        encrypted_data = encrypt_and_encode(img_data, aes_key)
+        self._save_image_to_storage(path, encrypted_data)
 
         # Update our record fields
         if six.PY3:
@@ -759,6 +774,61 @@ class SoftwareSecurePhotoVerification(PhotoVerification):
 
         self.save()
 
+    def _get_image_from_storage(self, path):
+        """
+        Given a path, read data from storage and return
+        Separated for ease of mocking in testing
+        """
+        with self._storage.open(path, mode='rb') as img_file:
+            byte_img_data = img_file.read()
+        return byte_img_data
+
+    @status_before_must_be("must_retry", "submitted", "approved", "denied")
+    def download_face_image(self):
+        """
+        Download the associated face image from storage
+        """
+        if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None):
+            return None
+        path = self._get_path("face")
+        byte_img_data = self._get_image_from_storage(path)
+
+        aes_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"]
+
+        try:
+            if six.PY3:
+                aes_key = codecs.decode(aes_key_str, "hex")
+            else:
+                aes_key = aes_key_str.decode("hex")
+
+            img_bytes = decode_and_decrypt(byte_img_data, aes_key)
+            return img_bytes
+        except:  # pylint: disable=bare-except
+            return None
+
+    @status_before_must_be("must_retry", "submitted", "approved", "denied")
+    def download_photo_id_image(self):
+        """
+        Download the associated id image from storage
+        """
+        if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None):
+            return None
+
+        path = self._get_path("photo_id")
+        byte_img_data = self._get_image_from_storage(path)
+
+        try:
+            # decode rsa encrypted aes key from base64
+            rsa_encrypted_aes_key = base64.urlsafe_b64decode(self.photo_id_key)
+
+            # decrypt aes key using rsa private key
+            rsa_private_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["RSA_PRIVATE_KEY"]
+            decrypted_aes_key = rsa_decrypt(rsa_encrypted_aes_key, rsa_private_key_str)
+            img_bytes = decode_and_decrypt(byte_img_data, decrypted_aes_key)
+            return img_bytes
+        except:  # pylint: disable=bare-except
+            return None
+
     @status_before_must_be("must_retry", "ready", "submitted")
     def submit(self, copy_id_photo_from=None):
         """
diff --git a/lms/djangoapps/verify_student/tests/test_views.py b/lms/djangoapps/verify_student/tests/test_views.py
index ce763db5439..2b7e9df8b2f 100644
--- a/lms/djangoapps/verify_student/tests/test_views.py
+++ b/lms/djangoapps/verify_student/tests/test_views.py
@@ -6,6 +6,8 @@ Tests of verify_student views.
 from datetime import timedelta
 from uuid import uuid4
 
+import base64
+import codecs
 import ddt
 import httpretty
 import mock
@@ -34,6 +36,7 @@ from lms.djangoapps.commerce.utils import EcommerceService
 from lms.djangoapps.verify_student.models import SoftwareSecurePhotoVerification, VerificationDeadline
 from lms.djangoapps.verify_student.services import IDVerificationService
 from lms.djangoapps.verify_student.views import PayAndVerifyView, checkout_with_ecommerce_service, render_to_response
+from lms.djangoapps.verify_student.ssencrypt import encrypt_and_encode, rsa_encrypt
 from openedx.core.djangoapps.embargo.test_utils import restrict_course
 from openedx.core.djangoapps.theming.tests.test_util import with_comprehensive_theme
 from openedx.core.djangoapps.user_api.accounts.api import get_account_settings
@@ -55,6 +58,43 @@ render_mock = Mock(side_effect=mock_render_to_response)
 
 PAYMENT_DATA_KEYS = {'payment_processor_name', 'payment_page_url', 'payment_form_data'}
 
+RSA_PUBLIC_KEY = b"""-----BEGIN PUBLIC KEY-----
+MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA1hLVjP0oV0Uy/+jQ+Upz
+c+eYc4Pyflb/WpfgYATggkoQdnsdplmvPtQr85+utgqKPxOh+PvYGW8QNUzjLIu4
+5/GlmvBa82i1jRMgEAxGI95bz7j9DtH+7mnj+06zR5xHwT49jK0zMs5MjMaz5WRq
+BUNkz7dxWzDrYJZQx230sPp6upy1Y5H5O8SnJVdghsh8sNciS4Bo4ZONQ3giBwxz
+h5svjspz1MIsOoShjbAdfG+4VX7sVwYlw2rnQeRsMH5/xpnNeqtScyOMoz0N9UDG
+dtRMNGa2MihAg7zh7/zckbUrtf+o5wQtlCJL1Kdj4EjshqYvCxzWnSM+MaYAjb3M
+EQIDAQAB
+-----END PUBLIC KEY-----"""
+RSA_PRIVATE_KEY = b"""-----BEGIN RSA PRIVATE KEY-----
+MIIEpAIBAAKCAQEA1hLVjP0oV0Uy/+jQ+Upzc+eYc4Pyflb/WpfgYATggkoQdnsd
+plmvPtQr85+utgqKPxOh+PvYGW8QNUzjLIu45/GlmvBa82i1jRMgEAxGI95bz7j9
+DtH+7mnj+06zR5xHwT49jK0zMs5MjMaz5WRqBUNkz7dxWzDrYJZQx230sPp6upy1
+Y5H5O8SnJVdghsh8sNciS4Bo4ZONQ3giBwxzh5svjspz1MIsOoShjbAdfG+4VX7s
+VwYlw2rnQeRsMH5/xpnNeqtScyOMoz0N9UDGdtRMNGa2MihAg7zh7/zckbUrtf+o
+5wQtlCJL1Kdj4EjshqYvCxzWnSM+MaYAjb3MEQIDAQABAoIBAQCviuA87fdfoOoS
+OerrEacc20QDLaby/QoGUtZ2RmmHzY40af7FQ3PWFIw6Ca5trrTwxnuivXnWWWG0
+I2mCRM0Kvfgr1n7ubOW7WnyHTFlT3mnxK2Ov/HmNLZ36nO2cgkXA6/Xy3rBGMC9L
+nUE1kSLzT/Fh965ntfS9zmVNNBhb6no0rVkGx5nK3vTI6kUmaa0m+E7KL/HweO4c
+JodhN8CX4gpxSrkuwJ7IHEPYspqc0jInMYKLmD3d2g3BiOctjzFmaj3lV5AUlujW
+z7/LVe5WAEaaxjwaMvwqrJLv9ogxWU3etJf22+Yy7r5gbPtqpqJrCZ5+WpGnUHws
+3mMGP2QBAoGBAOc3pzLFgGUREVPSFQlJ06QFtfKYqg9fFHJCgWu/2B2aVZc2aO/t
+Zhuoz+AgOdzsw+CWv7K0FH9sUkffk2VKPzwwwufLK3avD9gI0bhmBAYvdhS6A3nO
+YM3W+lvmaJtFL00K6kdd+CzgRnBS9cZ70WbcbtqjdXI6+mV1WdGUTLhBAoGBAO0E
+xhD4z+GjubSgfHYEZPgRJPqyUIfDH+5UmFGpr6zlvNN/depaGxsbhW8t/V6xkxsG
+MCgic7GLMihEiUMx1+/snVs5bBUx7OT9API0d+vStHCFlTTe6aTdmiduFD4PbDsq
+6E4DElVRqZhpIYusdDh7Z3fO2hm5ad4FfMlx65/RAoGAPYEfV7ETs06z9kEG2X6q
+7pGaUZrsecRH8xDfzmKswUshg2S0y0WyCJ+CFFNeMPdGL4LKIWYnobGVvYqqcaIr
+af5qijAQMrTkmQnXh56TaXXMijzk2czdEUQjOrjykIL5zxudMDi94GoUMqLOv+qF
+zD/MuRoMDsPDgaOSrd4t/kECgYEAzwBNT8NOIz3P0Z4cNSJPYIvwpPaY+IkE2SyO
+vzuYj0Mx7/Ew9ZTueXVGyzv6PfqOhJqZ8mNscZIlIyAAVWwxsHwRTfvPlo882xzP
+97i1R4OFTYSNNFi+69sSZ/9utGjZ2K73pjJuj487tD2VK5xZAH9edTd2KeNSP7LB
+MlpJNBECgYAmIswPdldm+G8SJd5j9O2fcDVTURjKAoSXCv2j4gEZzzfudpLWNHYu
+l8N6+LEIVTMAytPk+/bImHvGHKZkCz5rEMSuYJWOmqKI92rUtI6fz5DUb3XSbrwT
+3W+sdGFUK3GH1NAX71VxbAlFVLUetcMwai1+wXmGkRw6A7YezVFnhw==
+-----END RSA PRIVATE KEY-----"""
+
 
 def _mock_payment_processors():
     """
@@ -1259,14 +1299,7 @@ class TestSubmitPhotosForVerification(MockS3BotoMixin, TestVerificationBase):
             "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006",
             "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7",
             "FACE_IMAGE_AES_KEY": "f82400259e3b4f88821cd89838758292",
-            "RSA_PUBLIC_KEY": (
-                "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkgtz3fQdiXshy/RfOHkoHlhx/"
-                "SSPZ+nNyE9JZXtwhlzsXjnu+e9GOuJzgh4kUqo73ePIG5FxVU+mnacvufq2cu1SOx"
-                "lRYGyBK7qDf9Ym67I5gmmcNhbzdKcluAuDCPmQ4ecKpICQQldrDQ9HWDxwjbbcqpVB"
-                "PYWkE1KrtypGThmcehLmabf6SPq1CTAGlXsHgUtbWCwV6mqR8yScV0nRLln0djLDm9d"
-                "L8tIVFFVpAfBaYYh2Cm5EExQZjxyfjWd8P5H+8/l0pmK2jP7Hc0wuXJemIZbsdm+DSD"
-                "FhCGY3AILGkMwr068dGRxfBtBy/U9U5W+nStvkDdMrSgQezS5+V test@example.com"
-            ),
+            "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY,
             "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1",
             "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881",
             "S3_BUCKET": "test.example.com",
@@ -1784,14 +1817,7 @@ class TestReverifyView(TestVerificationBase):
             "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006",
             "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7",
             "FACE_IMAGE_AES_KEY": "f82400259e3b4f88821cd89838758292",
-            "RSA_PUBLIC_KEY": (
-                "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDkgtz3fQdiXshy/RfOHkoHlhx/"
-                "SSPZ+nNyE9JZXtwhlzsXjnu+e9GOuJzgh4kUqo73ePIG5FxVU+mnacvufq2cu1SOx"
-                "lRYGyBK7qDf9Ym67I5gmmcNhbzdKcluAuDCPmQ4ecKpICQQldrDQ9HWDxwjbbcqpVB"
-                "PYWkE1KrtypGThmcehLmabf6SPq1CTAGlXsHgUtbWCwV6mqR8yScV0nRLln0djLDm9d"
-                "L8tIVFFVpAfBaYYh2Cm5EExQZjxyfjWd8P5H+8/l0pmK2jP7Hc0wuXJemIZbsdm+DSD"
-                "FhCGY3AILGkMwr068dGRxfBtBy/U9U5W+nStvkDdMrSgQezS5+V test@example.com"
-            ),
+            "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY,
             "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1",
             "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881",
             "CERT_VERIFICATION_PATH": False,
@@ -1803,9 +1829,9 @@ class TestReverifyView(TestVerificationBase):
         },
     },
 )
-class TestPhotoURLView(ModuleStoreTestCase, TestVerificationBase):
+class TestPhotoURLView(TestVerificationBase):
     """
-    Tests for the results_callback view.
+    Tests for the photo url view.
     """
 
     def setUp(self):
@@ -1853,3 +1879,155 @@ class TestPhotoURLView(ModuleStoreTestCase, TestVerificationBase):
         url = reverse('verification_photo_urls', kwargs={'receipt_id': six.text_type(self.receipt_id)})
         response = self.client.get(url)
         self.assertEqual(response.status_code, 403)
+
+
+@override_settings(
+    VERIFY_STUDENT={
+        "SOFTWARE_SECURE": {
+            "API_URL": "https://verify.example.com/submit/",
+            "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006",
+            "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7",
+            "FACE_IMAGE_AES_KEY": b'32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae',
+            "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY,
+            "RSA_PRIVATE_KEY": RSA_PRIVATE_KEY,
+            "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1",
+            "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881",
+            "S3_BUCKET": "test-idv",
+            "CERT_VERIFICATION_PATH": False,
+        },
+        "DAYS_GOOD_FOR": 10,
+        "STORAGE_CLASS": 'storages.backends.s3boto.S3BotoStorage',
+        "STORAGE_KWARGS": {
+            'bucket': 'test-idv',
+        },
+    }
+)
+@ddt.ddt
+class TestDecodeImageViews(MockS3BotoMixin, TestVerificationBase):
+    """
+    Test for both face and photo id image decoding views
+    """
+
+    IMAGE_DATA = "abcd,1234"
+
+    def setUp(self):
+        super().setUp()
+        self.user = AdminFactory()
+        login_success = self.client.login(username=self.user.username, password='test')
+        self.assertTrue(login_success)
+
+    def _mock_submit_images(self):
+        """
+        Mocks submitting images for IDV and saving to S3
+        """
+        # create an attempt with a submitted status, and create a photo_id_key to use
+        # for decryption
+        attempt = SoftwareSecurePhotoVerification(
+            status="submitted",
+            user=self.user
+        )
+        rsa_key_str = settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["RSA_PUBLIC_KEY"]
+        rsa_encrypted_aes_key = rsa_encrypt(
+            codecs.decode(
+                settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"],
+                "hex"
+            ),
+            rsa_key_str
+        )
+        attempt.photo_id_key = codecs.encode(rsa_encrypted_aes_key, 'base64').decode('utf-8')
+
+        attempt.save()
+
+    def _decode_image(self, receipt_id, img_type):
+        """
+        Test function used to call decoding endpoint
+        Arg:
+            receipt_id(str): receipt ID for endpoint url
+            img_type(str): 'face' or 'photo_id', used to determine which endpoint to use
+        """
+        url_name = 'verification_decrypt_face_image'
+        if img_type == 'photo_id':
+            url_name = 'verification_decrypt_photo_id_image'
+        url = reverse(url_name, kwargs={'receipt_id': six.text_type(receipt_id)})
+
+        response = self.client.get(url)
+
+        return response
+
+    @ddt.data("face", "photo_id")
+    @patch.object(SoftwareSecurePhotoVerification, '_get_image_from_storage')
+    def test_download_image_response(self, img_type, _mock_get_storage):
+        _mock_get_storage.return_value = encrypt_and_encode(
+            b'\xd7m\xf8',
+            codecs.decode(settings.VERIFY_STUDENT["SOFTWARE_SECURE"]["FACE_IMAGE_AES_KEY"], "hex")
+        )
+        # upload 'images'
+        self._mock_submit_images()
+        attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
+        receipt_id = attempt.receipt_id
+
+        #mock downloading and decrypting images
+        response = self._decode_image(receipt_id, img_type)
+        self.assertEqual(response.status_code, 200)
+        self.assertEqual(response.content, base64.b64decode(self.IMAGE_DATA.split(",")[1]))
+
+    @ddt.data("face", "photo_id")
+    def test_403_for_non_staff(self, img_type):
+        self.user = UserFactory()
+        login_success = self.client.login(username=self.user.username, password='test')
+        self.assertTrue(login_success)
+
+        self._mock_submit_images()
+        attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
+        receipt_id = attempt.receipt_id
+
+        # mock downloading and decrypting images
+        response = self._decode_image(receipt_id, img_type)
+        self.assertEqual(response.status_code, 403)
+
+    @override_settings(
+        VERIFY_STUDENT={
+            "SOFTWARE_SECURE": {
+                "API_URL": "https://verify.example.com/submit/",
+                "API_ACCESS_KEY": "dcf291b5572942f99adaab4c2090c006",
+                "API_SECRET_KEY": "c392efdcc0354c5f922dc39844ec0dc7",
+                "FACE_IMAGE_AES_KEY": b'32fe72aaf2abb44de9e161131b5435c8d37cbdb6f5df242ae860b283115f2dae',
+                "RSA_PUBLIC_KEY": RSA_PUBLIC_KEY,
+                "AWS_ACCESS_KEY": "c987c7efe35c403caa821f7328febfa1",
+                "AWS_SECRET_KEY": "fc595fc657c04437bb23495d8fe64881",
+                "S3_BUCKET": "test-idv",
+                "CERT_VERIFICATION_PATH": False,
+            },
+            "DAYS_GOOD_FOR": 10,
+        }
+    )
+    @ddt.data("face", "photo_id")
+    def test_403_for_non_staging(self, img_type):
+        self._mock_submit_images()
+        attempt = SoftwareSecurePhotoVerification.objects.get(user=self.user)
+        receipt_id = attempt.receipt_id
+
+        # mock downloading and decrypting images
+        response = self._decode_image(receipt_id, img_type)
+        self.assertEqual(response.status_code, 403)
+
+    @ddt.data("face", "photo_id")
+    def test_404_if_invalid_receipt_id(self, img_type):
+        response = self._decode_image('00000000-0000-0000-0000-000000000000', img_type)
+        self.assertEqual(response.status_code, 404)
+
+    @ddt.data("face", "photo_id")
+    @patch.object(SoftwareSecurePhotoVerification, '_get_image_from_storage')
+    def test_404_for_decryption_error(self, img_type, _mock_get_storage):
+        _mock_get_storage.return_value = None
+        # create verification with no img data
+        attempt = SoftwareSecurePhotoVerification(
+            status="submitted",
+            user=self.user
+        )
+        attempt.save()
+        receipt_id = attempt.receipt_id
+
+        # mock downloading and decrypting images
+        response = self._decode_image(receipt_id, img_type)
+        self.assertEqual(response.status_code, 404)
diff --git a/lms/djangoapps/verify_student/urls.py b/lms/djangoapps/verify_student/urls.py
index 39f103611be..db258bbeb45 100644
--- a/lms/djangoapps/verify_student/urls.py
+++ b/lms/djangoapps/verify_student/urls.py
@@ -106,6 +106,18 @@ urlpatterns = [
         views.PhotoUrlsView.as_view(),
         name="verification_photo_urls"
     ),
+
+    url(
+        r'^decrypt-idv-images/face/{receipt_id}$'.format(receipt_id=IDV_RECEIPT_ID_PATTERN),
+        views.DecryptFaceImageView.as_view(),
+        name="verification_decrypt_face_image"
+    ),
+
+    url(
+        r'^decrypt-idv-images/photo-id/{receipt_id}$'.format(receipt_id=IDV_RECEIPT_ID_PATTERN),
+        views.DecryptPhotoIDImageView.as_view(),
+        name="verification_decrypt_photo_id_image"
+    ),
 ]
 
 # Fake response page for incourse reverification ( software secure )
diff --git a/lms/djangoapps/verify_student/views.py b/lms/djangoapps/verify_student/views.py
index e1b88610a49..3138e4205f9 100644
--- a/lms/djangoapps/verify_student/views.py
+++ b/lms/djangoapps/verify_student/views.py
@@ -12,7 +12,7 @@ from django.conf import settings
 from django.contrib.auth.decorators import login_required
 from django.contrib.staticfiles.storage import staticfiles_storage
 from django.db import transaction
-from django.http import Http404, HttpResponse, HttpResponseBadRequest
+from django.http import Http404, HttpResponse, HttpResponseBadRequest, HttpResponseForbidden
 from django.shortcuts import redirect
 from django.urls import reverse
 from django.utils.decorators import method_decorator
@@ -24,7 +24,6 @@ from django.views.decorators.http import require_POST
 from django.views.generic.base import View
 from edx_rest_api_client.exceptions import SlumberBaseException
 from ipware.ip import get_ip
-from opaque_keys import InvalidKeyError
 from opaque_keys.edx.keys import CourseKey
 from rest_framework.response import Response
 from rest_framework.views import APIView
@@ -1225,7 +1224,7 @@ class PhotoUrlsView(APIView):
     def get(self, request, receipt_id):
         """
         Endpoint for retrieving photo urls for IDV
-        GET /verify_student/photo_urls/{receipt_id}
+        GET /verify_student/photo-urls/{receipt_id}
 
         Returns:
             200 OK
@@ -1247,3 +1246,67 @@ class PhotoUrlsView(APIView):
 
         log.warning(u"Could not find verification with receipt ID %s.", receipt_id)
         raise Http404
+
+
+class DecryptFaceImageView(APIView):
+    """
+    Endpoint to retrieve decrypted IDV face image data. Can only be used on stage.
+    """
+
+    @method_decorator(require_global_staff)
+    def get(self, request, receipt_id):
+        """
+        Endpoint used for decrypting images on stage based on a given receipt ID
+        GET /verify_student/decrypt-idv-images/face/{receipt_id}
+
+        Returns:
+            200 OK
+            {
+                img
+            }
+        """
+        # if this endpoint is not being accessed on stage, raise a 403. Only stage will have an RSA_PRIVATE_KEY
+        if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None):
+            log.warning(u"Cannot access image decryption outside of staging environment")
+            return HttpResponseForbidden()
+
+        verification = SoftwareSecurePhotoVerification.get_verification_from_receipt(receipt_id)
+        if verification:
+            user_photo = verification.download_face_image()
+            if user_photo:
+                return HttpResponse(user_photo, content_type="image/png")
+
+        log.warning(u"Could not decrypt face image for receipt ID %s.", receipt_id)
+        raise Http404
+
+
+class DecryptPhotoIDImageView(APIView):
+    """
+        Endpoint to retrieve decrypted IDV photo ID image data. Can only be used on stage.
+    """
+
+    @method_decorator(require_global_staff)
+    def get(self, request, receipt_id):
+        """
+        Endpoint used for decrypting images on stage based on a given receipt ID
+        GET /verify_student/decrypt-idv-images/photo-id/{receipt_id}
+
+        Returns:
+            200 OK
+            {
+                img
+            }
+        """
+        # if this endpoint is not being accessed on stage, raise a 403. Only stage will have an RSA_PRIVATE_KEY
+        if not settings.VERIFY_STUDENT["SOFTWARE_SECURE"].get("RSA_PRIVATE_KEY", None):
+            log.warning(u"Cannot access image decryption outside of staging environment")
+            return HttpResponseForbidden()
+
+        verification = SoftwareSecurePhotoVerification.get_verification_from_receipt(receipt_id)
+        if verification:
+            id_photo = verification.download_photo_id_image()
+            if id_photo:
+                return HttpResponse(id_photo, content_type="image/png")
+
+        log.warning(u"Could not decrypt photo ID image for receipt ID %s.", receipt_id)
+        raise Http404
-- 
GitLab