Skip to content

Commit

Permalink
Merge pull request #515 from hmrc/APB-8276
Browse files Browse the repository at this point in the history
APB-8276 Setting up field encryption and support for unencrypted old data
  • Loading branch information
VLukovski authored Sep 25, 2024
2 parents 1f27beb + 2ed6f23 commit 7cd12f9
Show file tree
Hide file tree
Showing 11 changed files with 398 additions and 20 deletions.
25 changes: 23 additions & 2 deletions app/uk/gov/hmrc/agentservicesaccount/models/AgencyDetails.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,18 +16,31 @@

package uk.gov.hmrc.agentservicesaccount.models

import play.api.libs.json.{Json, OFormat}
import play.api.libs.functional.syntax.{toFunctionalBuilderOps, unlift}
import play.api.libs.json.{Format, Json, OFormat, __}
import uk.gov.hmrc.agentservicesaccount.utils.EncryptedStringUtil.fallbackStringFormat
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}

case class BusinessAddress(
addressLine1: String,
addressLine2: Option[String],
addressLine3: Option[String] = None,
addressLine4: Option[String]= None,
addressLine4: Option[String] = None,
postalCode: Option[String],
countryCode: String)

object BusinessAddress {
implicit val format: OFormat[BusinessAddress] = Json.format

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[BusinessAddress] =
(
(__ \ "addressLine1").format[String](fallbackStringFormat) and
(__ \ "addressLine2").formatNullable[String](fallbackStringFormat) and
(__ \ "addressLine3").formatNullable[String](fallbackStringFormat) and
(__ \ "addressLine4").formatNullable[String](fallbackStringFormat) and
(__ \ "postalCode").formatNullable[String](fallbackStringFormat) and
(__ \ "countryCode").format[String](fallbackStringFormat)
)(BusinessAddress.apply, unlift(BusinessAddress.unapply))
}

case class AgencyDetails(
Expand All @@ -39,5 +52,13 @@ case class AgencyDetails(

object AgencyDetails {
implicit val format: OFormat[AgencyDetails] = Json.format

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[AgencyDetails] =
(
(__ \ "agencyName").formatNullable[String](fallbackStringFormat) and
(__ \ "agencyEmail").formatNullable[String](fallbackStringFormat) and
(__ \ "agencyTelephone").formatNullable[String](fallbackStringFormat) and
(__ \ "agencyAddress").formatNullable[BusinessAddress](BusinessAddress.databaseFormat)
)(AgencyDetails.apply, unlift(AgencyDetails.unapply))
}

Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,22 @@

package uk.gov.hmrc.agentservicesaccount.models.desiDetails

import play.api.libs.json.{Json, OFormat}
import play.api.libs.functional.syntax.{toFunctionalBuilderOps, unlift}
import play.api.libs.json.{Format, Json, OFormat, __}
import uk.gov.hmrc.agentservicesaccount.models.AgencyDetails
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}

case class DesignatoryDetails(
agencyDetails: AgencyDetails,
otherServices: OtherServices
)
agencyDetails: AgencyDetails,
otherServices: OtherServices
)

object DesignatoryDetails {
implicit val desiDetailsFormat: OFormat[DesignatoryDetails] = Json.format[DesignatoryDetails]

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[DesignatoryDetails] =
(
(__ \ "agencyDetails").format[AgencyDetails](AgencyDetails.databaseFormat) and
(__ \ "otherServices").format[OtherServices](OtherServices.databaseFormat)
)(DesignatoryDetails.apply, unlift(DesignatoryDetails.unapply))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,32 +16,63 @@

package uk.gov.hmrc.agentservicesaccount.models.desiDetails

import play.api.libs.json.{Json, OFormat}
import play.api.libs.functional.syntax.{toFunctionalBuilderOps, unlift}
import play.api.libs.json.{Format, Json, OFormat, __}
import uk.gov.hmrc.agentservicesaccount.utils.EncryptedStringUtil.fallbackStringFormat
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}
import uk.gov.hmrc.domain.{CtUtr, SaUtr}

case class CtChanges(
applyChanges: Boolean,
ctAgentReference:Option[CtUtr]
ctAgentReference: Option[CtUtr]
)

object CtChanges {
implicit val ctChangesFormat: OFormat[CtChanges] = Json.format[CtChanges]

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[CtChanges] =
(
(__ \ "applyChanges").format[Boolean] and
(__ \ "ctAgentReference").formatNullable[String](fallbackStringFormat)
.bimap[Option[CtUtr]](
_.map(CtUtr(_)),
_.map(_.utr)
)
)(CtChanges.apply, unlift(CtChanges.unapply))
}


case class SaChanges(
applyChanges: Boolean,
saAgentReference:Option[SaUtr]
saAgentReference: Option[SaUtr]
)

object SaChanges {
implicit val saCodeChangesFormat: OFormat[SaChanges] = Json.format[SaChanges]

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[SaChanges] =
(
(__ \ "applyChanges").format[Boolean] and
(__ \ "saAgentReference").formatNullable[String](fallbackStringFormat)
.bimap[Option[SaUtr]](
_.map(SaUtr(_)),
_.map(_.utr)
)
)(SaChanges.apply, unlift(SaChanges.unapply))
}


case class OtherServices (
saChanges: SaChanges,
ctChanges: CtChanges
)
case class OtherServices(
saChanges: SaChanges,
ctChanges: CtChanges
)

object OtherServices {
implicit val otherServicesFormat: OFormat[OtherServices] = Json.format[OtherServices]

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[OtherServices] =
(
(__ \ "saChanges").format[SaChanges](SaChanges.databaseFormat) and
(__ \ "ctChanges").format[CtChanges](CtChanges.databaseFormat)
)(OtherServices.apply, unlift(OtherServices.unapply))
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,20 @@

package uk.gov.hmrc.agentservicesaccount.models.desiDetails

import play.api.libs.json.{Format, Json}
import play.api.libs.functional.syntax.{toFunctionalBuilderOps, unlift}
import play.api.libs.json.{Format, Json, __}
import uk.gov.hmrc.agentservicesaccount.utils.EncryptedStringUtil.fallbackStringFormat
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}

case class YourDetails(fullName: String,
telephone: String)

object YourDetails {
implicit val format: Format[YourDetails] = Json.format[YourDetails]

def databaseFormat(implicit crypto: Encrypter with Decrypter): Format[YourDetails] =
(
(__ \ "fullName").format[String](fallbackStringFormat) and
(__ \ "telephone").format[String](fallbackStringFormat)
)(YourDetails.apply, unlift(YourDetails.unapply))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Copyright 2024 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.agentservicesaccount.modules

import play.api.inject.{Binding, Module}
import play.api.{Configuration, Environment}
import uk.gov.hmrc.crypto.{Crypted, Decrypter, Encrypter, PlainBytes, PlainContent, PlainText, SymmetricCryptoFactory}

import java.nio.charset.StandardCharsets
import java.util.Base64

class CryptoProviderModule extends Module {

def aesCryptoInstance(configuration: Configuration): Encrypter with Decrypter = if (
configuration.underlying.getBoolean("fieldLevelEncryption.enable")
)
SymmetricCryptoFactory.aesCryptoFromConfig("fieldLevelEncryption", configuration.underlying)
else
NoCrypto

override def bindings(environment: Environment, configuration: Configuration): Seq[Binding[_]] =
Seq(
bind[Encrypter with Decrypter].qualifiedWith("aes").toInstance(aesCryptoInstance(configuration))
)
}

/** Encrypter/decrypter that does nothing (i.e. leaves content in plaintext). Only to be used for debugging.
*/
trait NoCrypto extends Encrypter with Decrypter {
def encrypt(plain: PlainContent): Crypted = plain match {
case PlainText(text) => Crypted(text)
case PlainBytes(bytes) => Crypted(new String(Base64.getEncoder.encode(bytes), StandardCharsets.UTF_8))
}
def decrypt(notEncrypted: Crypted): PlainText = PlainText(notEncrypted.value)
def decryptAsBytes(nullEncrypted: Crypted): PlainBytes = PlainBytes(Base64.getDecoder.decode(nullEncrypted.value))
}

object NoCrypto extends NoCrypto
Original file line number Diff line number Diff line change
Expand Up @@ -18,15 +18,19 @@ package uk.gov.hmrc.agentservicesaccount.services

import play.api.libs.json.{Reads, Writes}
import play.api.mvc.{Request, Result}
import uk.gov.hmrc.agentservicesaccount.controllers.sessionKeys
import uk.gov.hmrc.agentservicesaccount.controllers.{ARN, DESCRIPTION, DRAFT_NEW_CONTACT_DETAILS, DRAFT_SUBMITTED_BY, EMAIL, EMAIL_PENDING_VERIFICATION, NAME, PHONE, sessionKeys}
import uk.gov.hmrc.agentservicesaccount.models.desiDetails.{DesignatoryDetails, YourDetails}
import uk.gov.hmrc.agentservicesaccount.repository.SessionCacheRepository
import uk.gov.hmrc.agentservicesaccount.utils.EncryptedStringUtil
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}
import uk.gov.hmrc.mongo.cache.DataKey

import javax.inject.{Inject, Singleton}
import javax.inject.{Inject, Named, Singleton}
import scala.concurrent.{ExecutionContext, Future}

@Singleton
class SessionCacheService @Inject()(sessionCacheRepository: SessionCacheRepository) {
class SessionCacheService @Inject()(sessionCacheRepository: SessionCacheRepository)
(implicit @Named("aes") crypto: Encrypter with Decrypter) {


def withSessionItem[T](dataKey: DataKey[T])
Expand All @@ -37,16 +41,34 @@ class SessionCacheService @Inject()(sessionCacheRepository: SessionCacheReposito

def get[T](dataKey: DataKey[T])
(implicit reads: Reads[T], request: Request[_]): Future[Option[T]] = {
sessionCacheRepository.getFromSession[T](dataKey)
dataKey match {
case key: DataKey[String @unchecked] if Seq(NAME, EMAIL, PHONE, ARN, DESCRIPTION, EMAIL_PENDING_VERIFICATION).contains(key) =>
sessionCacheRepository.getFromSession(key)(EncryptedStringUtil.fallbackStringFormat, request)
case key: DataKey[DesignatoryDetails @unchecked] if key == DRAFT_NEW_CONTACT_DETAILS =>
sessionCacheRepository.getFromSession(key)(DesignatoryDetails.databaseFormat, request)
case key: DataKey[YourDetails @unchecked] if key == DRAFT_SUBMITTED_BY =>
sessionCacheRepository.getFromSession(key)(YourDetails.databaseFormat, request)
case _ =>
sessionCacheRepository.getFromSession[T](dataKey)
}
}

def put[T](dataKey: DataKey[T], value: T)
(implicit writes: Writes[T], request: Request[_]): Future[(String, String)] = {
sessionCacheRepository.putSession(dataKey, value)
dataKey match {
case key: DataKey[String @unchecked] if Seq(NAME, EMAIL, PHONE, ARN, DESCRIPTION, EMAIL_PENDING_VERIFICATION).contains(key) =>
sessionCacheRepository.putSession(key, value)(EncryptedStringUtil.fallbackStringFormat, request)
case key: DataKey[DesignatoryDetails @unchecked] if key == DRAFT_NEW_CONTACT_DETAILS =>
sessionCacheRepository.putSession(key, value)(DesignatoryDetails.databaseFormat, request)
case key: DataKey[YourDetails @unchecked] if key == DRAFT_SUBMITTED_BY =>
sessionCacheRepository.putSession(key, value)(YourDetails.databaseFormat, request)
case _ =>
sessionCacheRepository.putSession(dataKey, value)
}
}

def delete[T](dataKey: DataKey[T])
(implicit request: Request[_]): Future[Unit] = {
(implicit request: Request[_]): Future[Unit] = {
sessionCacheRepository.deleteFromSession(dataKey)
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Copyright 2024 HM Revenue & Customs
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package uk.gov.hmrc.agentservicesaccount.utils

import play.api.Logging
import play.api.libs.json.{Format, Json}
import uk.gov.hmrc.crypto.json.JsonEncryption.{stringDecrypter, stringEncrypter}
import uk.gov.hmrc.crypto.{Decrypter, Encrypter}

object EncryptedStringUtil extends Logging {
//TODO: replace all usages of this with stringDecrypterEncrypter once all old sessions have timed out
def fallbackStringFormat(implicit crypto: Encrypter with Decrypter): Format[String] =
Format(
json => try {
stringDecrypter.reads(json)
} catch {
case _: Throwable =>
logger.warn("[EncryptedStringUtil] found unencrypted string")
Json.fromJson[String](json)
},
stringEncrypter
)
}
8 changes: 8 additions & 0 deletions conf/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ play.modules.enabled += "uk.gov.hmrc.play.bootstrap.AuthModule"

play.modules.enabled += "uk.gov.hmrc.play.bootstrap.HttpClientModule"
play.modules.enabled += "uk.gov.hmrc.play.bootstrap.HttpClientV2Module"
play.modules.enabled += "uk.gov.hmrc.agentservicesaccount.modules.CryptoProviderModule"

play.modules.enabled += "uk.gov.hmrc.mongo.play.PlayMongoModule"

Expand Down Expand Up @@ -227,3 +228,10 @@ suspendedContactDetails {
}

pillar2-submission-frontend.external-url = "http://localhost:10053"


fieldLevelEncryption {
enable = true
key = "edkOOwt7uvzw1TXnFIN6aRVHkfWcgiOrbBvkEQvO65g="
previousKeys = []
}
1 change: 1 addition & 0 deletions project/AppDependencies.scala
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ object AppDependencies {
"com.beachape" %% "enumeratum-play" % enumeratumVersion,
"org.julienrf" %% "play-json-derived-codecs" % "11.0.0",
"org.apache.commons" % "commons-text" % "1.12.0",
"uk.gov.hmrc" %% "crypto-json-play-30" % "8.0.0"
)

val test: Seq[ModuleID] = Seq(
Expand Down
Loading

0 comments on commit 7cd12f9

Please sign in to comment.