├── .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 [](http://travis-ci.org/t3hnar/scala-bcrypt) [](https://coveralls.io/r/t3hnar/scala-bcrypt) [](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) [](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 |
--------------------------------------------------------------------------------