From d94529caad6174814426f6e8f016785c7d509138 Mon Sep 17 00:00:00 2001 From: Alex Ehrnschwender Date: Mon, 27 Jul 2015 22:39:25 -0700 Subject: [PATCH] adds basic model tests, replaces sha hash/salt with bcrypt --- app/models/App.scala | 8 +++- app/models/DeviceToken.scala | 9 ++--- app/models/Event.scala | 15 +++++--- app/models/RedisModel.scala | 6 +-- app/models/User.scala | 60 ++++++++++-------------------- build.sbt | 5 ++- project/build.properties | 2 +- test/models/AppSpec.scala | 42 +++++++++++++++++++++ test/models/DeviceTokenSpec.scala | 44 ++++++++++++++++++++++ test/models/EventSpec.scala | 42 +++++++++++++++++++++ test/models/RegistrationSpec.scala | 44 ++++++++++++++++++++++ test/models/UserSpec.scala | 34 +++++++++++++++++ 12 files changed, 253 insertions(+), 58 deletions(-) create mode 100644 test/models/AppSpec.scala create mode 100644 test/models/DeviceTokenSpec.scala create mode 100644 test/models/EventSpec.scala create mode 100644 test/models/RegistrationSpec.scala create mode 100644 test/models/UserSpec.scala diff --git a/app/models/App.scala b/app/models/App.scala index a4f9e2a..0d1ea06 100644 --- a/app/models/App.scala +++ b/app/models/App.scala @@ -80,6 +80,12 @@ object App extends RedisModel { case _ => None } + def delete(key: String): Boolean = { + App.findByKey(key).map(app => { + app.delete + }).getOrElse(false) + } + def create(userId: Long, name: String, appMode: Int, debugMode: Boolean, iosCertPassword: Option[String], gcmApiKey: Option[String]): Option[String] = { val key = RandomGenerator.generateKey(22) val secret = RandomGenerator.generateSecret(22) @@ -116,4 +122,4 @@ object RandomGenerator { def generateRandomString(length: Int, chars: Seq[Char]) = (1 to length map { _ => chars(random.nextInt(chars.length)) }).mkString -} \ No newline at end of file +} diff --git a/app/models/DeviceToken.scala b/app/models/DeviceToken.scala index 2d68d07..38e5203 100644 --- a/app/models/DeviceToken.scala +++ b/app/models/DeviceToken.scala @@ -1,7 +1,6 @@ package models import java.util.Date -import com.redis._ import com.redis.serialization._ import Parse.Implicits.parseLong @@ -13,7 +12,7 @@ case class DeviceToken(appKey: String, value: String, lastRegistrationDate: Date object DeviceToken extends RedisConnection { - def findAllByAppKey(appKey: String, limit: Option[(Int, Int)] = None) = { + def findAllByAppKey(appKey: String, limit: Option[(Int, Int)] = None): List[DeviceToken] = { def iterate(result: Iterable[Option[String]], acc: List[DeviceToken]): List[DeviceToken] = result match { case Some(value) :: Some(time) :: rest => { iterate(rest, DeviceToken(appKey, value, new Date(time.toLong)) :: acc) @@ -27,9 +26,9 @@ object DeviceToken extends RedisConnection { result map (iterate(_, List())) getOrElse List() } - def findByAppKeyAndValue(appKey: String, value: String) = + def findByAppKeyAndValue(appKey: String, value: String): Option[DeviceToken] = redis.get("device_token:" + appKey + ":" + value.toUpperCase) map { time => - Some(DeviceToken(appKey, value, new Date(time.toLong))) + Some(DeviceToken(appKey, value, new Date(time))) } getOrElse None def countAllByAppKey(appKey: String): Long = @@ -56,4 +55,4 @@ object DeviceToken extends RedisConnection { true } -} \ No newline at end of file +} diff --git a/app/models/Event.scala b/app/models/Event.scala index 7d343e9..65d4d90 100644 --- a/app/models/Event.scala +++ b/app/models/Event.scala @@ -17,9 +17,8 @@ case class Event(date: Date, severity: Short, message: String) { } -object Event { +object Event extends RedisConnection { - val redis = new RedisClient("localhost", 6379) val maxEventsCount = 1000 object Severity { @@ -31,7 +30,7 @@ object Event { val CRITICAL: Short = 5 } - def findAllByAppKey(appKey: String, offset: Int, count: Int) = { + def findAllByAppKey(appKey: String, offset: Int = 0, count: Int = 50): List[Event] = { redis.lrange("events:" + appKey, offset, count) map { list => list.flatten map { jsonString => val json = Json.parse(jsonString) @@ -43,10 +42,10 @@ object Event { } getOrElse List() } - def countAllByAppKey(appKey: String) = + def countAllByAppKey(appKey: String): Long = redis.llen("events:" + appKey).getOrElse[Long](0) - def create(appKey: String, severity: Short, message: String) = { + def create(appKey: String, severity: Short, message: String): Boolean = { val log = Json.toJson(Map( "date" -> Json.toJson(System.currentTimeMillis()), "severity" -> Json.toJson(severity), @@ -56,4 +55,8 @@ object Event { redis.ltrim("events:" + appKey, 0, maxEventsCount - 1) } -} \ No newline at end of file + def deleteByAppKey(appKey: String): Option[Long] = { + redis.del(s"events:$appKey") + } + +} diff --git a/app/models/RedisModel.scala b/app/models/RedisModel.scala index 49ee466..0de7805 100644 --- a/app/models/RedisModel.scala +++ b/app/models/RedisModel.scala @@ -3,14 +3,12 @@ package models import com.redis._ trait RedisConnection { - val redis = new RedisClient("localhost", 6379) - } trait RedisModel extends RedisConnection { - def createOrUpdateHash(key: String, hash: Map[String, Any]) = { + def createOrUpdateHash(key: String, hash: Map[String, Any]): Boolean = { val noneFields = noneFieldsFromMap(hash) val valuesHash = valuesMapFromMap(hash) @@ -31,4 +29,4 @@ trait RedisModel extends RedisConnection { def valuesMapFromMap(m: Map[String, Any]) = (m -- noneFieldsFromMap(m)) map { case (k, Some(v)) => k -> v; case (k, v) => k -> v } -} \ No newline at end of file +} diff --git a/app/models/User.scala b/app/models/User.scala index 8dd4059..3f78889 100644 --- a/app/models/User.scala +++ b/app/models/User.scala @@ -1,65 +1,45 @@ package models -case class User(email: String, encryptedPassword: String, passwordSalt: String) +import com.github.t3hnar.bcrypt._ + +case class User(email: String, encryptedPassword: String) object User extends RedisModel { def fromMap(attrs: Map[String, String]): User = { User(attrs.getOrElse("email", ""), - attrs.getOrElse("encryptedPassword", ""), - attrs.getOrElse("passwordSalt", "")) + attrs.getOrElse("encryptedPassword", "")) } - def authenticate(email: String, password: String) = User.findByEmail(email) map { user => - PasswordUtil.authenticate(password, PasswordUtil.toByteArray(user.encryptedPassword), PasswordUtil.toByteArray(user.passwordSalt)) + def authenticate(email: String, password: String): Boolean = User.findByEmail(email) map { user => + PasswordUtil.authenticate(password, user.encryptedPassword) } getOrElse false - def findByEmail(email: String) = redis.hgetall("user:" + email) map { m => + def findByEmail(email: String): Option[User] = redis.hgetall("user:" + email) map { m => if (m.nonEmpty) Some(fromMap(m)) else None } getOrElse None - def create(email: String, password: String) = { - val passwordSalt = PasswordUtil.generateSalt - val encryptedPassword = PasswordUtil.encryptPassword(password, passwordSalt) - val attrs = Map("email" -> email, "encryptedPassword" -> PasswordUtil.toString(encryptedPassword), "passwordSalt" -> PasswordUtil.toString(passwordSalt)) + def create(email: String, password: String): Boolean = { + val encryptedPassword = PasswordUtil.encryptPassword(password) + val attrs = Map("email" -> email, "encryptedPassword" -> encryptedPassword) createOrUpdateHash("user:" + email, attrs) } + def delete(email: String): Option[Long] = { + redis.del(s"user:$email") + } + } -// http://www.javacodegeeks.com/2012/05/secure-password-storage-donts-dos-and.html +// http://codahale.com/how-to-safely-store-a-password/ object PasswordUtil { - import java.nio.charset.Charset - import java.security.{NoSuchAlgorithmException, SecureRandom} - import java.security.spec.{InvalidKeySpecException, KeySpec} - import java.util.Arrays - - import javax.crypto.SecretKeyFactory - import javax.crypto.spec.PBEKeySpec - - def authenticate(attemptedPassword: String, encryptedPassword: Array[Byte], salt: Array[Byte]) = { - val encryptedAttemptedPassword = encryptPassword(attemptedPassword, salt) - Arrays.equals(encryptedPassword, encryptedAttemptedPassword) + def authenticate(attemptedPassword: String, encryptedPassword: String) = { + attemptedPassword.isBcrypted(encryptedPassword) } - def encryptPassword(password: String, salt: Array[Byte]) = { - val spec = new PBEKeySpec(password.toCharArray, salt, 10000, 160) - val f = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1") - f.generateSecret(spec).getEncoded + def encryptPassword(password: String) = { + password.bcrypt } - def generateSalt = { - val random = SecureRandom.getInstance("SHA1PRNG") - val salt: Array[Byte] = Array(0, 0, 0, 0, 0, 0, 0, 0) - random.nextBytes(salt) - salt - } - - def toString(bytes: Array[Byte]) = - new sun.misc.BASE64Encoder().encode(bytes) - - def toByteArray(s: String) = - new sun.misc.BASE64Decoder().decodeBuffer(s) - -} \ No newline at end of file +} diff --git a/build.sbt b/build.sbt index 3cb0a57..7293fbd 100644 --- a/build.sbt +++ b/build.sbt @@ -3,7 +3,10 @@ name := "plush" version := "1.1-SNAPSHOT" libraryDependencies := Seq( - "net.debasishg" %% "redisclient" % "2.10" + "net.debasishg" %% "redisclient" % "2.10", + "com.github.t3hnar" %% "scala-bcrypt" % "2.4", + "org.scalatest" % "scalatest_2.10" % "2.0" % "test", + "org.scalatestplus" %% "play" % "1.0.0" % "test" ) scalacOptions := Seq("-feature") diff --git a/project/build.properties b/project/build.properties index 0974fce..64abd37 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.0 +sbt.version=0.13.6 diff --git a/test/models/AppSpec.scala b/test/models/AppSpec.scala new file mode 100644 index 0000000..919cfd4 --- /dev/null +++ b/test/models/AppSpec.scala @@ -0,0 +1,42 @@ +package models + +import org.scalatestplus.play.{OneAppPerSuite, PlaySpec} + +class AppSpec extends PlaySpec with OneAppPerSuite { + val testUserId = 123L + val testAppName = "testAppName" + val testAppMode = 0 + val testAppDebugMode = true + val testAppIosCertPass = Some("testIosCertPass") + val testAppGcmKey = Some("testGcmKey") + + var testApp: Option[App] = None + var testAppKey: Option[String] = None + + "App" must { + "create an app" in { + val created = App.create(testUserId, testAppName, testAppMode, testAppDebugMode, testAppIosCertPass, testAppGcmKey) + assert(created.nonEmpty) + testAppKey = created + } + + "find an app by key" in { + val found = App.findByKey(testAppKey.get) + assert(found.nonEmpty) + assert(found.get.name == testAppName) + assert(found.get.userId == testUserId) + assert(found.get.iosCertPassword == testAppIosCertPass) + assert(found.get.gcmApiKey == testAppGcmKey) + } + + "find all" in { + val found = App.all + assert(found.nonEmpty) + assert(found.head.name == testAppName) + assert(found.head.userId == testUserId) + assert(found.head.iosCertPassword == testAppIosCertPass) + assert(found.head.gcmApiKey == testAppGcmKey) + } + + } +} diff --git a/test/models/DeviceTokenSpec.scala b/test/models/DeviceTokenSpec.scala new file mode 100644 index 0000000..75ec2bf --- /dev/null +++ b/test/models/DeviceTokenSpec.scala @@ -0,0 +1,44 @@ +package models + +import org.scalatestplus.play.{OneAppPerSuite, PlaySpec} +import play.api.Play + + +class DeviceTokenSpec extends PlaySpec with OneAppPerSuite { + val testAppKey = "testAppKey" + val testValue = "testValue" + + "DeviceToken" must { + + "create a device token" in { + val created = DeviceToken.create(testAppKey, testValue) + assert(created.nonEmpty) + assert(created.get.appKey == testAppKey) + assert(created.get.value == testValue.toUpperCase) + } + + "count all by app key" in { + val count = DeviceToken.countAllByAppKey(testAppKey) + assert(count == 1L) + } + + "find all by app key" in { + val found = DeviceToken.findAllByAppKey(testAppKey) + assert(found.length == 1) + assert(found.head.appKey == testAppKey) + assert(found.head.value == testValue.toUpperCase) + } + + "find by app key and value" in { + val found = DeviceToken.findByAppKeyAndValue(testAppKey, testValue) + assert(found.nonEmpty) + assert(found.get.appKey == testAppKey) + assert(found.get.value == testValue) + } + + "delete a device token" in { + val deleted = DeviceToken.delete(testAppKey, testValue) + assert(deleted) + } + } +} diff --git a/test/models/EventSpec.scala b/test/models/EventSpec.scala new file mode 100644 index 0000000..8074a5a --- /dev/null +++ b/test/models/EventSpec.scala @@ -0,0 +1,42 @@ +package models + +import org.scalatestplus.play.{OneAppPerSuite, PlaySpec} +import play.api.Play + + +class EventSpec extends PlaySpec with OneAppPerSuite { + val testAppKey = "testAppKey" + val testSeverity = 0.toShort + val testMessage = "testMessage" + + "Event" must { + "start the FakeApplication" in { + Play.maybeApplication mustBe Some(app) + } + + "create an event" in { + val created = Event.create(testAppKey, testSeverity, testMessage) + assert(created) + } + + + "count all by app key" in { + val count = Event.countAllByAppKey(testAppKey) + assert(count == 1) + } + + "find all by app key" in { + val found = Event.findAllByAppKey(testAppKey) + assert(found.length == 1) + assert(found.head.message == testMessage) + assert(found.head.severity == testSeverity) + } + + "remove by app key" in { + val removed = Event.deleteByAppKey(testAppKey) + assert(removed.nonEmpty) + assert(removed.get == 1L) + } + + } +} diff --git a/test/models/RegistrationSpec.scala b/test/models/RegistrationSpec.scala new file mode 100644 index 0000000..86d8fcf --- /dev/null +++ b/test/models/RegistrationSpec.scala @@ -0,0 +1,44 @@ +package models + +import org.scalatestplus.play.{OneAppPerSuite, PlaySpec} +import play.api.Play + + +class RegistrationSpec extends PlaySpec with OneAppPerSuite { + val testAppKey = "testAppKey" + val testValue = "testValue" + + "Registration" must { + + "create a registration" in { + val created = Registration.create(testAppKey, testValue) + assert(created.nonEmpty) + assert(created.get.appKey == testAppKey) + assert(created.get.value == testValue) + } + + "count all by app key" in { + val count = Registration.countAllByAppKey(testAppKey) + assert(count == 1L) + } + + "find all by app key" in { + val found = Registration.findAllByAppKey(testAppKey) + assert(found.length == 1) + assert(found.head.appKey == testAppKey) + assert(found.head.value == testValue) + } + + "find by app key and value" in { + val found = Registration.findByAppKeyAndValue(testAppKey, testValue) + assert(found.nonEmpty) + assert(found.get.appKey == testAppKey) + assert(found.get.value == testValue) + } + + "delete a registration" in { + val deleted = Registration.delete(testAppKey, testValue) + assert(deleted) + } + } +} diff --git a/test/models/UserSpec.scala b/test/models/UserSpec.scala new file mode 100644 index 0000000..cf8526c --- /dev/null +++ b/test/models/UserSpec.scala @@ -0,0 +1,34 @@ +package models + +import org.scalatestplus.play.{OneAppPerSuite, PlaySpec} + +class UserSpec extends PlaySpec with OneAppPerSuite { + val testEmail = "testEmail" + val testPasswordRaw = "testPassword" + + "User" must { + "create a user" in { + val created = User.create(testEmail, testPasswordRaw) + assert(created) + } + + "authenticate a user" in { + val authenticated = User.authenticate(testEmail, testPasswordRaw) + assert(authenticated) + } + + "find by email" in { + val found = User.findByEmail(testEmail) + assert(found.nonEmpty) + assert(found.get.email == testEmail) + assert(found.get.encryptedPassword.nonEmpty) + } + + "delete a user" in { + val deleted = User.delete(testEmail) + assert(deleted.nonEmpty) + assert(deleted.get == 1L) + } + + } +}