mirror of
https://github.com/digital-asset/daml.git
synced 2024-09-20 01:07:18 +03:00
Encrypt credentials (#6296)
* Encrypt credentials changelog_begin changelog_end * Move key aquisition and write a big fat warning * Only write encrypted token to DB.
This commit is contained in:
parent
ad16f563b4
commit
07f24c0d93
@ -38,7 +38,6 @@ import com.daml.lf.engine.trigger.Response._
|
||||
import com.daml.daml_lf_dev.DamlLf
|
||||
import com.daml.grpc.adapter.{AkkaExecutionSequencerPool, ExecutionSequencerFactory}
|
||||
import com.daml.platform.services.time.TimeProviderType
|
||||
import com.daml.ledger.api.refinements.ApiTypes.Party
|
||||
import scalaz.syntax.traverse._
|
||||
|
||||
import scala.concurrent.{Await, ExecutionContext, Future}
|
||||
@ -58,7 +57,7 @@ case class LedgerConfig(
|
||||
commandTtl: Duration,
|
||||
)
|
||||
|
||||
final case class UserCredentials(token: String)
|
||||
final case class UserCredentials(token: EncryptedToken)
|
||||
|
||||
final case class RunningTrigger(
|
||||
triggerInstance: UUID,
|
||||
@ -194,6 +193,18 @@ object Server {
|
||||
jdbcConfig: Option[JdbcConfig],
|
||||
): Behavior[Message] = Behaviors.setup { ctx =>
|
||||
val triggerDao = jdbcConfig.map(TriggerDao(_)(ctx.system.executionContext))
|
||||
|
||||
val key =
|
||||
sys.env.get("TRIGGER_SERVICE_SECRET_KEY") match {
|
||||
case Some(key) => key
|
||||
case None => {
|
||||
val logMsg =
|
||||
"WARNING : The environment variable 'TRIGGER_SERVICE_SECRET_KEY' is not defined. It is highly recommended that a non-empty value for this variable be set. If the service startup parameters do not include the '--no-secret-key' option, the service will now terminate."
|
||||
ctx.log.info(logMsg)
|
||||
"secret key"
|
||||
}
|
||||
}
|
||||
|
||||
val server = new Server(dar, triggerDao)
|
||||
|
||||
// http doesn't know about akka typed so provide untyped system
|
||||
@ -215,7 +226,7 @@ object Server {
|
||||
triggerName: Identifier): Either[String, JsValue] = {
|
||||
for {
|
||||
trigger <- Trigger.fromIdentifier(server.compiledPackages, triggerName).right
|
||||
party = Party(TokenManagement.decodeCredentials(credentials)._1);
|
||||
party = TokenManagement.decodeCredentials(key, credentials)._1
|
||||
triggerInstance = UUID.randomUUID
|
||||
_ = ctx.spawn(
|
||||
TriggerRunner(
|
||||
@ -277,10 +288,9 @@ object Server {
|
||||
entity(as[StartParams]) {
|
||||
params =>
|
||||
TokenManagement
|
||||
.findCredentials(request)
|
||||
.findCredentials(key, request)
|
||||
.fold(
|
||||
unauthorized =>
|
||||
complete(errorResponse(StatusCodes.Unauthorized, unauthorized.message)),
|
||||
message => complete(errorResponse(StatusCodes.Unauthorized, message)),
|
||||
credentials =>
|
||||
startTrigger(credentials, params.triggerName) match {
|
||||
case Left(err) =>
|
||||
@ -335,10 +345,9 @@ object Server {
|
||||
extractRequest {
|
||||
request =>
|
||||
TokenManagement
|
||||
.findCredentials(request)
|
||||
.findCredentials(key, request)
|
||||
.fold(
|
||||
unauthorized =>
|
||||
complete(errorResponse(StatusCodes.Unauthorized, unauthorized.message)),
|
||||
message => complete(errorResponse(StatusCodes.Unauthorized, message)),
|
||||
credentials =>
|
||||
listTriggers(credentials) match {
|
||||
case Left(err) =>
|
||||
@ -361,10 +370,9 @@ object Server {
|
||||
extractRequest {
|
||||
request =>
|
||||
TokenManagement
|
||||
.findCredentials(request)
|
||||
.findCredentials(key, request)
|
||||
.fold(
|
||||
unauthorized =>
|
||||
complete(errorResponse(StatusCodes.Unauthorized, unauthorized.message)),
|
||||
message => complete(errorResponse(StatusCodes.Unauthorized, message)),
|
||||
credentials =>
|
||||
stopTrigger(uuid, credentials) match {
|
||||
case Left(err) =>
|
||||
|
@ -3,42 +3,94 @@
|
||||
|
||||
package com.daml.lf.engine.trigger
|
||||
|
||||
import com.daml.lf.data.Ref.Party
|
||||
import akka.http.scaladsl.model.headers.{Authorization, BasicHttpCredentials}
|
||||
import akka.http.scaladsl.model.HttpRequest
|
||||
|
||||
import scalaz.syntax.std.option._
|
||||
import scalaz.{\/}
|
||||
|
||||
import java.nio.charset.StandardCharsets
|
||||
import java.util
|
||||
import java.security.MessageDigest
|
||||
import javax.crypto.Cipher
|
||||
import javax.crypto.spec.SecretKeySpec
|
||||
|
||||
case class Unauthorized(message: String) extends Error(message)
|
||||
case class EncryptedToken(token: String)
|
||||
case class UnencryptedToken(token: String)
|
||||
|
||||
object TokenManagement {
|
||||
|
||||
// Utility to get the username and password out of a basic auth
|
||||
// token. By construction we ensure that there will always be two
|
||||
// components (see 'findCredentials'). We use the first component to
|
||||
// identify parties.
|
||||
def decodeCredentials(credentials: UserCredentials): (String, String) = {
|
||||
val token = credentials.token
|
||||
val bytes = java.util.Base64.getDecoder.decode(token.getBytes())
|
||||
val components = new String(bytes, StandardCharsets.UTF_8).split(":")
|
||||
(components(0), components(1))
|
||||
// TL;DR You can store the SALT in plaintext without any form of
|
||||
// obfuscation or encryption, but don't just give it out to anyone
|
||||
// that wants it.
|
||||
private val SALT = "jMhKlOuJnM34G6NHkqo9V010GhLAqOpF0BePojHgh1HgNg8^72k"
|
||||
|
||||
// Given 'key', use 'SALT' to produce an AES (Advanced Encryption
|
||||
// Standard) secret key specification. This utility is called from
|
||||
// the 'encrypt' and 'decrypt' functions.
|
||||
private def keyToSpec(key: String): SecretKeySpec = {
|
||||
var keyBytes: Array[Byte] = (SALT + key).getBytes("UTF-8")
|
||||
val sha: MessageDigest = MessageDigest.getInstance("SHA-1")
|
||||
keyBytes = sha.digest(keyBytes)
|
||||
keyBytes = util.Arrays.copyOf(keyBytes, 16)
|
||||
new SecretKeySpec(keyBytes, "AES")
|
||||
}
|
||||
|
||||
/*
|
||||
User : alice
|
||||
Password : &alC2l3SDS*V
|
||||
curl -X GET localhost:8080/hello -H "Authorization: Basic YWxpY2U6JmFsQzJsM1NEUypW"
|
||||
*/
|
||||
def findCredentials(req: HttpRequest): Unauthorized \/ UserCredentials = {
|
||||
// AES encrypt 'value' given 'key'. Proceed by first encrypting the
|
||||
// value and then base64 encode the result (the resulting string
|
||||
// consists of characters strictly in the set [a-z], [A-Z], [0-9] +
|
||||
// and /.
|
||||
private def encrypt(key: String, value: UnencryptedToken): EncryptedToken = {
|
||||
val cipher: Cipher = Cipher.getInstance("AES/ECB/PKCS5Padding")
|
||||
cipher.init(Cipher.ENCRYPT_MODE, keyToSpec(key))
|
||||
val bytes = java.util.Base64.getEncoder
|
||||
.encode(cipher.doFinal(value.token.getBytes("UTF-8")))
|
||||
EncryptedToken(new String(bytes, StandardCharsets.UTF_8))
|
||||
}
|
||||
|
||||
// AES decrypt 'value' given 'key'. Proceed by first decoding from
|
||||
// base64 then decrypt the result.
|
||||
private def decrypt(key: String, value: EncryptedToken): UnencryptedToken = {
|
||||
val cipher: Cipher = Cipher.getInstance("AES/ECB/PKCS5PADDING")
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyToSpec(key))
|
||||
UnencryptedToken(
|
||||
new String(
|
||||
cipher.doFinal(java.util.Base64.getDecoder.decode(value.token)),
|
||||
StandardCharsets.UTF_8))
|
||||
}
|
||||
|
||||
// Utility to get the username and password out of a basic auth
|
||||
// token. By construction we ensure that there will always be two
|
||||
// components and that the first component is a syntactically valid
|
||||
// party identifier (see 'findCredentials').
|
||||
def decodeCredentials(
|
||||
key: String,
|
||||
credentials: UserCredentials): (com.daml.ledger.api.refinements.ApiTypes.Party, String) = {
|
||||
val components = decrypt(key, credentials.token).token.split(":")
|
||||
(com.daml.ledger.api.refinements.ApiTypes.Party(components(0)), components(1))
|
||||
}
|
||||
|
||||
// Parse the user credentials out of a request's headers.
|
||||
def findCredentials(key: String, req: HttpRequest): Either[String, UserCredentials] = {
|
||||
req.headers
|
||||
.collectFirst {
|
||||
case Authorization(c @ BasicHttpCredentials(username, password)) => {
|
||||
UserCredentials(c.token())
|
||||
val token = c.token()
|
||||
val bytes = java.util.Base64.getDecoder.decode(token.getBytes())
|
||||
UserCredentials(encrypt(key, UnencryptedToken(new String(bytes, StandardCharsets.UTF_8))))
|
||||
}
|
||||
}
|
||||
.toRightDisjunction(Unauthorized("missing Authorization header with Basic Token"))
|
||||
} match {
|
||||
// Check the given username conforms to the syntactic
|
||||
// requirements of a party identifier.
|
||||
case Some(credentials) =>
|
||||
decodeCredentials(key, credentials) match {
|
||||
case (party, _) =>
|
||||
val ident = party.toString()
|
||||
if (Party.fromString(ident).isRight) {
|
||||
Right(credentials)
|
||||
} else {
|
||||
Left("invalid party identifier '" + ident + "'")
|
||||
}
|
||||
}
|
||||
case None => Left("missing Authorization header with Basic Token")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -81,7 +81,9 @@ object TriggerDao {
|
||||
}
|
||||
|
||||
def addRunningTrigger(t: RunningTrigger): ConnectionIO[Unit] = {
|
||||
val partyToken = t.credentials.token
|
||||
val partyToken: String = t.credentials match {
|
||||
case UserCredentials(EncryptedToken(token)) => token
|
||||
}
|
||||
val fullTriggerName = t.triggerName.toString
|
||||
val insertTrigger: Fragment = Fragment.const(
|
||||
s"insert into running_triggers values ('${t.triggerInstance}', '$partyToken', '$fullTriggerName')"
|
||||
@ -100,7 +102,9 @@ object TriggerDao {
|
||||
}
|
||||
|
||||
def getTriggersForParty(credentials: UserCredentials): ConnectionIO[Vector[UUID]] = {
|
||||
val partyToken = credentials.token
|
||||
val partyToken: String = credentials match {
|
||||
case UserCredentials(EncryptedToken(token)) => token
|
||||
}
|
||||
val select = Fragment.const("select trigger_instance from running_triggers")
|
||||
val where = Fragment.const(s" where party_token = '${partyToken}'")
|
||||
val order = Fragment.const(" order by running_triggers")
|
||||
|
@ -460,7 +460,20 @@ class ServiceTest extends AsyncFlatSpec with Eventually with Matchers with Postg
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
it should "give a 'not found' response for a stop request with unparseable UUID" in withTriggerServiceAndDb(
|
||||
it should "give an 'unauthorized' response for a start request with an invalid party identifier" in withTriggerServiceAndDb(
|
||||
Some(dar)) { (uri: Uri, client: LedgerClient, ledgerProxy: Proxy) =>
|
||||
for {
|
||||
resp <- startTrigger(uri, s"$testPkgId:TestTrigger:trigger", User("Alice-!", "&alC2l3SDS*V"))
|
||||
_ <- resp.status should equal(StatusCodes.Unauthorized)
|
||||
body <- responseBodyToString(resp)
|
||||
JsObject(fields) = body.parseJson
|
||||
_ <- fields.get("status") should equal(Some(JsNumber(StatusCodes.Unauthorized.intValue)))
|
||||
_ <- fields.get("errors") should equal(
|
||||
Some(JsArray(JsString("invalid party identifier 'Alice-!'"))))
|
||||
} yield succeed
|
||||
}
|
||||
|
||||
it should "give a 'not found' response for a stop request with an unparseable UUID" in withTriggerServiceAndDb(
|
||||
None) { (uri: Uri, client: LedgerClient, ledgerProxy: Proxy) =>
|
||||
val uuid: String = "No More Mr Nice Guy"
|
||||
val req = HttpRequest(
|
||||
|
Loading…
Reference in New Issue
Block a user