Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
target/
src_managed/

.sbtopts

Expand Down
28 changes: 27 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import org.openqa.selenium.chrome.{ChromeDriver, ChromeOptions}
import org.openqa.selenium.firefox.{FirefoxOptions, FirefoxProfile}
import org.openqa.selenium.remote.server.{DriverFactory, DriverProvider}
import org.scalajs.jsenv.selenium.SeleniumJSEnv
import sbt.nio.file.FileTreeView

import JSEnv._
name := "bobcats"
Expand Down Expand Up @@ -87,21 +88,46 @@ val disciplineMUnitVersion = "2.0.0-M2"

lazy val root = tlCrossRootProject.aggregate(core, testRuntime)

val aesGcmEncryptTestsGenerate = taskKey[Seq[File]]("Generate AES GCM test cases")
val aesCbcTestsGenerate = taskKey[Seq[File]]("Generate AES CBC test cases")

lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform)
.in(file("core"))
.settings(
name := "bobcats",
libraryDependencies ++= Seq(
"org.typelevel" %%% "cats-core" % catsVersion,
"org.typelevel" %%% "cats-effect-kernel" % catsEffectVersion,
"org.typelevel" %%% "cats-effect-std" % catsEffectVersion,
"org.scodec" %%% "scodec-bits" % scodecBitsVersion,
"co.fs2" %%% "fs2-core" % fs2Version,
"org.scalameta" %%% "munit" % munitVersion % Test,
"org.typelevel" %%% "cats-laws" % catsVersion % Test,
"org.typelevel" %%% "cats-effect" % catsEffectVersion % Test,
"org.typelevel" %%% "discipline-munit" % disciplineMUnitVersion % Test,
"org.typelevel" %%% "munit-cats-effect" % munitCEVersion % Test
)
),
// Generate the sources /outside/ of the cross
(Test / sourceManaged) := crossProjectBaseDirectory.value / "shared" / "src_managed" / "test",
aesGcmEncryptTestsGenerate := AESGCMEncryptTestVectorGenerator
.task(
aesGcmEncryptTestsGenerate,
"AESGCMEncryptTestVectors"
)
.value,
aesCbcTestsGenerate := AESCBCTestVectorGenerator
.task(
aesCbcTestsGenerate,
"AESCBCTestVectors"
)
.value,
aesGcmEncryptTestsGenerate / fileInputs +=
(crossProjectBaseDirectory.value / "shared" / "src" / "test" / "resources").toGlob / "gcmEncryptExtIV*.rsp",
aesCbcTestsGenerate / fileInputs +=
(crossProjectBaseDirectory.value / "shared" / "src" / "test" / "resources").toGlob / "CBC*256.rsp",
Test / sourceGenerators ++= Seq(
aesGcmEncryptTestsGenerate.taskValue,
aesCbcTestsGenerate.taskValue)
)
.dependsOn(testRuntime % Test)

Expand Down
27 changes: 27 additions & 0 deletions core/js/src/main/scala/bobcats/AlgorithmPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
/*
* Copyright 2021 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package bobcats

private[bobcats] trait AlgorithmPlatform
private[bobcats] trait HashAlgorithmPlatform
private[bobcats] trait HmacAlgorithmPlatform

private[bobcats] trait CipherAlgorithmPlatform {}

// private[bobcats] trait AlgorithmParameterSpecPlatform[+A <: Algorithm]

// private[bobcats] trait IvParameterSpecPlatform[+A <: CipherAlgorithm]
204 changes: 204 additions & 0 deletions core/js/src/main/scala/bobcats/CipherPlatform.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/*
* Copyright 2021 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package bobcats

import cats.effect.kernel.{Async, Sync}
import scodec.bits.ByteVector
import cats.syntax.all._

import scala.scalajs.js

private[bobcats] trait CipherPlatform[F[_]]

private final class SubtleCryptoCipher[F[_]](implicit F: Async[F]) extends UnsealedCipher[F] {

import facade.browser.{crypto, AesCbcParams, AesGcmParams, AesImportParams}
import BlockCipherAlgorithm._

override def importKey[A <: CipherAlgorithm[_]](
key: ByteVector,
algorithm: A): F[SecretKey[A]] =
F.pure(SecretKeySpec(key, algorithm))

override def encrypt[P <: CipherParams, A <: CipherAlgorithm[P]](
key: SecretKey[A],
params: P,
data: ByteVector): F[ByteVector] =
key match {
case SecretKeySpec(key, alg) =>
params match {
case AES.CBC.Params(iv, padding) =>
for {
key <- F.fromPromise(
F.delay(
crypto
.subtle
.importKey(
"raw",
key.toUint8Array,
new AesImportParams {
val name = "AES-CBC"
},
false,
js.Array("encrypt"))))
cipherText <- F.fromPromise(
F.delay(
crypto
.subtle
.encrypt(AesCbcParams(iv.data.toJSArrayBuffer), key, data.toJSArrayBuffer)))
} yield {
// TODO: `ArrayBuffer#resize doesn't seem to exist`
val bytes = ByteVector.view(cipherText)
if (padding) bytes else bytes.take(data.length)
}

case AES.GCM.Params(iv, padding, tagLength, ad) =>
for {
key <-
if (!padding) {
F.raiseError(
new UnsupportedAlgorithm(
"`SubtleCrypto` does not support no padding for AEAD ciphers"))
} else
F.fromPromise(
F.delay(
crypto
.subtle
.importKey(
"raw",
key.toUint8Array,
new AesImportParams {
val name = "AES-GCM"
},
false,
js.Array("encrypt")))).adaptError {
// This is useless
case e: js.JavaScriptException =>
new UnsupportedAlgorithm(alg.toString, e)
}
cipherText <- F.fromPromise(
F.delay(
crypto
.subtle
.encrypt(
AesGcmParams(
iv.data.toJSArrayBuffer,
if (ad.isEmpty) js.undefined else ad.toJSArrayBuffer,
tagLength.bitLength
),
key,
data.toJSArrayBuffer)))
} yield ByteVector.view(cipherText)
}
}

def decrypt[P <: CipherParams, A <: CipherAlgorithm[P]](
key: SecretKey[A],
params: P,
data: ByteVector): F[ByteVector] = ???

}

private final class CryptoCipher[F[_]](ciphers: js.Array[String])(implicit F: Sync[F])
extends UnsealedCipher[F] {

import facade.node.crypto
import facade.node.CipherOptions

import BlockCipherAlgorithm._

def importKey[A <: CipherAlgorithm[_]](key: ByteVector, algorithm: A): F[SecretKey[A]] =
F.pure(SecretKeySpec(key, algorithm))

// TODO: Macro
private def aesGcmName(keyLength: AES.KeyLength): String =
keyLength.value match {
case 128 => "aes-128-gcm"
case 192 => "aes-192-gcm"
case 256 => "aes-256-gcm"
}

private def aesCbcName(keyLength: AES.KeyLength): String =
keyLength.value match {
case 128 => "aes-128-cbc"
case 192 => "aes-192-cbc"
case 256 => "aes-256-cbc"
}

override def encrypt[P <: CipherParams, A <: CipherAlgorithm[P]](
key: SecretKey[A],
params: P,
data: ByteVector): F[ByteVector] = {
key match {
case SecretKeySpec(key, algorithm) =>
try {
val bytes = (algorithm, params) match {
case (gcm: AES.GCM, AES.GCM.Params(iv, padding, tagLength, ad)) =>
val name = aesGcmName(gcm.keyLength)
if (!ciphers.contains(name)) {
throw new UnsupportedAlgorithm(name)
}
val cipher = crypto
.createCipheriv(
name,
key.toUint8Array,
iv.data.toUint8Array,
new CipherOptions {
val authTagLength = tagLength.byteLength
}
)
.setAutoPadding(padding)
.setAAD(ad.toUint8Array)
val cipherText = cipher.update(data.toUint8Array)
ByteVector.view(cipherText) ++ ByteVector.view(cipher.`final`()) ++ ByteVector
.view(cipher.getAuthTag())
case (cbc: AES.CBC, AES.CBC.Params(iv, padding)) =>
val name = aesCbcName(cbc.keyLength)
if (!ciphers.contains(name)) {
throw new UnsupportedAlgorithm(name)
}
val cipher = crypto
.createCipheriv(
name,
key.toUint8Array,
iv.data.toUint8Array
)
.setAutoPadding(padding)
val cipherText = cipher.update(data.toUint8Array)
ByteVector.view(cipherText) ++ ByteVector.view(cipher.`final`())
}
F.pure(bytes)
} catch {
case e: GeneralSecurityException => F.raiseError(e)
}
}
}

def decrypt[P <: CipherParams, A <: CipherAlgorithm[P]](
key: SecretKey[A],
params: P,
data: ByteVector): F[ByteVector] = ???
}

private[bobcats] trait CipherCompanionPlatform {

private[bobcats] def forCryptoCiphers[F[_]: Sync](ciphers: js.Array[String]): Cipher[F] =
new CryptoCipher(ciphers)

private[bobcats] def forSubtleCrypto[F[_]: Async]: Cipher[F] = new SubtleCryptoCipher

}
8 changes: 8 additions & 0 deletions core/js/src/main/scala/bobcats/CryptoPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,22 @@ private[bobcats] trait CryptoCompanionPlatform {
def forAsync[F[_]](implicit F: Async[F]): Resource[F, Crypto[F]] = {
Resource.pure(
if (facade.isNodeJSRuntime) {

// TODO: Fix
import facade.node.crypto

val ciphers = crypto.getCiphers()

new UnsealedCrypto[F] {
override def hash: Hash[F] = Hash.forSyncNodeJS
override def hmac: Hmac[F] = Hmac.forAsyncNodeJS
override def cipher: Cipher[F] = Cipher.forCryptoCiphers(ciphers)
}
} else {
new UnsealedCrypto[F] {
override def hash: Hash[F] = Hash.forAsyncSubtleCrypto
override def hmac: Hmac[F] = Hmac.forAsyncSubtleCrypto
override def cipher: Cipher[F] = Cipher.forSubtleCrypto
}
}
)
Expand Down
69 changes: 69 additions & 0 deletions core/js/src/main/scala/bobcats/facade/browser/Cipher.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Copyright 2021 Typelevel
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package bobcats.facade.browser

import scala.scalajs.js

private[bobcats] sealed trait Algorithm extends js.Object

// private[bobcats] object Algorithm {
// def toWebCrypto[A <: CipherAlgorithm](iv: IvParameterSpec[A]) =
// iv.algorithm match {
// case CipherAlgorithm.AESCBC256(PaddingMode.None) =>
// throw new UnsupportedOperationException
// case CipherAlgorithm.AESGCM256(PaddingMode.None) =>
// throw new UnsupportedOperationException
// case CipherAlgorithm.AESCBC256(_) =>
// AesCbcParams(iv.initializationVector.toUint8Array.buffer)
// case CipherAlgorithm.AESGCM256(_) =>
// AesGcmParams(iv.initializationVector.toUint8Array.buffer)
// }
// }

private[bobcats] trait AesCbcParams extends Algorithm {
val name: String
val iv: js.typedarray.ArrayBuffer
}

private[bobcats] object AesCbcParams {
def apply(_iv: js.typedarray.ArrayBuffer): AesCbcParams =
new AesCbcParams {
val name = "AES-CBC"
val iv = _iv
}
}

trait AesGcmParams extends Algorithm {
val name: String
val iv: js.typedarray.ArrayBuffer
val additionalData: js.UndefOr[js.typedarray.ArrayBuffer] = js.undefined
val tagLength: js.UndefOr[Int] = js.undefined
}

private[bobcats] object AesGcmParams {
def apply(
_iv: js.typedarray.ArrayBuffer,
_additionalData: js.UndefOr[js.typedarray.ArrayBuffer] = js.undefined,
_tagLength: js.UndefOr[Int] = js.undefined
): AesGcmParams =
new AesGcmParams {
val name = "AES-GCM"
val iv = _iv
override val additionalData = _additionalData
override val tagLength = _tagLength
}
}
6 changes: 6 additions & 0 deletions core/js/src/main/scala/bobcats/facade/browser/CryptoKey.scala
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,9 @@ import scala.scalajs.js

@js.native
private[bobcats] trait CryptoKey extends js.Any

@js.native
private[bobcats] trait HmacCryptoKey extends CryptoKey

@js.native
private[bobcats] trait AesCryptoKey extends CryptoKey
Loading