├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── build.sbt ├── project ├── build.properties └── plugins.sbt ├── src ├── main │ └── scala │ │ └── com │ │ └── github │ │ └── t3hnar │ │ └── bcrypt │ │ ├── BCrypt.scala │ │ └── package.scala └── test │ └── scala │ └── com │ └── github │ └── t3hnar │ └── bcrypt │ └── bcryptSpec.scala └── version.sbt /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | target 4 | 5 | # Mac 6 | .DS_Store 7 | 8 | # VS Code / Metals 9 | .bsp 10 | .metals/ 11 | .vscode/ 12 | .bloop/ 13 | metals.sbt -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | sudo: false 3 | 4 | language: scala 5 | 6 | scala: 7 | - 2.13.3 8 | - 2.12.12 9 | - 2.11.12 10 | 11 | jdk: 12 | - openjdk11 13 | 14 | script: sbt ++$TRAVIS_SCALA_VERSION clean coverage test 15 | 16 | after_success: sbt ++$TRAVIS_SCALA_VERSION coverageReport coveralls 17 | 18 | cache: 19 | directories: 20 | - $HOME/.ivy2/cache 21 | - $HOME/.sbt 22 | 23 | before_cache: 24 | - find $HOME/.ivy2/cache -name "ivydata-*.properties" -print -delete 25 | - find $HOME/.sbt -name "*.lock" -print -delete -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This software is licensed under the Apache 2 license, quoted below. 2 | 3 | Copyright 2012-2013 Yaroslav Klymko [t3hnar@gmail.com] 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not 6 | use this file except in compliance with the License. You may obtain a copy of 7 | the License at 8 | 9 | [http://www.apache.org/licenses/LICENSE-2.0] 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 13 | WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 14 | License for the specific language governing permissions and limitations under 15 | the License. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Scala Bcrypt [![Build Status](https://secure.travis-ci.org/t3hnar/scala-bcrypt.svg)](http://travis-ci.org/t3hnar/scala-bcrypt) [![Coverage Status](https://coveralls.io/repos/t3hnar/scala-bcrypt/badge.svg)](https://coveralls.io/r/t3hnar/scala-bcrypt) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/8e0fb26d880446428fd94b7e051e9cb0)](https://www.codacy.com/app/evolution-gaming/scala-bcrypt?utm_source=github.com&utm_medium=referral&utm_content=t3hnar/scala-bcrypt&utm_campaign=Badge_Grade) [![Version](https://img.shields.io/maven-central/v/com.github.t3hnar/scala-bcrypt_2.11.svg?label=version)](http://search.maven.org/#search%7Cga%7C1%7Cg%3Acom.github.t3hnar%20AND%20scala-bcrypt) 2 | 3 | Scala Bcrypt is a scala friendly wrapper of [jBCRYPT](http://www.mindrot.org/projects/jBCrypt/) 4 | 5 | ## Examples 6 | 7 | ### Safe APIs 8 | The safe APIs will result in `scala.util.Failure`s and `scala.util.Success`s when executing operations to explicitly 9 | indicate the possibility that certain bcrypt operations can fail due to providing incorrect salt versions or number of 10 | rounds (eg. > 30 rounds). 11 | 12 | #### Encrypt password 13 | 14 | ```scala 15 | scala> import com.github.t3hnar.bcrypt._ 16 | import com.github.t3hnar.bcrypt._ 17 | 18 | scala> "password".bcryptSafeBounded 19 | res1: Try[String] = Success($2a$10$iXIfki6AefgcUsPqR.niQ.FvIK8vdcfup09YmUxmzS/sQeuI3QOFG) 20 | ``` 21 | 22 | #### Validate password 23 | 24 | ```scala 25 | scala> "password".isBcryptedSafeBounded("$2a$10$iXIfki6AefgcUsPqR.niQ.FvIK8vdcfup09YmUxmzS/sQeuI3QOFG") 26 | res2: Try[Boolean] = Success(true) 27 | ``` 28 | 29 | #### Composition 30 | Since `Try` is monadic, you can use a for-comprehension to compose operations that return `Success` or `Failure` with 31 | fail-fast semantics. You can also use the desugared notation (`flatMap`s and `map`s) if you prefer. 32 | ```scala 33 | scala> val bcryptAndVerify = for { 34 | bcrypted <- "hello".bcryptBounded(12) 35 | result <- "hello".isBcryptedSafeBounded(bcrypted) 36 | } yield result 37 | res: Try[Boolean] = Success(true) 38 | ``` 39 | 40 | #### Advanced usage 41 | 42 | By default, the `salt` generated internally, and developer does not need to generate and store salt. 43 | But if you decide that you need to manage salt, you can use `bcrypt` in the following way: 44 | 45 | ```scala 46 | scala> val salt = generateSalt 47 | salt: String = $2a$10$8K1p/a0dL1LXMIgoEDFrwO 48 | 49 | scala> "password".bcryptBounded(salt) 50 | res3: Try[String] = Success($2a$10$8K1p/a0dL1LXMIgoEDFrwOfMQbLgtnOoKsWc.6U6H0llP3puzeeEu) 51 | ``` 52 | 53 | ### Unsafe APIs 54 | The Unsafe APIs will result in Exceptions being thrown when executing operations as certain bcrypt operations can fail 55 | due to providing incorrect salt versions or number of rounds (eg. > 30 rounds or password longer than 71 bytes). These Unsafe APIs are present for 56 | backwards compatibility reasons and should be avoided if possible. 57 | 58 | #### Encrypt password 59 | 60 | ```scala 61 | scala> import com.github.t3hnar.bcrypt._ 62 | import com.github.t3hnar.bcrypt._ 63 | 64 | scala> "password".bcryptBounded 65 | res1: String = $2a$10$iXIfki6AefgcUsPqR.niQ.FvIK8vdcfup09YmUxmzS/sQeuI3QOFG 66 | ``` 67 | 68 | #### Validate password 69 | 70 | ```scala 71 | scala> "password".isBcryptedBounded("$2a$10$iXIfki6AefgcUsPqR.niQ.FvIK8vdcfup09YmUxmzS/sQeuI3QOFG") 72 | res2: Boolean = true 73 | ``` 74 | 75 | #### Advanced usage 76 | 77 | ```scala 78 | scala> val salt = generateSalt 79 | salt: String = $2a$10$8K1p/a0dL1LXMIgoEDFrwO 80 | 81 | scala> "password".bcryptBounded(salt) 82 | res3: String = $2a$10$8K1p/a0dL1LXMIgoEDFrwOfMQbLgtnOoKsWc.6U6H0llP3puzeeEu 83 | ``` 84 | 85 | ## Setup 86 | 87 | #### SBT 88 | ```scala 89 | libraryDependencies += "com.github.t3hnar" %% "scala-bcrypt" % "4.3.1" 90 | ``` 91 | 92 | #### Maven 93 | ```xml 94 | 95 | com.github.t3hnar 96 | scala-bcrypt_2.13 97 | 4.3.1 98 | 99 | ``` 100 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | name := "scala-bcrypt" 2 | 3 | organization := "com.github.t3hnar" 4 | 5 | description := "Scala wrapper for jBcrypt + pom.xml inside" 6 | 7 | scalaVersion := "2.13.3" // crossScalaVersions.value.last 8 | 9 | crossScalaVersions := Seq("2.11.12", "2.12.18", "2.13.12", "3.3.1") 10 | 11 | releaseCrossBuild := true 12 | 13 | licenses := Seq(("Apache License, Version 2.0", url("http://www.apache.org/licenses/LICENSE-2.0"))) 14 | 15 | homepage := Some(new URL("https://github.com/t3hnar/scala-bcrypt")) 16 | 17 | startYear := Some(2012) 18 | 19 | scalacOptions := Seq( 20 | "-encoding", "UTF-8", 21 | "-feature", 22 | "-unchecked", 23 | "-deprecation", 24 | "-Xlint", 25 | "-Ywarn-dead-code", 26 | "-Ywarn-numeric-widen") 27 | 28 | libraryDependencies ++= Seq( 29 | "de.svenkubiak" % "jBCrypt" % "0.4.3", 30 | "org.scalatest" %% "scalatest" % "3.2.17" % Test/*, 31 | "org.scalatest" %% "scalatest" % "3.2.0" % Test, 32 | "org.scalatest" %% "scalatest-wordspec" % "3.2.0" % Test*/) 33 | 34 | pomExtra in Global := { 35 | 36 | git@github.com:t3hnar/scala-bcrypt.git 37 | scm:git:git@github.com:t3hnar/scala-bcrypt.git 38 | scm:git:git@github.com:t3hnar/scala-bcrypt.git 39 | 40 | 41 | 42 | t3hnar 43 | Yaroslav Klymko 44 | t3hnar@gmail.com 45 | 46 | 47 | } 48 | 49 | releasePublishArtifactsAction := PgpKeys.publishSigned.value 50 | 51 | publishTo := Some( 52 | if (isSnapshot.value) 53 | Opts.resolver.sonatypeSnapshots 54 | else 55 | Opts.resolver.sonatypeStaging 56 | ) 57 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.9.7 -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | addSbtPlugin("com.github.gseitz" % "sbt-release" % "1.0.13") 2 | 3 | addSbtPlugin("org.xerial.sbt" % "sbt-sonatype" % "3.9.4") 4 | 5 | addSbtPlugin("com.jsuereth" % "sbt-pgp" % "2.0.1") 6 | 7 | addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.6.1") 8 | 9 | addSbtPlugin("org.scoverage" % "sbt-coveralls" % "1.2.7") -------------------------------------------------------------------------------- /src/main/scala/com/github/t3hnar/bcrypt/BCrypt.scala: -------------------------------------------------------------------------------- 1 | package com.github.t3hnar.bcrypt 2 | 3 | import org.mindrot.jbcrypt.{BCrypt => B} 4 | 5 | /** 6 | * @author Yaroslav Klymko 7 | */ 8 | object BCrypt { 9 | def gensalt(rounds: Int = 10): String = B.gensalt(rounds) 10 | } -------------------------------------------------------------------------------- /src/main/scala/com/github/t3hnar/bcrypt/package.scala: -------------------------------------------------------------------------------- 1 | package com.github.t3hnar 2 | 3 | import org.mindrot.jbcrypt.{BCrypt => B} 4 | 5 | import scala.util.{Failure, Try} 6 | 7 | /** 8 | * @author Yaroslav Klymko 9 | */ 10 | package object bcrypt { 11 | 12 | // Maybe consider moving the non deprecated methods no another package with the same method names (loose the "bounded") 13 | // This way the only change the developers would need to make is change the package 14 | implicit class BCryptStrOps(val pswrd: String) extends AnyVal { 15 | 16 | @deprecated("Use boundedBcrypt instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 17 | def bcrypt: String = doBcrypt 18 | 19 | def boundedBcrypt: String = { 20 | if(moreThanLength()) throw illegalArgumentException 21 | else doBcrypt 22 | } 23 | 24 | // The defualt rounds in BCrypt.gensalt() is 10. This may lead to user confusion when using the api. 25 | // I suggest adding an explicit 1, or updating the documentation. 26 | private[this] def doBcrypt: String = B.hashpw(pswrd, BCrypt.gensalt()) 27 | 28 | @deprecated("Use bcryptBounded(rounds: Int) instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 29 | def bcrypt(rounds: Int): String = doBcrypt(rounds) 30 | 31 | def bcryptBounded(rounds: Int): String = { 32 | if(moreThanLength()) throw illegalArgumentException 33 | else doBcrypt(rounds) 34 | } 35 | 36 | def bcryptSafeBounded: Try[String] = { 37 | if(moreThanLength()) Failure(illegalArgumentException) 38 | else Try(doBcrypt) 39 | } 40 | 41 | private[this] def doBcrypt(rounds: Int): String = B.hashpw(pswrd, BCrypt.gensalt(rounds)) 42 | 43 | @deprecated("Use bcryptBounded(salt: String) instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 44 | def bcrypt(salt: String): String = doBcrypt(salt) 45 | 46 | def bcryptBounded(salt: String): String = { 47 | if(moreThanLength()) throw illegalArgumentException 48 | else doBcrypt(salt) 49 | } 50 | 51 | private[this] def doBcrypt(salt: String): String = B.hashpw(pswrd, salt) 52 | 53 | @deprecated("Use isBcryptedBounded(hash: String) instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 54 | def isBcrypted(hash: String): Boolean = doIsBcrypted(hash) 55 | 56 | def isBcryptedBounded(hash: String): Boolean = { 57 | if(moreThanLength()) throw illegalArgumentException 58 | else doIsBcrypted(hash) 59 | } 60 | 61 | private[this] def doIsBcrypted(hash: String): Boolean = B.checkpw(pswrd, hash) 62 | 63 | @deprecated("Use bcryptSafeBounded(rounds: Int) instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 64 | def bcryptSafe(rounds: Int): Try[String] = Try(doBcrypt(rounds)) 65 | 66 | def bcryptSafeBounded(rounds: Int): Try[String] = { 67 | if(moreThanLength()) Failure(illegalArgumentException) 68 | else Try(doBcrypt(rounds)) 69 | } 70 | 71 | @deprecated("Use bcryptSafeBounded(salt: String) instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 72 | def bcryptSafe(salt: String): Try[String] = Try(doBcrypt(salt)) 73 | 74 | def bcryptSafeBounded(salt: String): Try[String] = { 75 | if(moreThanLength()) Failure(illegalArgumentException) 76 | else Try(doBcrypt(salt)) 77 | } 78 | 79 | @deprecated("Use isBcryptedSafeBounded(hash: String) instead.\nMore information at https://github.com/t3hnar/scala-bcrypt/issues/23", "4.3.0") 80 | def isBcryptedSafe(hash: String): Try[Boolean] = Try(doIsBcrypted(hash)) 81 | 82 | def isBcryptedSafeBounded(hash: String): Try[Boolean] = { 83 | if(moreThanLength()) Failure(illegalArgumentException) 84 | else Try(doIsBcrypted(hash)) 85 | } 86 | 87 | private[this] def illegalArgumentException = new IllegalArgumentException(s"$pswrd was more than 71 bytes long.") 88 | 89 | private[this] def moreThanLength(length: Int = 71): Boolean = pswrd.length > length 90 | } 91 | 92 | def generateSalt: String = B.gensalt() 93 | } 94 | -------------------------------------------------------------------------------- /src/test/scala/com/github/t3hnar/bcrypt/bcryptSpec.scala: -------------------------------------------------------------------------------- 1 | package com.github.t3hnar.bcrypt 2 | 3 | 4 | import scala.util.Success 5 | import org.scalatest.matchers.should.Matchers 6 | import org.scalatest.wordspec.AnyWordSpec 7 | 8 | class bcryptSpec extends AnyWordSpec with Matchers { 9 | "safe APIs" should { 10 | "bounded APIs" should { 11 | "encrypt, check if bcrypted and fail if bounds are greater than 71 bytes long" in { 12 | val password = "my password" 13 | val tryHash = password.bcryptSafeBounded 14 | tryHash.map { hash => 15 | password.isBcryptedSafeBounded(hash) shouldEqual Success(true) 16 | "my new password".isBcryptedSafeBounded(hash) shouldEqual Success(false) 17 | }.getOrElse(fail("failed while trying to bcrypt")) 18 | val longPassword = Range(0, 20).map(_ => password).mkString("") 19 | longPassword.bcryptSafeBounded.isFailure should be(true) 20 | } 21 | 22 | "encrypt with provided salt, check if bcrypted and fail if bounds are greater than 71 bytes long" in { 23 | val salt = BCrypt.gensalt() 24 | val password = "password" 25 | val hash = password.bcryptSafeBounded(salt) 26 | hash.isSuccess shouldEqual true 27 | val extractedHash = hash.get 28 | password.isBcryptedSafeBounded(extractedHash) shouldEqual Success(true) 29 | "my new password".isBcryptedSafeBounded(extractedHash) shouldEqual Success(false) 30 | val longPassword = Range(0, 20).map(_ => password).mkString("") 31 | longPassword.bcryptSafeBounded(salt).isFailure should be(true) 32 | } 33 | 34 | "encrypt with provided rounds, check if bcrypted and fail if bounds are greater than 71 bytes long" in { 35 | val password = "password" 36 | val hash = password.bcryptSafeBounded(10) 37 | hash.isSuccess shouldEqual true 38 | val extractedHash = hash.get 39 | password.isBcryptedSafeBounded(extractedHash) shouldEqual Success(true) 40 | "my new password".isBcryptedSafeBounded(extractedHash) shouldEqual Success(false) 41 | val longPassword = Range(0, 20).map(_ => password).mkString("") 42 | longPassword.bcryptSafeBounded(10).isFailure should be(true) 43 | } 44 | 45 | "attempting to check isBcrypted against a non-bcrypted string will result in a scala.util.Failure" in { 46 | val password = "password" 47 | val result = password.isBcryptedSafeBounded(password) 48 | val longPassword = Range(0, 20).map(_ => password).mkString("") 49 | result.isFailure shouldEqual true 50 | longPassword.isBcryptedSafeBounded(password).isFailure shouldEqual true 51 | } 52 | 53 | "attempting to use rounds > 30 will result in a scala.util.Failure" in { 54 | val result = "password".bcryptSafeBounded(31) 55 | result.isFailure shouldEqual true 56 | } 57 | 58 | "attempting to use an invalid salt will result in a scala.util.Failure" in { 59 | val result = "password".bcryptSafeBounded("invalid-salt") 60 | result.isFailure shouldEqual true 61 | } 62 | } 63 | 64 | "unbounded APIs" should { 65 | "encrypt and check if bcrypted" in { 66 | val hash = "my password".bcrypt 67 | "my password".isBcryptedSafe(hash) shouldEqual Success(true) 68 | "my new password".isBcryptedSafe(hash) shouldEqual Success(false) 69 | } 70 | 71 | "encrypt with provided salt and check if bcrypted" in { 72 | val salt = BCrypt.gensalt() 73 | val hash = "password".bcryptSafe(salt) 74 | hash.isSuccess shouldEqual true 75 | val extractedHash = hash.get 76 | "password".isBcryptedSafe(extractedHash) shouldEqual Success(true) 77 | "my new password".isBcryptedSafe(extractedHash) shouldEqual Success(false) 78 | } 79 | 80 | "attempting to check isBcrypted against a non-bcrypted string will result in a scala.util.Failure" in { 81 | val result = "password".isBcryptedSafe("password") 82 | result.isFailure shouldEqual true 83 | } 84 | 85 | "attempting to use rounds > 30 will result in a scala.util.Failure" in { 86 | val result = "password".bcryptSafe(31) 87 | result.isFailure shouldEqual true 88 | } 89 | 90 | "attempting to use an invalid salt will result in a scala.util.Failure" in { 91 | val result = "password".bcryptSafe("invalid-salt") 92 | result.isFailure shouldEqual true 93 | } 94 | } 95 | } 96 | 97 | "unsafe APIs" should { 98 | "bounded APIs" should { 99 | "encrypt, check if bcrypted and fail if bounds are greater than 71 bytes long" in { 100 | val password = "my password" 101 | val hash = password.boundedBcrypt 102 | password.isBcryptedBounded(hash) shouldEqual true 103 | "my new password".isBcryptedBounded(hash) shouldEqual false 104 | val longPassword = Range(0, 20).map(_ => password).mkString("") 105 | val cought = intercept[IllegalArgumentException](longPassword.boundedBcrypt) 106 | cought.getMessage should be(s"$longPassword was more than 71 bytes long.") 107 | val cought2 = intercept[IllegalArgumentException](longPassword.isBcryptedBounded(hash)) 108 | cought2.getMessage should be(s"$longPassword was more than 71 bytes long.") 109 | } 110 | 111 | "encrypt with provided salt and check if bcrypted" in { 112 | val salt = BCrypt.gensalt() 113 | val password = "password" 114 | val hash = password.bcryptBounded(salt) 115 | password.isBcryptedBounded(hash) shouldEqual true 116 | "my new password".isBcryptedBounded(hash) shouldEqual false 117 | val longPassword = Range(0, 20).map(_ => password).mkString("") 118 | val cought = intercept[IllegalArgumentException](longPassword.boundedBcrypt) 119 | cought.getMessage should be(s"$longPassword was more than 71 bytes long.") 120 | } 121 | 122 | "encrypt with provided rounds and check if bcrypted" in { 123 | val password = "password" 124 | val hash = password.bcryptBounded(10) 125 | password.isBcryptedBounded(hash) shouldEqual true 126 | "my new password".isBcryptedBounded(hash) shouldEqual false 127 | val longPassword = Range(0, 20).map(_ => password).mkString("") 128 | val cought = intercept[IllegalArgumentException](longPassword.bcryptBounded(10)) 129 | cought.getMessage should be(s"$longPassword was more than 71 bytes long.") 130 | } 131 | 132 | "throw an exception if bcrypt parameters are incorrect" in { 133 | val invalidSalt = "bad-salt" 134 | val caught = intercept[IllegalArgumentException]("password".bcryptBounded(invalidSalt)) 135 | caught.getMessage shouldEqual "Invalid salt version" 136 | } 137 | } 138 | 139 | "unbounded APIs" should { 140 | "encrypt and check if bcrypted" in { 141 | val hash = "my password".bcrypt 142 | "my password".isBcrypted(hash) shouldEqual true 143 | "my new password".isBcrypted(hash) shouldEqual false 144 | } 145 | 146 | "encrypt and check if bcrypted with rounds" in { 147 | val hash = "my password".bcrypt(10) 148 | "my password".isBcrypted(hash) shouldEqual true 149 | "my new password".isBcrypted(hash) shouldEqual false 150 | } 151 | 152 | "encrypt with provided salt and check if bcrypted" in { 153 | val salt = BCrypt.gensalt() 154 | val hash = "password".bcrypt(salt) 155 | "password".isBcrypted(hash) shouldEqual true 156 | "my new password".isBcrypted(hash) shouldEqual false 157 | } 158 | 159 | "throw an exception if bcrypt parameters are incorrect" in { 160 | val invalidSalt = "bad-salt" 161 | val caught = intercept[IllegalArgumentException] { 162 | "password".bcrypt(invalidSalt) 163 | } 164 | caught.getMessage shouldEqual "Invalid salt version" 165 | } 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /version.sbt: -------------------------------------------------------------------------------- 1 | ThisBuild / version := "4.3.2-SNAPSHOT" 2 | --------------------------------------------------------------------------------