diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2fec79a..54a7b8a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,42 +1,10 @@ name: CI -on: [push, pull_request] +on: + push: + branches: [ master ] + pull_request: jobs: test: - - runs-on: ubuntu-latest - - strategy: - matrix: - scala: - - 2.13.14 - - 2.12.19 - - 3.3.3 - - steps: - - uses: actions/checkout@v2 - - - uses: coursier/cache-action@v5 - - - name: scala - uses: olafurpg/setup-scala@v10 - with: - java-version: openjdk@1.11 - - - name: build ${{ matrix.scala }} - run: sbt ++${{ matrix.scala }} clean coverage test - - - name: test coverage - if: success() - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} - run: sbt ++${{ matrix.scala }} coverageReport coverageAggregate coveralls - - - name: slack - uses: homoluctus/slatify@master - if: failure() && github.ref == 'refs/heads/master' - with: - type: ${{ job.status }} - job_name: Build - url: ${{ secrets.SLACK_WEBHOOK }} \ No newline at end of file + uses: evolution-gaming/scala-github-actions/.github/workflows/ci.yml@dde27b9bd793d41d5aacf8fb74403c9de5da1146 # v6.3.0 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a8a2f2b..3b7a7cc 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,11 +1,11 @@ -name: Publish new Release +name: Publish Release on: - release: - types: [published] - branches: [master] + push: + tags: + - 'v*' jobs: release: - uses: evolution-gaming/scala-github-actions/.github/workflows/release.yml@v1 + uses: evolution-gaming/scala-github-actions/.github/workflows/release.yml@v5 secrets: inherit diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..d31bdbb --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,96 @@ +# Main goals: +# - nicer commit diffs (trailing commas, no alignment for pattern matching, force new lines) +# - better interop with default IntelliJ IDEA setup (matching import and modifiers sorting logic) +# - better developer experience on laptop screens (like 16' MBPs) with IntelliJ IDEA (line wraps) + +version = 3.11.5 + +runner.dialect = scala213source3 + +# only format files tracked by git +project.git = true + +maxColumn = 120 +trailingCommas = always + +preset = default +# do not align to make nicer commit diffs +align.preset = none + +indent { + # altering defnSite and extendSite to have this: + # final class MyErr extends RuntimeException( + # "super error message", + # ) + # instead of this: + # final class MyErr extends RuntimeException( + # "super error message", + # ) + defnSite = 2 + extendSite = 0 +} + +spaces { + # makes string interpolation with curlies more visually distinct + inInterpolatedStringCurlyBraces = true +} + +newlines { + # keep author new lines where possible + source = keep + # force new line after "(implicit" for multi-line arg lists + implicitParamListModifierForce = [after] + avoidForSimpleOverflow = [ + tooLong, # if the line would be too long even after newline inserted, do nothing + slc, # do nothing if overflow caused by single line comment + ] +} + +verticalMultiline { + atDefnSite = true + arityThreshold = 4 # more than 3 args in a list will be turned vertical + newlineAfterOpenParen = true # for nicer commit diffs +} + +# for nicer commit diffs - forces new line before last parenthesis: +# class MyCls( +# arg1: String, +# arg2: String, +# ) extends MyTrait { +# +# without it: +# class MyCls( +# arg1: String, +# arg2: String) extends MyTrait { +danglingParentheses.exclude = [] + +docstrings { + # easier to view diffs in IDEA on 16' MBP screen if docs max line are shorter than code + wrapMaxColumn = 100 + # next settings make it similar to the default IDEA javadoc formatting + style = Asterisk + oneline = unfold + blankFirstLine = unfold +} + +rewrite.rules = [ + Imports, + RedundantParens, + SortModifiers, + prefercurlyfors, +] + +# put visibility modifier first +rewrite.sortModifiers.preset = styleGuide + +# Import sorting as similar as possible to scalafix's "OrganizeImports.preset = INTELLIJ_2020_3". +# Scalafix is not used as its commands mess up "all .." build aliases and it takes long time to run, +# while its code semantic based features are not needed here. +# I.e. detection of unused imports is done with Scala compiler options. +rewrite.imports { + sort = ascii + groups = [ + [".*"], + ["java\\..*", "javax\\..*", "scala\\..*"], + ] +} diff --git a/build.sbt b/build.sbt index 460e574..aa841eb 100644 --- a/build.sbt +++ b/build.sbt @@ -1,31 +1,34 @@ +import sbtversionpolicy.Compatibility.BinaryCompatible + name := "crypto" organization := "com.evolutiongaming" -homepage := Some(url("https://github.com/evolution-gaming/crypto")) +homepage := Some(uri("https://github.com/evolution-gaming/crypto")) startYear := Some(2016) organizationName := "Evolution" -organizationHomepage := Some(url("https://evolution.com")) +organizationHomepage := Some(uri("https://evolution.com")) publishTo := Some(Resolver.evolutionReleases) scalaVersion := crossScalaVersions.value.head -crossScalaVersions := Seq("2.13.18", "2.12.19", "3.3.8") +crossScalaVersions := Seq("2.13.18", "3.3.8") + +versionPolicyIntention := BinaryCompatible libraryDependencies ++= Seq( - "com.typesafe" % "config" % "1.4.9", - "commons-codec" % "commons-codec" % "1.15" , - "org.scalatest" %% "scalatest" % "3.2.20" % Test + "com.typesafe" % "config" % "1.4.9", + "commons-codec" % "commons-codec" % "1.15", + "org.scalatest" %% "scalatest" % "3.2.20" % Test, ) -licenses := Seq(("Apache-2.0", url("http://www.apache.org/licenses/LICENSE-2.0"))) - -releaseCrossBuild := true +licenses := Seq(("Apache-2.0", uri("https://www.apache.org/licenses/LICENSE-2.0"))) -//addCommandAlias("check", "all versionPolicyCheck Compile/doc") -addCommandAlias("check", "show version") -addCommandAlias("build", "+all compile test") +// check is called with + from the release action +addCommandAlias("check", "all versionPolicyCheck Compile/doc scalafmtCheckRepo") +addCommandAlias("fmt", "+all scalafmtRepo") +addCommandAlias("build", "all compile testFull") diff --git a/project/build.properties b/project/build.properties index 081fdbb..fdcf9af 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.10.0 +sbt.version = 2.0.6 diff --git a/project/plugins.sbt b/project/plugins.sbt index 2bfc514..54e584d 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,9 +1,11 @@ addSbtPlugin("org.scoverage" % "sbt-scoverage" % "2.4.4") -addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.3.11") - -addSbtPlugin("com.github.sbt" % "sbt-release" % "1.0.15") +addSbtPlugin("com.github.sbt" % "sbt-dynver" % "5.1.1") addSbtPlugin("com.evolution" % "sbt-scalac-opts-plugin" % "0.2.0") -addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.1.2") \ No newline at end of file +addSbtPlugin("com.evolution" % "sbt-artifactory-plugin" % "0.1.2") + +addSbtPlugin("ch.epfl.scala" % "sbt-version-policy" % "3.3.0") + +addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.6.2") diff --git a/src/main/scala/com/evolutiongaming/crypto/Crypto.scala b/src/main/scala/com/evolutiongaming/crypto/Crypto.scala index 467a9b4..3c05b42 100644 --- a/src/main/scala/com/evolutiongaming/crypto/Crypto.scala +++ b/src/main/scala/com/evolutiongaming/crypto/Crypto.scala @@ -1,19 +1,20 @@ package com.evolutiongaming.crypto +import org.apache.commons.codec.binary.{Base64, Hex} +import org.apache.commons.codec.digest.DigestUtils + import java.nio.ByteBuffer import java.nio.charset.StandardCharsets.UTF_8 import java.security.SecureRandom - import javax.crypto.spec.{GCMParameterSpec, IvParameterSpec, SecretKeySpec} import javax.crypto.{AEADBadTagException, Cipher} -import org.apache.commons.codec.binary.{Base64, Hex} -import org.apache.commons.codec.digest.DigestUtils /** - * Copyright (C) 2009-2016 Lightbend Inc. - * - * Based on https://github.com/playframework/playframework/blob/master/framework/src/play/src/main/scala/play/api/libs/Crypto.scala - */ + * Copyright (C) 2009-2016 Lightbend Inc. + * + * Based on + * https://github.com/playframework/playframework/blob/master/framework/src/play/src/main/scala/play/api/libs/Crypto.scala + */ object Crypto { class DecryptAuthException(cause: Throwable) extends Exception( "Decrypted value is not the original one, most likely wrong private key used for decryption", @@ -27,34 +28,40 @@ object Crypto { private lazy val secureRandom = new SecureRandom /** - * Encrypts a string with the AES algorithm and the supplied private key - pair to the - * [[decryptAES]] method. - * - * AES/GCM/NoPadding transformation is used with 128 bit key for authenticated encryption. - * The secret key entropy is obtained from the given private key by applying the SHA256 hash. - * - * @param value string value to encrypt - * @param privateKey private key string to use in encryption - * @return an encrypted string - */ + * Encrypts a string with the AES algorithm and the supplied private key - pair to the + * [[decryptAES]] method. + * + * AES/GCM/NoPadding transformation is used with 128 bit key for authenticated encryption. The + * secret key entropy is obtained from the given private key by applying the SHA256 hash. + * + * @param value + * string value to encrypt + * @param privateKey + * private key string to use in encryption + * @return + * an encrypted string + */ def encryptAES(value: String, privateKey: String): String = { s"3-${ AES_V3.encrypt(value, privateKey) }" } /** - * Decrypts a string with the AES algorithm and the supplied private key - pair to the - * [[encryptAES]] method. - * - * Additionally to the current [[encryptAES]] encryption mode, several legacy modes supported. - * - * If the current [[encryptAES]] algorithm is used, it is guaranteed that if a decrypted value is returned - * it is the original one and the private key is valid. In case a wrong private key is used, an exception - * will be thrown. - * - * @param value an encrypted string produced by the [[encryptAES]] method - * @param privateKey private key string used in encryption - * @return decrypted string - */ + * Decrypts a string with the AES algorithm and the supplied private key - pair to the + * [[encryptAES]] method. + * + * Additionally to the current [[encryptAES]] encryption mode, several legacy modes supported. + * + * If the current [[encryptAES]] algorithm is used, it is guaranteed that if a decrypted value is + * returned it is the original one and the private key is valid. In case a wrong private key is + * used, an exception will be thrown. + * + * @param value + * an encrypted string produced by the [[encryptAES]] method + * @param privateKey + * private key string used in encryption + * @return + * decrypted string + */ def decryptAES(value: String, privateKey: String): String = { val separator = "-" val sepIndex = value.indexOf(separator) @@ -70,17 +77,19 @@ object Crypto { AES_V2.decrypt(data, privateKey) case "3" => AES_V3.decrypt(data, privateKey) - case _ => + case _ => throw new RuntimeException("Unknown version") } } } - /** AES legacy V0 (no versioning) mode support - it has restrictions on key size */ + /** + * AES legacy V0 (no versioning) mode support - it has restrictions on key size + */ private object AES_V0 { private val CipherAlgorithm = "AES" private val CipherTransformation = "AES" - private val KeySizeBytes: Int = 16 //128 bit + private val KeySizeBytes: Int = 16 // 128 bit def decrypt(value: String, privateKey: String): String = { val privateKeyBytes = privateKey.getBytes(UTF_8) @@ -95,9 +104,9 @@ object Crypto { } /** - * AES legacy V1 mode support: - * - no restrictions on key size - SHA256 hash is used to obtain key entropy - */ + * AES legacy V1 mode support: + * - no restrictions on key size - SHA256 hash is used to obtain key entropy + */ private object AES_V1 { private val CipherTransformation = "AES" @@ -112,10 +121,10 @@ object Crypto { } /** - * AES legacy V1 mode support: - * - no restrictions on key size - SHA256 hash is used to obtain key entropy - * - AES/CTR/NoPadding (128 bit key) cipher with IV - */ + * AES legacy V1 mode support: + * - no restrictions on key size - SHA256 hash is used to obtain key entropy + * - AES/CTR/NoPadding (128 bit key) cipher with IV + */ private object AES_V2 { private val CipherTransformation = "AES/CTR/NoPadding" @@ -133,12 +142,12 @@ object Crypto { } /** - * Current AES mode - V3: - * - no restrictions on key size - SHA256 hash is used to obtain key entropy - * - AES/GCM/NoPadding (128 bit key) cipher is used to provide authenticated encryption - * - dynamic length random IV - 12 bytes by default with possible extension up to 255 bytes - * - 128 bit auth tag length - */ + * Current AES mode - V3: + * - no restrictions on key size - SHA256 hash is used to obtain key entropy + * - AES/GCM/NoPadding (128 bit key) cipher is used to provide authenticated encryption + * - dynamic length random IV - 12 bytes by default with possible extension up to 255 bytes + * - 128 bit auth tag length + */ private object AES_V3 { /* implementation based on @@ -156,7 +165,7 @@ object Crypto { https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-38d.pdf */ private val CurrentIVLengthBytes = 12 - private val MinIVLengthBytes = 12 //does not allow decrypting with IVs smaller than 12 bytes (96 bits) + private val MinIVLengthBytes = 12 // does not allow decrypting with IVs smaller than 12 bytes (96 bits) def encrypt(value: String, privateKey: String): String = { val skeySpec = aesSKey128bitWithSha256(privateKey.getBytes(UTF_8)) @@ -170,9 +179,9 @@ object Crypto { } private def encodeEncryptedToString(iv: Array[Byte], encryptedValue: Array[Byte]): String = { - //1 byte for dynamic IV length encoding + // 1 byte for dynamic IV length encoding val buf = ByteBuffer.allocate(1 + iv.length + encryptedValue.length) - //encode IV length as an unsigned byte + // encode IV length as an unsigned byte buf.put(iv.length.toByte) buf.put(iv) buf.put(encryptedValue) @@ -185,7 +194,7 @@ object Crypto { val cipher = Cipher.getInstance(CipherTransformation) val ivLength = readValidIvLength(payload) - val ivOffset = 1 //1 byte for encoded IV length + val ivOffset = 1 // 1 byte for encoded IV length val ivEndIdx = ivOffset + ivLength require(payload.length >= ivEndIdx, "invalid data size") val gcmParamSpec = new GCMParameterSpec(AuthTagLengthBits, payload, ivOffset, ivLength) @@ -208,12 +217,12 @@ object Crypto { } /** - * Creates a SecretKeySpec instance for an AES algorithm with an 128 bit key produced from SHA256 hash of - * the private key data. - */ + * Creates a SecretKeySpec instance for an AES algorithm with an 128 bit key produced from SHA256 + * hash of the private key data. + */ private def aesSKey128bitWithSha256(privateKeyBytes: Array[Byte]): SecretKeySpec = { val privateKeyDigest = DigestUtils.sha256(privateKeyBytes) - val effectiveSecretKey = privateKeyDigest.take(16) //128 bit = 16 bytes + val effectiveSecretKey = privateKeyDigest.take(16) // 128 bit = 16 bytes new SecretKeySpec(effectiveSecretKey, "AES") } } diff --git a/src/main/scala/com/evolutiongaming/crypto/DecryptConfig.scala b/src/main/scala/com/evolutiongaming/crypto/DecryptConfig.scala index 29e8720..382c2d9 100644 --- a/src/main/scala/com/evolutiongaming/crypto/DecryptConfig.scala +++ b/src/main/scala/com/evolutiongaming/crypto/DecryptConfig.scala @@ -12,7 +12,7 @@ object DecryptConfig { def apply(password: String, config: Config = ConfigFactory.load()): String = try { if ( config.hasPath(EncryptedPasswordsPath) && - config.getBoolean(EncryptedPasswordsPath) + config.getBoolean(EncryptedPasswordsPath) ) { val secret = config getString AppSecretPath Crypto.decryptAES(password, secret) diff --git a/src/main/scala/com/evolutiongaming/crypto/Encrypt.scala b/src/main/scala/com/evolutiongaming/crypto/Encrypt.scala index 151e51d..cfd4cf7 100644 --- a/src/main/scala/com/evolutiongaming/crypto/Encrypt.scala +++ b/src/main/scala/com/evolutiongaming/crypto/Encrypt.scala @@ -13,7 +13,10 @@ object Encrypt extends App { generated } - require(privateKey.length == 16, s"Expected privateKey to have 16 characters, got ${privateKey.length} characters instead") + require( + privateKey.length == 16, + s"Expected privateKey to have 16 characters, got ${ privateKey.length } characters instead", + ) val encrypted = Crypto.encryptAES(value, privateKey) println(encrypted) diff --git a/src/test/scala/com/evolutiongaming/crypto/CryptoSpec.scala b/src/test/scala/com/evolutiongaming/crypto/CryptoSpec.scala index 7b1b043..9c9c3a6 100644 --- a/src/test/scala/com/evolutiongaming/crypto/CryptoSpec.scala +++ b/src/test/scala/com/evolutiongaming/crypto/CryptoSpec.scala @@ -1,14 +1,13 @@ package com.evolutiongaming.crypto -import java.nio.charset.StandardCharsets.UTF_8 - -import javax.crypto.Cipher -import javax.crypto.spec.{GCMParameterSpec, SecretKeySpec} import org.apache.commons.codec.binary.Base64 import org.apache.commons.codec.digest.DigestUtils import org.scalatest.flatspec.AnyFlatSpec import org.scalatest.matchers.should.Matchers +import java.nio.charset.StandardCharsets.UTF_8 +import javax.crypto.Cipher +import javax.crypto.spec.{GCMParameterSpec, SecretKeySpec} import scala.util.Random class CryptoSpec extends AnyFlatSpec with Matchers { diff --git a/version.sbt b/version.sbt deleted file mode 100644 index 8fec683..0000000 --- a/version.sbt +++ /dev/null @@ -1 +0,0 @@ -ThisBuild / version := "2.2.1-SNAPSHOT"