diff --git a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala index 0f58d3b35..3dcb996b1 100644 --- a/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala +++ b/apps/dashboard-app/src/main/scala/net/shrine/dashboard/jwtauth/ShrineJwtAuthenticator.scala @@ -1,202 +1,202 @@ package net.shrine.dashboard.jwtauth import java.io.ByteArrayInputStream import java.security.{Principal, Key, PrivateKey} import java.security.cert.{CertificateFactory, X509Certificate} import java.util.Date import io.jsonwebtoken.impl.TextCodec import io.jsonwebtoken.{Jws, ClaimJwtException, Claims, SignatureAlgorithm, Jwts} import net.shrine.crypto.{KeyStoreDescriptorParser, KeyStoreCertCollection} import net.shrine.dashboard.DashboardConfigSource import net.shrine.i2b2.protocol.pm.User import net.shrine.log.Loggable import net.shrine.protocol.Credential import spray.http.HttpHeaders.{Authorization, `WWW-Authenticate`} import spray.http.{HttpRequest, OAuth2BearerToken, HttpHeader, HttpChallenge} import spray.routing.AuthenticationFailedRejection.{CredentialsMissing, CredentialsRejected} import spray.routing.AuthenticationFailedRejection import spray.routing.authentication._ import scala.concurrent.{Future, ExecutionContext} import scala.util.{Success, Failure, Try} /** * An Authenticator that uses Jwt in a Bearer header to authenticate. See http://jwt.io/introduction/ for what this is all about, * https://tools.ietf.org/html/rfc7519 for what it might include for claims. * * @author david * @since 12/21/15 */ object ShrineJwtAuthenticator extends Loggable { val config = DashboardConfigSource.config val certCollection: KeyStoreCertCollection = KeyStoreCertCollection.fromFileRecoverWithClassPath(KeyStoreDescriptorParser(config.getConfig("shrine.keystore"))) //from https://groups.google.com/forum/#!topic/spray-user/5DBEZUXbjtw def authenticate(implicit ec: ExecutionContext): ContextAuthenticator[User] = { ctx => Future { val attempt: Try[Authentication[User]] = for { header:HttpHeader <- extractAuthorizationHeader(ctx.request) jwtsString:String <- extractJwtsStringAndCheckScheme(header) jwtsClaims <- extractJwtsClaims(jwtsString) cert: X509Certificate <- extractAndCheckCert(jwtsClaims) jwtsBody:Claims <- Try{jwtsClaims.getBody} jwtsSubject <- failIfNull(jwtsBody.getSubject,MissingRequiredJwtsClaim("subject",cert.getSubjectDN)) jwtsIssuer <- failIfNull(jwtsBody.getSubject,MissingRequiredJwtsClaim("issuer",cert.getSubjectDN)) } yield { val user = User( fullName = cert.getSubjectDN.getName, username = jwtsSubject, domain = jwtsIssuer, credential = Credential(jwtsIssuer, isToken = false), params = Map(), rolesByProject = Map() ) Right(user) } //todo use a fold() in Scala 2.12 attempt match { case Success(rightUser) => rightUser case Failure(x) => x match { case anticipated: ShrineJwtException => info(s"Failed to authenticate due to ${anticipated.toString}",anticipated) anticipated.rejection case fromJwts: ClaimJwtException => info(s"Failed to authenticate due to ${fromJwts.toString} while authenticating ${ctx.request}",fromJwts) rejectedCredentials /* case x: CertificateExpiredException => { //todo will these even be thrown here? Get some identification here info(s"Cert expired.", x) rejectedCredentials } case x: CertificateNotYetValidException => { info(s"Cert not yet valid.", x) rejectedCredentials } */ case unanticipated => warn(s"Unanticipated ${unanticipated.toString} while authenticating ${ctx.request}",unanticipated) rejectedCredentials } } } } def createOAuthCredentials(user:User): OAuth2BearerToken = { - val base64Cert = new String(TextCodec.BASE64URL.encode(certCollection.myCert.get.getEncoded)) + val base64Cert:String = TextCodec.BASE64URL.encode(certCollection.myCert.get.getEncoded) val key: PrivateKey = certCollection.myKeyPair.privateKey val expiration: Date = new Date(System.currentTimeMillis() + 30 * 1000) //good for 30 seconds val jwtsString = Jwts.builder(). setHeaderParam("kid", base64Cert). setSubject(s"${user.username} at ${user.domain}"). setIssuer(java.net.InetAddress.getLocalHost.getHostName). //todo is it OK for me to use issuer this way or should I use my own claim? setExpiration(expiration). signWith(SignatureAlgorithm.RS512, key). compact() OAuth2BearerToken(jwtsString) } def extractAuthorizationHeader(request: HttpRequest):Try[HttpHeader] = Try { case class NoAuthorizationHeaderException(request: HttpRequest) extends ShrineJwtException(s"No ${Authorization.name} header found in $request",missingCredentials) //noinspection ComparingUnrelatedTypes request.headers.find(_.name.equals(Authorization.name)).getOrElse{throw NoAuthorizationHeaderException(request)} } def extractJwtsStringAndCheckScheme(httpHeader: HttpHeader) = Try { val splitHeaderValue: Array[String] = httpHeader.value.trim.split(" ") if (splitHeaderValue.length != 2) { case class WrongNumberOfSegmentsException(httpHeader: HttpHeader) extends ShrineJwtException(s"Header had ${splitHeaderValue.length} space-delimited segments, not 2, in $httpHeader.",missingCredentials) throw new WrongNumberOfSegmentsException(httpHeader) } else if(splitHeaderValue(0) != BearerAuthScheme) { case class NotBearerAuthException(httpHeader: HttpHeader) extends ShrineJwtException(s"Expected $BearerAuthScheme, not ${splitHeaderValue(0)} in $httpHeader.",missingCredentials) throw new NotBearerAuthException(httpHeader) } else splitHeaderValue(1) } def extractJwtsClaims(jwtsString:String): Try[Jws[Claims]] = Try { Jwts.parser().setSigningKeyResolver(new SigningKeyResolverBridge()).parseClaimsJws(jwtsString) } def extractAndCheckCert(jwtsClaims:Jws[Claims]): Try[X509Certificate] = Try { val cert = KeySource.certForString(jwtsClaims.getHeader.getKeyId) val issuingSite = jwtsClaims.getBody.getIssuer //todo is this the right way to find a cert in the certCollection? debug(s"certCollection.caCerts.contains(${cert.getSubjectX500Principal}) is ${certCollection.caCerts.contains(cert.getSubjectX500Principal)}") certCollection.caCerts.get(cert.getSubjectX500Principal).fold{ //if not in the keystore, check that the issuer is available val issuer: Principal = cert.getIssuerX500Principal case class CertIssuerNotInCollectionException(issuingSite:String,issuer: Principal) extends ShrineJwtException(s"Could not find a CA certificate with issuer DN $issuer. Known CA cert aliases are ${certCollection.caCertAliases.mkString(",")}") val signingCert = certCollection.caCerts.getOrElse(issuer,{throw CertIssuerNotInCollectionException(issuingSite,issuer)}) //verify that the cert was signed using the signingCert //todo this can throw CertificateException, NoSuchAlgorithmException, InvalidKeyException, NoSuchProviderException, SignatureException cert.verify(signingCert.getPublicKey) //todo has cert expired? info(s"${cert.getSubjectX500Principal} verified using $issuer from the KeyStore") cert }{ principal => //if the cert is in the certCollection then all is well info(s"$principal is in the KeyStore") cert } } def failIfNull[E](e:E,t:Throwable):Try[E] = Try { if(e == null) throw t else e } case class MissingRequiredJwtsClaim(field:String,principal: Principal) extends ShrineJwtException(s"$field is null from ${principal.getName}") val BearerAuthScheme = "Bearer" val challengeHeader: `WWW-Authenticate` = `WWW-Authenticate`(HttpChallenge(BearerAuthScheme, "dashboard-to-dashboard")) val missingCredentials: Authentication[User] = Left(AuthenticationFailedRejection(CredentialsMissing, List(challengeHeader))) val rejectedCredentials: Authentication[User] = Left(AuthenticationFailedRejection(CredentialsRejected, List(challengeHeader))) } class KeySource {} object KeySource extends Loggable { def keyForString(string: String): Key = { val certificate =certForString(string) //todo validate cert with something like obtainAndValidateSigningCert //check date on cert vs time. throws CertificateExpiredException or CertificateNotYetValidException for problems //todo skip this until you rebuild the certs used for testing certificate.checkValidity(now) certificate.getPublicKey } def certForString(string: String): X509Certificate = { val certBytes = TextCodec.BASE64URL.decode(string) val inputStream = new ByteArrayInputStream(certBytes) val certificate = try { CertificateFactory.getInstance("X.509").generateCertificate(inputStream).asInstanceOf[X509Certificate] } finally { inputStream.close() } certificate } } abstract class ShrineJwtException(message:String, val rejection:Authentication[User] = ShrineJwtAuthenticator.rejectedCredentials, cause:Throwable = null) extends RuntimeException(message,cause) \ No newline at end of file diff --git a/commons/crypto/pom.xml b/commons/crypto/pom.xml index 651056024..db4a74ca9 100644 --- a/commons/crypto/pom.xml +++ b/commons/crypto/pom.xml @@ -1,52 +1,64 @@ 4.0.0 SHRINE Crypto shrine-crypto jar net.shrine shrine-base 1.22.2.0-SNAPSHOT ../../pom.xml net.shrine shrine-test-commons ${project.version} test-jar test net.shrine shrine-protocol ${project.version} net.shrine shrine-config ${project.version} + + + org.bouncycastle + bcprov-jdk15on + ${bouncy-castle-version} + + + + org.cryptacular + cryptacular + ${cryptacular-version} + src/main/scala src/test/scala net.alchim31.maven scala-maven-plugin org.apache.maven.plugins maven-jar-plugin test-jar diff --git a/commons/crypto/src/main/scala/net/shrine/crypto/DefaultSignerVerifier.scala b/commons/crypto/src/main/scala/net/shrine/crypto/DefaultSignerVerifier.scala index 33fe4aac0..f4f168b6e 100644 --- a/commons/crypto/src/main/scala/net/shrine/crypto/DefaultSignerVerifier.scala +++ b/commons/crypto/src/main/scala/net/shrine/crypto/DefaultSignerVerifier.scala @@ -1,179 +1,172 @@ package net.shrine.crypto -import java.security.PrivateKey -import java.security.{ Signature => JSig } -import java.security.cert.Certificate +import java.security.{ Signature => JSig, PrivateKey, PublicKey } +import java.security.cert.{Certificate, X509Certificate} import javax.xml.datatype.XMLGregorianCalendar + +import net.shrine.protocol.{BroadcastMessage, CertId, Signature, CertData} import net.shrine.log.Loggable -import net.shrine.protocol.BroadcastMessage -import net.shrine.protocol.CertId -import net.shrine.protocol.Signature import net.shrine.util.{Tries, XmlDateHelper, XmlGcEnrichments} + import scala.concurrent.duration.Duration -import net.shrine.protocol.CertData -import scala.util.Success -import scala.util.Failure -import java.security.cert.X509Certificate -import scala.util.Try -import java.security.PublicKey +import scala.util.{Success, Failure, Try} /** * @author clint * @since Nov 25, 2013 */ final class DefaultSignerVerifier(certCollection: CertCollection) extends Signer with Verifier with Loggable { logStartup() import DefaultSignerVerifier._ import SigningCertStrategy._ override def sign(message: BroadcastMessage, signingCertStrategy: SigningCertStrategy): BroadcastMessage = { val signatureOption = for { signedBy <- certCollection.myCertId KeyPair(_, myPrivateKey) = certCollection.myKeyPair myCert <- certCollection.myCert } yield { val timestamp = XmlDateHelper.now //TODO: Find way to attach only the public part of the signing cert, plus its signatures //TODO: Currently the whole signing cert is attached (right?) val signingCert = { if (signingCertStrategy.attachSigningCert) { Some(CertData(myCert)) } else { None } } Signature(timestamp, signedBy, signingCert, DefaultSignerVerifier.sign(myPrivateKey, toBytes(message, timestamp))) } val sig = signatureOption.getOrElse(throw new Exception(s"Can't sign, no private keys. Known ids: ${certCollection.ids}")) message.withSignature(sig) } import Tries.toTry private[crypto] def isSignedByTrustedCA(attachedCert: X509Certificate): Try[Boolean] = { for { attachedCertSignerCert <- toTry(certCollection.caCerts.get(CertCollection.getIssuer(attachedCert)))(new Exception(s"Couldn't find CA certificate with issuer DN '${attachedCert.getIssuerDN}'; known CA cert aliases: ${certCollection.caCertAliases.mkString(",")}")) caPublicKey = attachedCertSignerCert.getPublicKey } yield { attachedCert.isSignedBy(caPublicKey) } } private[crypto] def obtainAndValidateSigningCert(signature: Signature): Try[X509Certificate] = { signature.signingCert match { //If the signing cert was sent with the signature, and the signing cert was signed by a CA we trust - case Some(signingCertData) => { + case Some(signingCertData) => for { signingCert <- Try(signingCertData.toCertificate) signedByTrustedCA <- isSignedByTrustedCA(signingCert) } yield { if (signedByTrustedCA) { signingCert } else { throw new Exception(s"Couldn't verify: signing cert with serial '${signingCert.getSerialNumber}' was part of the signature, but was not signed by any CA we trust. Aliases of trusted CAs are: ${certCollection.caCertAliases.map(s => s"'$s'").mkString(",")}") } } - } + //Otherwise, look up the signing cert in our keystore by its CertId - case None => { + case None => val signerCertOption = certCollection.get(signature.signedBy) toTry(signerCertOption)(new Exception(s"Couldn't verify: can't find signer key with CertId ${signature.signedBy}")) - } } } override def verifySig(message: BroadcastMessage, maxSignatureAge: Duration): Boolean = { def notTooOld(sig: Signature): Boolean = { import scala.concurrent.duration._ import XmlGcEnrichments._ val sigValidityEndTime = sig.timestamp + maxSignatureAge sigValidityEndTime > XmlDateHelper.now } message.signature match { case None => false case Some(signature) => { val signerCertAttempt: Try[X509Certificate] = obtainAndValidateSigningCert(signature) val verificationAttempt = for { signerCert <- signerCertAttempt if notTooOld(signature) } yield { DefaultSignerVerifier.verify(signerCert, toBytes(message, signature.timestamp), signature.value.array) } verificationAttempt match { case Success(result) => result case Failure(reason) => { warn(s"Error verifying signature for message with id '${message.requestId}': ", reason) false } } } } } private def logStartup() { debug(s"DefaultSignerVerifier using cert collection: ") debug(s"Private key id: ${certCollection.myCertId}") debug(s"Known certs: ") def certIdToMessage(certId: CertId): String = s" ${certId.serial} with name '${certId.name.getOrElse("")}'" certCollection.ids.foreach { certId => debug(certIdToMessage(certId)) } debug(s"Known CA certs: ") certCollection.caCerts.values.map(KeyStoreCertCollection.toCertId).foreach { certId => debug(certIdToMessage(certId)) } } } object DefaultSignerVerifier { val signatureAlgorithm = "SHA256withRSA" private implicit final class HasBooleanSignedBy(val cert: X509Certificate) extends AnyVal { def isSignedBy(caPubKey: PublicKey): Boolean = Try { cert.verify(caPubKey); true }.getOrElse(false) } private def toBytes(message: BroadcastMessage, timestamp: XMLGregorianCalendar): Array[Byte] = { val messageXml = message.copy(signature = None).toXmlString val timestampXml = timestamp.toXMLFormat (messageXml + timestampXml).getBytes("UTF-8") } private[crypto] def sign(signingKey: PrivateKey, bytes: Array[Byte]): Array[Byte] = { val signerVerifier = getSignerVerifier signerVerifier.initSign(signingKey) signerVerifier.update(bytes) signerVerifier.sign } private[crypto] def verify(signerCert: Certificate, signedBytes: Array[Byte], signatureBytes: Array[Byte]): Boolean = { val signerVerifier = getSignerVerifier signerVerifier.initVerify(signerCert) signerVerifier.update(signedBytes) signerVerifier.verify(signatureBytes) } private def getSignerVerifier: JSig = JSig.getInstance(signatureAlgorithm) } \ No newline at end of file diff --git a/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreCertCollection.scala b/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreCertCollection.scala index 2b703ad9a..37a793a9a 100644 --- a/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreCertCollection.scala +++ b/commons/crypto/src/main/scala/net/shrine/crypto/KeyStoreCertCollection.scala @@ -1,158 +1,156 @@ package net.shrine.crypto import java.io.{IOException, FileInputStream, InputStream, File} -import java.security.KeyStore -import java.security.PrivateKey +import java.security.{KeyStore, PrivateKey, Key, Principal} import java.security.cert.X509Certificate import javax.naming.ldap.{Rdn, LdapName} + import net.shrine.log.Loggable +import net.shrine.protocol.CertId import scala.collection.JavaConverters.enumerationAsScalaIteratorConverter -import net.shrine.protocol.CertId -import java.security.Key -import java.security.Principal /** * @author clint * @since Nov 22, 2013 */ final case class KeyStoreCertCollection(keystore: KeyStore, descriptor: KeyStoreDescriptor) extends CertCollection with Loggable { override def size: Int = keystore.size override def get(id: CertId): Option[X509Certificate] = certsById.get(id) override def iterator: Iterator[X509Certificate] = certsById.valuesIterator ++ caCerts.valuesIterator override def ids: Iterable[CertId] = certsById.keys import KeyStoreCertCollection.toCertId override lazy val caIds: Iterable[CertId] = caCerts.values.map(toCertId) override def caCertAliases: Seq[String] = descriptor.caCertAliases override lazy val caCerts: Map[Principal, X509Certificate] = { caCertAliases.flatMap(getX509Cert).map(cert => CertCollection.getIssuer(cert) -> cert).toMap } override lazy val myCert: Option[X509Certificate] = descriptor.privateKeyAlias.flatMap(getX509Cert) override lazy val myCertId: Option[CertId] = myCert.map(toCertId) lazy val myCommonName:Option[String] = { myCert.flatMap{cert:X509Certificate => KeyStoreCertCollection.extractCommonName(cert) } } override val myKeyPair: KeyPair = { val privateKeyAlias: String = descriptor.privateKeyAlias match { case Some(alias) => if(isPrivateKey(alias)) { alias } else throw new Exception(s"No key, or no private key component, at alias '$alias'") case _ => val privateKeyAliases = keystore.aliases.asScala.filter(isPrivateKey).toIndexedSeq privateKeyAliases.size match { case 1 => val alias = privateKeyAliases.head info(s"Found one cert with a private key, with alias '$alias'") alias case 0 => throw new Exception(s"No aliases point to certs with private keys. Known aliases are: $privateKeyAliases") case n => throw new Exception(s"$n aliases point to certs with private keys: $privateKeyAliases; specify the private key to use with the privateKeyAlias option") } } val keyPairOption = for { cert <- getX509Cert(privateKeyAlias) privateKey <- getPrivateKey(privateKeyAlias) } yield KeyPair(cert.getPublicKey, privateKey) require(keyPairOption.isDefined, "Private key alias must be defined, and identify a cert with a private key component, or exactly one cert with a private key component must be present in the keystore") keyPairOption.get } private def getKey(alias: String): Option[Key] = { Option(keystore.getKey(alias, descriptor.password.toCharArray)) } private def isPrivateKey(alias: String): Boolean = { getKey(alias).exists(_.isInstanceOf[PrivateKey]) } private def getPrivateKey(alias: String): Option[PrivateKey] = { getKey(alias).collect { case pk: PrivateKey => pk } } private lazy val certsById: Map[CertId, X509Certificate] = { import scala.collection.JavaConverters._ val nonCaAliases = keystore.aliases.asScala.toSet -- caCertAliases val certs = nonCaAliases.toSeq.flatMap(getX509Cert) certs.map(cert => (toCertId(cert), cert)).toMap } private[crypto] def getX509Cert(alias: String): Option[X509Certificate] = { Option(keystore.getCertificate(alias).asInstanceOf[X509Certificate]) } } object KeyStoreCertCollection extends Loggable { /** Try the file system if a keystore file exists, else try the classpath*/ def fromFileRecoverWithClassPath(descriptor: KeyStoreDescriptor): KeyStoreCertCollection = { if(new File(descriptor.file).exists) fromFile(descriptor) else fromClassPathResource(descriptor) } def fromFile(descriptor: KeyStoreDescriptor): KeyStoreCertCollection = { require(new File(descriptor.file).exists,s"Keystore file '${descriptor.file}' exists? ${new File(descriptor.file).exists}") fromStream(descriptor, new FileInputStream(_)) } def fromClassPathResource(descriptor: KeyStoreDescriptor): KeyStoreCertCollection = { fromStream(descriptor, getClass.getClassLoader.getResourceAsStream) } def fromStream(descriptor: KeyStoreDescriptor, streamFrom: String => InputStream): KeyStoreCertCollection = { def toString(descriptor: KeyStoreDescriptor) = descriptor.copy(password = "********").toString debug(s"Loading keystore using descriptor: ${toString(descriptor)}") val stream = streamFrom(descriptor.file) require(stream != null,s"null stream for descriptor ${toString(descriptor)}") val keystore = KeyStore.getInstance(descriptor.keyStoreType.name) try { keystore.load(stream, descriptor.password.toCharArray) } catch {case x:IOException => throw new IOException(s"Unable to load keystore from $descriptor",x)} import scala.collection.JavaConverters._ debug(s"Keystore aliases: ${keystore.aliases.asScala.mkString(",")}") debug(s"Keystore ${toString(descriptor)} loaded successfully") KeyStoreCertCollection(keystore, descriptor) } private[crypto] def toCertId(cert: X509Certificate): CertId = { //TODO: Is getSubjectDN right for a human-readable name? CertId(cert.getSerialNumber, Option(cert.getSubjectDN.getName)) } def extractCommonName(cert:X509Certificate):Option[String] = { val ldapDn = new LdapName(cert.getSubjectX500Principal.getName) import collection.JavaConverters._ val rdns: Array[Rdn] = ldapDn.getRdns.asScala.toArray rdns.collectFirst{case rdn:Rdn if rdn.getType == "CN" => rdn.getValue.toString} } } \ No newline at end of file diff --git a/pom.xml b/pom.xml index 0c584257c..c9c1d9416 100644 --- a/pom.xml +++ b/pom.xml @@ -1,376 +1,389 @@ 4.0.0 SHRINE net.shrine shrine-base pom 1.22.2.0-SNAPSHOT UTF-8 4.2.5.RELEASE 2.6.2 2.11.8 2.11 4.12 1.7.18 1.2.17 1.19 2.2.6 3.2.2 0.9.6-RC4 1.2.1 1.4.191 3.4 5.1.38 2.3 3.0.1 0.9.5 1.3.3 2.3.14 2.4.0 3.3.0 3.1.1 3.1.1.1 + 1.55 + 1.2.0 apps/dashboard-app apps/dashboard-war apps/steward-app apps/steward-war apps/proxy apps/shrine-app apps/war qep/service hub/broadcaster-aggregator hub/broadcaster-service adapter/adapter-api adapter/adapter-service hms-support tools commons/util commons/auth commons/protocol-query commons/data-commons commons/protocol commons/crypto commons/client commons/config commons/ont-support commons/test-commons install integration shrine-webclient net.alchim31.maven scala-maven-plugin ${scala-maven-plugin-version} compile compile compile test-compile testCompile test-compile process-resources compile incremental true -XX:+AggressiveOpts -XX:CompileThreshold=500 -XX:+UseFastAccessorMethods -XX:+UseStringCache -XX:+OptimizeStringConcat -XX:+TieredCompilation -XX:+UseConcMarkSweepGC -XX:+DoEscapeAnalysis -server -Xms64m -Xmx1024m -XX:MaxPermSize=384m ${scala-version} -Xcheckinit -unchecked -deprecation -Xlint:adapted-args,inaccessible,infer-any,missing-interpolator,private-shadow,type-parameter-shadow,unsound-match 7 7 maven-compiler-plugin 7 7 org.codehaus.mojo buildnumber-maven-plugin 1.1 org.apache.maven.plugins maven-jar-plugin 2.4 org.apache.maven.plugins maven-war-plugin 2.1.1 org.codehaus.mojo buildnumber-maven-plugin validate create {0,date,yyyy-MM-dd HH:mm:ss} (not available) org.apache.maven.plugins maven-jar-plugin true ${buildNumber} ${scmBranch} ${timestamp} org.apache.maven.plugins maven-war-plugin true ${buildNumber} ${scmBranch} ${timestamp} org.apache.tomcat.maven tomcat7-maven-plugin 2.2 true true true true http://shrine-dev1.catalyst:6060/shrine/rest/happy scm:git:https://open.med.harvard.edu/stash/scm/shrine/shrine.git CBMI-Nexus https://repo.open.med.harvard.edu/nexus/content/groups/public/ com.typesafe config ${typesafe-config-version} log4j log4j ${log4j-version} org.springframework spring-jdbc ${spring.version} test com.h2database h2 ${h2-version} test org.easymock easymock ${easymock-version} test org.slf4j slf4j-log4j12 ${slf4j-version} test mysql mysql-connector-java ${mysql-version} net.sf.opencsv opencsv ${opencsv-version} net.liftweb lift-json_${scala-major-version} ${lift-version} com.sun.jersey jersey-server ${jersey-version} com.sun.jersey jersey-servlet ${jersey-version} com.sun.jersey jersey-client ${jersey-version} org.squeryl squeryl_${scala-major-version} ${squeryl-version} javax.servlet javax.servlet-api ${servlet-api-version} provided + + + org.bouncycastle + bcprov-jdk15on + ${bouncy-castle-version} + + + org.cryptacular + cryptacular + ${cryptacular-version} + org.scala-lang scala-library ${scala-version} junit junit ${junit-version} test org.scalatest scalatest_${scala-major-version} ${scalatest-version} test org.scala-lang scala-actors org.scala-lang scala-reflect org.scala-lang scala-actors ${scala-version} test org.scala-lang scala-reflect ${scala-version} nexus Nexus Repo https://repo.open.med.harvard.edu/nexus/content/repositories/snapshots false nexus Nexus Repo https://repo.open.med.harvard.edu/nexus/content/repositories/releases-internal