├── .github └── workflows │ ├── ci.yml │ └── clean.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE.txt ├── README.md ├── build.sbt ├── core ├── js │ └── src │ │ └── main │ │ └── scala │ │ └── bobcats │ │ ├── CryptoPlatform.scala │ │ ├── Hash1Platform.scala │ │ ├── HashPlatform.scala │ │ ├── HmacPlatform.scala │ │ ├── KeyPlatform.scala │ │ ├── SecureEqPlatform.scala │ │ ├── SecurityException.scala │ │ └── facade │ │ ├── browser │ │ ├── CryptoKey.scala │ │ ├── Hmac.scala │ │ ├── SubtleCrypto.scala │ │ └── crypto.scala │ │ ├── node │ │ ├── Hash.scala │ │ ├── Hmac.scala │ │ ├── KeyObject.scala │ │ ├── crypto.scala │ │ └── package.scala │ │ └── package.scala ├── jvm │ └── src │ │ └── main │ │ └── scala │ │ └── bobcats │ │ ├── CryptoPlatform.scala │ │ ├── Hash1Platform.scala │ │ ├── HashPlatform.scala │ │ ├── HmacPlatform.scala │ │ ├── KeyPlatform.scala │ │ ├── Providers.scala │ │ ├── SecureEqPlatform.scala │ │ └── package.scala ├── native │ └── src │ │ └── main │ │ └── scala │ │ └── bobcats │ │ ├── Context.scala │ │ ├── CryptoPlatform.scala │ │ ├── Hash1Platform.scala │ │ ├── HashPlatform.scala │ │ ├── HmacPlatform.scala │ │ ├── KeyPlatform.scala │ │ ├── SecureEqPlatform.scala │ │ ├── exceptions.scala │ │ ├── openssl │ │ ├── crypto.scala │ │ ├── err.scala │ │ ├── evp.scala │ │ ├── package.scala │ │ └── params.scala │ │ └── package.scala └── shared │ └── src │ ├── main │ └── scala │ │ └── bobcats │ │ ├── Algorithm.scala │ │ ├── Crypto.scala │ │ ├── Hash.scala │ │ ├── Hash1.scala │ │ ├── Hmac.scala │ │ ├── Hotp.scala │ │ ├── Key.scala │ │ └── SecureEq.scala │ └── test │ └── scala │ └── bobcats │ ├── CryptoSuite.scala │ ├── HashSuite.scala │ ├── HmacSuite.scala │ ├── HotpSuite.scala │ └── SecureEqSuite.scala ├── flake.lock ├── flake.nix └── project ├── JSEnv.scala ├── build.properties └── plugins.sbt /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Continuous Integration 9 | 10 | on: 11 | pull_request: 12 | branches: ['**', '!update/**', '!pr/**'] 13 | push: 14 | branches: ['**', '!update/**', '!pr/**'] 15 | tags: [v*] 16 | 17 | env: 18 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 19 | 20 | 21 | concurrency: 22 | group: ${{ github.workflow }} @ ${{ github.ref }} 23 | cancel-in-progress: true 24 | 25 | jobs: 26 | build: 27 | name: Test 28 | strategy: 29 | matrix: 30 | os: [ubuntu-22.04] 31 | scala: [3.3.6, 2.12.20, 2.13.16] 32 | java: [temurin@8] 33 | jsenv: [NodeJS, Chrome, Firefox] 34 | project: [rootJS, rootJVM, rootNative] 35 | exclude: 36 | - scala: 3.3.6 37 | jsenv: Chrome 38 | - scala: 3.3.6 39 | jsenv: Firefox 40 | - scala: 2.12.20 41 | jsenv: Chrome 42 | - scala: 2.12.20 43 | jsenv: Firefox 44 | - project: rootJVM 45 | jsenv: Chrome 46 | - project: rootJVM 47 | jsenv: Firefox 48 | runs-on: ${{ matrix.os }} 49 | timeout-minutes: 60 50 | steps: 51 | - name: Checkout current branch (full) 52 | uses: actions/checkout@v4 53 | with: 54 | fetch-depth: 0 55 | 56 | - name: Setup sbt 57 | uses: sbt/setup-sbt@v1 58 | 59 | - name: Setup Java (temurin@8) 60 | id: setup-java-temurin-8 61 | if: matrix.java == 'temurin@8' 62 | uses: actions/setup-java@v4 63 | with: 64 | distribution: temurin 65 | java-version: 8 66 | cache: sbt 67 | 68 | - name: sbt update 69 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 70 | run: sbt +update 71 | 72 | - name: Setup NodeJS v16 LTS 73 | if: matrix.project == 'rootJS' && matrix.jsenv == 'NodeJS' 74 | uses: actions/setup-node@v3 75 | with: 76 | node-version: 16 77 | 78 | - name: Check that workflows are up to date 79 | run: sbt githubWorkflowCheck 80 | 81 | - name: Check headers and formatting 82 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' && matrix.jsenv == 'NodeJS' 83 | run: 'sbt ''project ${{ matrix.project }}'' ''++ ${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' headerCheckAll scalafmtCheckAll ''project /'' scalafmtSbtCheck' 84 | 85 | - name: scalaJSLink 86 | if: matrix.project == 'rootJS' 87 | run: 'sbt ''project ${{ matrix.project }}'' ''++ ${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' Test/scalaJSLinkerResult' 88 | 89 | - name: nativeLink 90 | if: matrix.project == 'rootNative' 91 | run: 'sbt ''project ${{ matrix.project }}'' ''++ ${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' Test/nativeLink' 92 | 93 | - name: Test 94 | run: 'sbt ''project ${{ matrix.project }}'' ''++ ${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' test' 95 | 96 | - name: Check binary compatibility 97 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' && matrix.jsenv == 'NodeJS' 98 | run: 'sbt ''project ${{ matrix.project }}'' ''++ ${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' mimaReportBinaryIssues' 99 | 100 | - name: Generate API documentation 101 | if: matrix.java == 'temurin@8' && matrix.os == 'ubuntu-22.04' && matrix.jsenv == 'NodeJS' 102 | run: 'sbt ''project ${{ matrix.project }}'' ''++ ${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' doc' 103 | 104 | - name: Make target directories 105 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 106 | run: mkdir -p core/native/target core/js/target core/jvm/target project/target 107 | 108 | - name: Compress target directories 109 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 110 | run: tar cf targets.tar core/native/target core/js/target core/jvm/target project/target 111 | 112 | - name: Upload target directories 113 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 114 | uses: actions/upload-artifact@v4 115 | with: 116 | name: target-${{ matrix.os }}-${{ matrix.java }}-${{ matrix.scala }}-${{ matrix.jsenv }}-${{ matrix.project }} 117 | path: targets.tar 118 | 119 | publish: 120 | name: Publish Artifacts 121 | needs: [build] 122 | if: github.event_name != 'pull_request' && (startsWith(github.ref, 'refs/tags/v') || github.ref == 'refs/heads/main') 123 | strategy: 124 | matrix: 125 | os: [ubuntu-22.04] 126 | java: [temurin@8] 127 | runs-on: ${{ matrix.os }} 128 | steps: 129 | - name: Checkout current branch (full) 130 | uses: actions/checkout@v4 131 | with: 132 | fetch-depth: 0 133 | 134 | - name: Setup sbt 135 | uses: sbt/setup-sbt@v1 136 | 137 | - name: Setup Java (temurin@8) 138 | id: setup-java-temurin-8 139 | if: matrix.java == 'temurin@8' 140 | uses: actions/setup-java@v4 141 | with: 142 | distribution: temurin 143 | java-version: 8 144 | cache: sbt 145 | 146 | - name: sbt update 147 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 148 | run: sbt +update 149 | 150 | - name: Download target directories (3.3.6, NodeJS, rootJS) 151 | uses: actions/download-artifact@v4 152 | with: 153 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.6-NodeJS-rootJS 154 | 155 | - name: Inflate target directories (3.3.6, NodeJS, rootJS) 156 | run: | 157 | tar xf targets.tar 158 | rm targets.tar 159 | 160 | - name: Download target directories (3.3.6, NodeJS, rootJVM) 161 | uses: actions/download-artifact@v4 162 | with: 163 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.6-NodeJS-rootJVM 164 | 165 | - name: Inflate target directories (3.3.6, NodeJS, rootJVM) 166 | run: | 167 | tar xf targets.tar 168 | rm targets.tar 169 | 170 | - name: Download target directories (3.3.6, NodeJS, rootNative) 171 | uses: actions/download-artifact@v4 172 | with: 173 | name: target-${{ matrix.os }}-${{ matrix.java }}-3.3.6-NodeJS-rootNative 174 | 175 | - name: Inflate target directories (3.3.6, NodeJS, rootNative) 176 | run: | 177 | tar xf targets.tar 178 | rm targets.tar 179 | 180 | - name: Download target directories (2.12.20, NodeJS, rootJS) 181 | uses: actions/download-artifact@v4 182 | with: 183 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20-NodeJS-rootJS 184 | 185 | - name: Inflate target directories (2.12.20, NodeJS, rootJS) 186 | run: | 187 | tar xf targets.tar 188 | rm targets.tar 189 | 190 | - name: Download target directories (2.12.20, NodeJS, rootJVM) 191 | uses: actions/download-artifact@v4 192 | with: 193 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20-NodeJS-rootJVM 194 | 195 | - name: Inflate target directories (2.12.20, NodeJS, rootJVM) 196 | run: | 197 | tar xf targets.tar 198 | rm targets.tar 199 | 200 | - name: Download target directories (2.12.20, NodeJS, rootNative) 201 | uses: actions/download-artifact@v4 202 | with: 203 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.12.20-NodeJS-rootNative 204 | 205 | - name: Inflate target directories (2.12.20, NodeJS, rootNative) 206 | run: | 207 | tar xf targets.tar 208 | rm targets.tar 209 | 210 | - name: Download target directories (2.13.16, NodeJS, rootJS) 211 | uses: actions/download-artifact@v4 212 | with: 213 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.16-NodeJS-rootJS 214 | 215 | - name: Inflate target directories (2.13.16, NodeJS, rootJS) 216 | run: | 217 | tar xf targets.tar 218 | rm targets.tar 219 | 220 | - name: Download target directories (2.13.16, NodeJS, rootJVM) 221 | uses: actions/download-artifact@v4 222 | with: 223 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.16-NodeJS-rootJVM 224 | 225 | - name: Inflate target directories (2.13.16, NodeJS, rootJVM) 226 | run: | 227 | tar xf targets.tar 228 | rm targets.tar 229 | 230 | - name: Download target directories (2.13.16, NodeJS, rootNative) 231 | uses: actions/download-artifact@v4 232 | with: 233 | name: target-${{ matrix.os }}-${{ matrix.java }}-2.13.16-NodeJS-rootNative 234 | 235 | - name: Inflate target directories (2.13.16, NodeJS, rootNative) 236 | run: | 237 | tar xf targets.tar 238 | rm targets.tar 239 | 240 | - name: Import signing key 241 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE == '' 242 | env: 243 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 244 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 245 | run: echo $PGP_SECRET | base64 -d -i - | gpg --import 246 | 247 | - name: Import signing key and strip passphrase 248 | if: env.PGP_SECRET != '' && env.PGP_PASSPHRASE != '' 249 | env: 250 | PGP_SECRET: ${{ secrets.PGP_SECRET }} 251 | PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} 252 | run: | 253 | echo "$PGP_SECRET" | base64 -d -i - > /tmp/signing-key.gpg 254 | echo "$PGP_PASSPHRASE" | gpg --pinentry-mode loopback --passphrase-fd 0 --import /tmp/signing-key.gpg 255 | (echo "$PGP_PASSPHRASE"; echo; echo) | gpg --command-fd 0 --pinentry-mode loopback --change-passphrase $(gpg --list-secret-keys --with-colons 2> /dev/null | grep '^sec:' | cut --delimiter ':' --fields 5 | tail -n 1) 256 | 257 | - name: Publish 258 | env: 259 | SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} 260 | SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} 261 | SONATYPE_CREDENTIAL_HOST: ${{ secrets.SONATYPE_CREDENTIAL_HOST }} 262 | run: sbt tlCiRelease 263 | 264 | dependency-submission: 265 | name: Submit Dependencies 266 | if: github.event.repository.fork == false && github.event_name != 'pull_request' 267 | strategy: 268 | matrix: 269 | os: [ubuntu-22.04] 270 | java: [temurin@8] 271 | runs-on: ${{ matrix.os }} 272 | steps: 273 | - name: Checkout current branch (full) 274 | uses: actions/checkout@v4 275 | with: 276 | fetch-depth: 0 277 | 278 | - name: Setup sbt 279 | uses: sbt/setup-sbt@v1 280 | 281 | - name: Setup Java (temurin@8) 282 | id: setup-java-temurin-8 283 | if: matrix.java == 'temurin@8' 284 | uses: actions/setup-java@v4 285 | with: 286 | distribution: temurin 287 | java-version: 8 288 | cache: sbt 289 | 290 | - name: sbt update 291 | if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' 292 | run: sbt +update 293 | 294 | - name: Submit Dependencies 295 | uses: scalacenter/sbt-dependency-submission@v2 296 | with: 297 | modules-ignore: rootjs_3 rootjs_2.12 rootjs_2.13 rootjvm_3 rootjvm_2.12 rootjvm_2.13 rootnative_3 rootnative_2.12 rootnative_2.13 testruntime_3 testruntime_2.12 testruntime_2.13 testruntime_sjs1_3 testruntime_sjs1_2.12 testruntime_sjs1_2.13 testruntime_native0.4_3 testruntime_native0.4_2.12 testruntime_native0.4_2.13 298 | configs-ignore: test scala-tool scala-doc-tool test-internal 299 | -------------------------------------------------------------------------------- /.github/workflows/clean.yml: -------------------------------------------------------------------------------- 1 | # This file was automatically generated by sbt-github-actions using the 2 | # githubWorkflowGenerate task. You should add and commit this file to 3 | # your git repository. It goes without saying that you shouldn't edit 4 | # this file by hand! Instead, if you wish to make changes, you should 5 | # change your sbt build configuration to revise the workflow description 6 | # to meet your needs, then regenerate this file. 7 | 8 | name: Clean 9 | 10 | on: push 11 | 12 | jobs: 13 | delete-artifacts: 14 | name: Delete Artifacts 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | steps: 19 | - name: Delete artifacts 20 | run: | 21 | # Customize those three lines with your repository and credentials: 22 | REPO=${GITHUB_API_URL}/repos/${{ github.repository }} 23 | 24 | # A shortcut to call GitHub API. 25 | ghapi() { curl --silent --location --user _:$GITHUB_TOKEN "$@"; } 26 | 27 | # A temporary file which receives HTTP response headers. 28 | TMPFILE=/tmp/tmp.$$ 29 | 30 | # An associative array, key: artifact name, value: number of artifacts of that name. 31 | declare -A ARTCOUNT 32 | 33 | # Process all artifacts on this repository, loop on returned "pages". 34 | URL=$REPO/actions/artifacts 35 | while [[ -n "$URL" ]]; do 36 | 37 | # Get current page, get response headers in a temporary file. 38 | JSON=$(ghapi --dump-header $TMPFILE "$URL") 39 | 40 | # Get URL of next page. Will be empty if we are at the last page. 41 | URL=$(grep '^Link:' "$TMPFILE" | tr ',' '\n' | grep 'rel="next"' | head -1 | sed -e 's/.*.*//') 42 | rm -f $TMPFILE 43 | 44 | # Number of artifacts on this page: 45 | COUNT=$(( $(jq <<<$JSON -r '.artifacts | length') )) 46 | 47 | # Loop on all artifacts on this page. 48 | for ((i=0; $i < $COUNT; i++)); do 49 | 50 | # Get name of artifact and count instances of this name. 51 | name=$(jq <<<$JSON -r ".artifacts[$i].name?") 52 | ARTCOUNT[$name]=$(( $(( ${ARTCOUNT[$name]} )) + 1)) 53 | 54 | id=$(jq <<<$JSON -r ".artifacts[$i].id?") 55 | size=$(( $(jq <<<$JSON -r ".artifacts[$i].size_in_bytes?") )) 56 | printf "Deleting '%s' #%d, %'d bytes\n" $name ${ARTCOUNT[$name]} $size 57 | ghapi -X DELETE $REPO/actions/artifacts/$id 58 | done 59 | done 60 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | 3 | .sbtopts 4 | 5 | # vim 6 | *.sw? 7 | 8 | # intellij 9 | .idea/ 10 | 11 | # Ignore [ce]tags files 12 | tags 13 | 14 | # Metals 15 | .metals/ 16 | .bsp/ 17 | .bloop/ 18 | metals.sbt 19 | .vscode 20 | 21 | # npm 22 | node_modules/ 23 | -------------------------------------------------------------------------------- /.scalafmt.conf: -------------------------------------------------------------------------------- 1 | version = 3.8.1 2 | 3 | runner.dialect = Scala213Source3 4 | 5 | maxColumn = 96 6 | 7 | includeCurlyBraceInSelectChains = true 8 | includeNoParensInSelectChains = true 9 | 10 | optIn { 11 | breakChainOnFirstMethodDot = false 12 | forceBlankLineBeforeDocstring = true 13 | } 14 | 15 | binPack { 16 | literalArgumentLists = true 17 | parentConstructors = Never 18 | } 19 | 20 | danglingParentheses { 21 | defnSite = false 22 | callSite = false 23 | ctrlSite = false 24 | 25 | exclude = [] 26 | } 27 | 28 | newlines { 29 | beforeCurlyLambdaParams = multilineWithCaseOnly 30 | afterCurlyLambda = squash 31 | implicitParamListModifierPrefer = before 32 | sometimesBeforeColonInMethodReturnType = true 33 | } 34 | 35 | align.preset = none 36 | align.stripMargin = true 37 | 38 | assumeStandardLibraryStripMargin = true 39 | 40 | docstrings { 41 | style = Asterisk 42 | oneline = unfold 43 | } 44 | 45 | project.git = true 46 | 47 | trailingCommas = never 48 | 49 | rewrite { 50 | // RedundantBraces honestly just doesn't work, otherwise I'd love to use it 51 | rules = [PreferCurlyFors, RedundantParens, SortImports] 52 | 53 | redundantBraces { 54 | maxLines = 1 55 | stringInterpolation = true 56 | } 57 | } 58 | 59 | rewriteTokens { 60 | "⇒": "=>" 61 | "→": "->" 62 | "←": "<-" 63 | } 64 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # bobcats 2 | 3 | Cross-platform cryptography (JVM, Node.js, browsers) for the Cats ecosystem. 4 | To learn more about the concept please see [http4s/http4s#5044](https://github.com/http4s/http4s/issues/5044). 5 | 6 | ```scala 7 | libraryDependencies += "org.typelevel" %%% "bobcats" % "0.1-a404b48" // or latest hash on main branch 8 | ``` 9 | -------------------------------------------------------------------------------- /build.sbt: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | import org.openqa.selenium.WebDriver 18 | import org.openqa.selenium.chrome.{ChromeDriver, ChromeOptions} 19 | import org.openqa.selenium.firefox.{FirefoxOptions, FirefoxProfile} 20 | import org.openqa.selenium.remote.server.{DriverFactory, DriverProvider} 21 | import org.scalajs.jsenv.selenium.SeleniumJSEnv 22 | 23 | import JSEnv._ 24 | name := "bobcats" 25 | 26 | ThisBuild / tlBaseVersion := "0.1" 27 | ThisBuild / tlUntaggedAreSnapshots := false 28 | 29 | ThisBuild / developers := List( 30 | tlGitHubDev("armanbilge", "Arman Bilge") 31 | ) 32 | ThisBuild / startYear := Some(2021) 33 | 34 | ThisBuild / crossScalaVersions := Seq("3.3.6", "2.12.20", "2.13.16") 35 | 36 | ThisBuild / githubWorkflowBuildPreamble ++= Seq( 37 | WorkflowStep.Use( 38 | UseRef.Public("actions", "setup-node", "v3"), 39 | name = Some("Setup NodeJS v16 LTS"), 40 | params = Map("node-version" -> "16"), 41 | cond = Some("matrix.project == 'rootJS' && matrix.jsenv == 'NodeJS'") 42 | ) 43 | ) 44 | 45 | val jsenvs = List(NodeJS, Chrome, Firefox).map(_.toString) 46 | ThisBuild / githubWorkflowBuildMatrixAdditions += "jsenv" -> jsenvs 47 | ThisBuild / githubWorkflowBuildSbtStepPreamble += s"set Global / useJSEnv := JSEnv.$${{ matrix.jsenv }}" 48 | ThisBuild / githubWorkflowBuildMatrixExclusions ++= { 49 | for { 50 | scala <- (ThisBuild / crossScalaVersions).value.init 51 | jsenv <- jsenvs.tail 52 | } yield MatrixExclude(Map("scala" -> scala, "jsenv" -> jsenv)) 53 | } 54 | ThisBuild / githubWorkflowBuildMatrixExclusions ++= { 55 | for { 56 | jsenv <- jsenvs.tail 57 | } yield MatrixExclude(Map("project" -> "rootJVM", "jsenv" -> jsenv)) 58 | } 59 | 60 | ThisBuild / githubWorkflowScalaVersions := (ThisBuild / crossScalaVersions).value 61 | 62 | lazy val useJSEnv = 63 | settingKey[JSEnv]("Use Node.js or a headless browser for running Scala.js tests") 64 | Global / useJSEnv := NodeJS 65 | 66 | ThisBuild / Test / jsEnv := { 67 | val old = (Test / jsEnv).value 68 | 69 | useJSEnv.value match { 70 | case NodeJS => old 71 | case Firefox => 72 | val options = new FirefoxOptions() 73 | options.setHeadless(true) 74 | new SeleniumJSEnv(options) 75 | case Chrome => 76 | val options = new ChromeOptions() 77 | options.setHeadless(true) 78 | new SeleniumJSEnv(options) 79 | } 80 | } 81 | 82 | val catsVersion = "2.11.0" 83 | val fs2Version = "3.12.0" 84 | val catsEffectVersion = "3.6.1" 85 | val scodecBitsVersion = "1.1.38" 86 | val munitVersion = "1.0.0" 87 | val munitCEVersion = "2.0.0-M3" 88 | val disciplineMUnitVersion = "2.0.0-M3" 89 | 90 | lazy val root = tlCrossRootProject.aggregate(core, testRuntime) 91 | 92 | lazy val core = crossProject(JSPlatform, JVMPlatform, NativePlatform) 93 | .in(file("core")) 94 | .settings( 95 | name := "bobcats", 96 | libraryDependencies ++= Seq( 97 | "org.typelevel" %%% "cats-core" % catsVersion, 98 | "org.typelevel" %%% "cats-effect-kernel" % catsEffectVersion, 99 | "org.scodec" %%% "scodec-bits" % scodecBitsVersion, 100 | "co.fs2" %%% "fs2-core" % fs2Version, 101 | "org.scalameta" %%% "munit" % munitVersion % Test, 102 | "org.typelevel" %%% "cats-laws" % catsVersion % Test, 103 | "org.typelevel" %%% "cats-effect" % catsEffectVersion % Test, 104 | "org.typelevel" %%% "discipline-munit" % disciplineMUnitVersion % Test, 105 | "org.typelevel" %%% "munit-cats-effect" % munitCEVersion % Test 106 | ), 107 | scalacOptions ++= { 108 | if (scalaVersion.value.startsWith("2.12")) Seq("-Wconf:cat=unused:s") else Seq.empty 109 | } 110 | ) 111 | .dependsOn(testRuntime % Test) 112 | 113 | lazy val testRuntime = crossProject(JSPlatform, JVMPlatform, NativePlatform) 114 | .crossType(CrossType.Pure) 115 | .in(file("test-runtime")) 116 | .enablePlugins(BuildInfoPlugin, NoPublishPlugin) 117 | .settings( 118 | buildInfoPackage := "bobcats" 119 | ) 120 | .jvmSettings( 121 | buildInfoKeys := Seq( 122 | BuildInfoKey.sbtbuildinfoConstantEntry("runtime" -> "JVM") 123 | ) 124 | ) 125 | .jsSettings( 126 | buildInfoKeys := Seq( 127 | BuildInfoKey("runtime" -> useJSEnv.value.toString) 128 | ) 129 | ) 130 | .nativeSettings( 131 | buildInfoKeys := Seq( 132 | BuildInfoKey.sbtbuildinfoConstantEntry("runtime" -> "Native") 133 | ) 134 | ) 135 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/CryptoPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.{Async, Resource} 20 | 21 | private[bobcats] trait CryptoCompanionPlatform { 22 | def forAsync[F[_]](implicit F: Async[F]): Resource[F, Crypto[F]] = { 23 | Resource.pure( 24 | if (facade.isNodeJSRuntime) { 25 | new UnsealedCrypto[F] { 26 | override def hash: Hash[F] = Hash.forSyncNodeJS 27 | override def hmac: Hmac[F] = Hmac.forAsyncNodeJS 28 | } 29 | } else { 30 | new UnsealedCrypto[F] { 31 | override def hash: Hash[F] = Hash.forAsyncSubtleCrypto 32 | override def hmac: Hmac[F] = Hmac.forAsyncSubtleCrypto 33 | } 34 | } 35 | ) 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/Hash1Platform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.syntax.all._ 20 | import cats.effect.{Async, Resource, Sync} 21 | import scodec.bits.ByteVector 22 | import fs2.{Chunk, Pipe, Stream} 23 | 24 | private[bobcats] final class SubtleCryptoDigest[F[_]](algorithm: String)(implicit F: Async[F]) 25 | extends UnsealedHash1[F] { 26 | 27 | import facade.browser._ 28 | 29 | override def digest(data: ByteVector): F[ByteVector] = 30 | F.fromPromise(F.delay(crypto.subtle.digest(algorithm, data.toUint8Array.buffer))) 31 | .map(ByteVector.view) 32 | 33 | override def pipe: Pipe[F, Byte, Byte] = throw new UnsupportedOperationException( 34 | "Browsers do not support streaming") 35 | 36 | } 37 | 38 | private final class NodeCryptoDigest[F[_]](algorithm: String)(implicit F: Sync[F]) 39 | extends UnsealedHash1[F] { 40 | 41 | override def digest(data: ByteVector): F[ByteVector] = 42 | F.pure { 43 | // Assume we've checked the algorithm already 44 | val hash = facade.node.crypto.createHash(algorithm) 45 | hash.update(data.toUint8Array) 46 | ByteVector.view(hash.digest()) 47 | } 48 | 49 | override val pipe: Pipe[F, Byte, Byte] = 50 | in => 51 | Stream.eval(F.delay(facade.node.crypto.createHash(algorithm))).flatMap { hash => 52 | in.chunks 53 | .fold(hash) { (h, d) => 54 | h.update(d.toUint8Array) 55 | h 56 | } 57 | .flatMap(h => Stream.chunk(Chunk.uint8Array(h.digest()))) 58 | } 59 | } 60 | 61 | private[bobcats] trait Hash1CompanionPlatform { 62 | private[bobcats] def fromJSCryptoName[F[_]](alg: String)(implicit F: Sync[F]): F[Hash1[F]] = 63 | if (facade.node.crypto.getHashes().contains(alg)) { 64 | F.pure(new NodeCryptoDigest(alg)(F)) 65 | } else { 66 | F.raiseError(new NoSuchAlgorithmException(s"${alg} MessageDigest not available")) 67 | } 68 | 69 | def forAsync[F[_]: Async](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = 70 | if (facade.isNodeJSRuntime) Resource.eval(fromJSCryptoName(algorithm.toStringNodeJS)) 71 | else { 72 | // SubtleCrypto does not have a way of checking the supported hashes 73 | Resource.pure(new SubtleCryptoDigest(algorithm.toStringWebCrypto)) 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/HashPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.{Async, Resource, Sync} 20 | import cats.syntax.all._ 21 | import scodec.bits.ByteVector 22 | import fs2.{Pipe, Stream} 23 | 24 | private[bobcats] trait HashCompanionPlatform { 25 | private[bobcats] def forSyncNodeJS[F[_]: Sync]: Hash[F] = 26 | new UnsealedHash[F] { 27 | override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = 28 | in => 29 | Stream.eval(Hash1.fromJSCryptoName(algorithm.toStringNodeJS)).flatMap { hash => 30 | in.through(hash.pipe) 31 | } 32 | 33 | override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = 34 | Hash1.fromJSCryptoName(algorithm.toStringNodeJS).flatMap(_.digest(data)) 35 | } 36 | 37 | private[bobcats] def forAsyncSubtleCrypto[F[_]: Async]: Hash[F] = 38 | new UnsealedHash[F] { 39 | override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = 40 | new SubtleCryptoDigest(algorithm.toStringWebCrypto).pipe 41 | 42 | override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = 43 | new SubtleCryptoDigest(algorithm.toStringWebCrypto).digest(data) 44 | } 45 | 46 | def forAsync[F[_]: Async]: Resource[F, Hash[F]] = 47 | Resource.pure(if (facade.isNodeJSRuntime) forSyncNodeJS else forAsyncSubtleCrypto) 48 | } 49 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/HmacPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.Async 20 | import cats.syntax.all._ 21 | import scodec.bits.ByteVector 22 | 23 | import scala.scalajs.js 24 | import java.lang 25 | 26 | private[bobcats] trait HmacPlatform[F[_]] 27 | 28 | private[bobcats] trait HmacCompanionPlatform { 29 | private[bobcats] def forAsyncNodeJS[F[_]](implicit F: Async[F]): Hmac[F] = 30 | new UnsealedHmac[F] { 31 | import facade.node._ 32 | override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = 33 | key match { 34 | case SecretKeySpec(key, algorithm) => 35 | F.catchNonFatal { 36 | val hmac = crypto.createHmac(algorithm.toStringNodeJS, key.toUint8Array) 37 | hmac.update(data.toUint8Array) 38 | ByteVector.view(hmac.digest()) 39 | } 40 | case _ => F.raiseError(new InvalidKeyException) 41 | } 42 | 43 | override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = 44 | F.async_[SecretKey[A]] { cb => 45 | crypto.generateKey( 46 | "hmac", 47 | GenerateKeyOptions(algorithm.minimumKeyLength * lang.Byte.SIZE), 48 | (err, key) => 49 | cb( 50 | Option(err) 51 | .map(js.JavaScriptException) 52 | .toLeft(SecretKeySpec(ByteVector.view(key.`export`()), algorithm))) 53 | ) 54 | } 55 | 56 | override def importKey[A <: HmacAlgorithm]( 57 | key: ByteVector, 58 | algorithm: A): F[SecretKey[A]] = 59 | F.pure(SecretKeySpec(key, algorithm)) 60 | } 61 | 62 | private[bobcats] def forAsyncSubtleCrypto[F[_]](implicit F: Async[F]): Hmac[F] = 63 | new UnsealedHmac[F] { 64 | import bobcats.facade.browser._ 65 | override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = 66 | key match { 67 | case SecretKeySpec(key, algorithm) => 68 | for { 69 | key <- F.fromPromise( 70 | F.delay( 71 | crypto 72 | .subtle 73 | .importKey( 74 | "raw", 75 | key.toUint8Array, 76 | HmacImportParams(algorithm.toStringWebCrypto), 77 | false, 78 | js.Array("sign")))) 79 | signature <- F.fromPromise( 80 | F.delay(crypto.subtle.sign("HMAC", key, data.toUint8Array.buffer))) 81 | } yield ByteVector.view(signature) 82 | case _ => F.raiseError(new InvalidKeyException) 83 | } 84 | 85 | override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = 86 | for { 87 | key <- F.fromPromise( 88 | F.delay( 89 | crypto 90 | .subtle 91 | .generateKey( 92 | HmacKeyGenParams(algorithm.toStringWebCrypto), 93 | true, 94 | js.Array("sign")))) 95 | exported <- F.fromPromise(F.delay(crypto.subtle.exportKey("raw", key))) 96 | } yield SecretKeySpec(ByteVector.view(exported), algorithm) 97 | 98 | override def importKey[A <: HmacAlgorithm]( 99 | key: ByteVector, 100 | algorithm: A): F[SecretKey[A]] = 101 | F.pure(SecretKeySpec(key, algorithm)) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/KeyPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | private[bobcats] trait KeyPlatform 20 | private[bobcats] trait PublicKeyPlatform 21 | private[bobcats] trait PrivateKeyPlatform 22 | private[bobcats] trait SecretKeyPlatform 23 | private[bobcats] trait SecretKeySpecPlatform[+A <: Algorithm] 24 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/SecureEqPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scodec.bits.ByteVector 20 | 21 | private[bobcats] trait SecureEqCompanionPlatform { this: SecureEq.type => 22 | 23 | implicit lazy val secureEqForByteVector: SecureEq[ByteVector] = 24 | if (facade.isNodeJSRuntime) 25 | new NodeJSSecureEq 26 | else 27 | new ByteVectorSecureEq 28 | 29 | private[this] final class NodeJSSecureEq extends SecureEq[ByteVector] { 30 | import facade.node._ 31 | 32 | override def eqv(x: ByteVector, y: ByteVector): Boolean = 33 | x.length == y.length && crypto.timingSafeEqual(x.toUint8Array, y.toUint8Array) 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/SecurityException.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | class GeneralSecurityException(message: String = null, cause: Throwable = null) 20 | extends Exception(message, cause) 21 | 22 | class NoSuchAlgorithmException(message: String = null, cause: Throwable = null) 23 | extends GeneralSecurityException(message, cause) 24 | 25 | class KeyException(message: String = null, cause: Throwable = null) 26 | extends GeneralSecurityException(message, cause) 27 | 28 | class InvalidKeyException(message: String = null, cause: Throwable = null) 29 | extends KeyException(message, cause) 30 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/browser/CryptoKey.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.browser 18 | 19 | import scala.scalajs.js 20 | 21 | @js.native 22 | private[bobcats] trait CryptoKey extends js.Any 23 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/browser/Hmac.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.browser 18 | 19 | import scala.scalajs.js 20 | 21 | @js.native 22 | private[bobcats] trait HmacCryptoKey extends CryptoKey 23 | 24 | @js.native 25 | private[bobcats] sealed trait HmacImportParams extends js.Any 26 | 27 | private[bobcats] object HmacImportParams { 28 | def apply(hash: String): HmacImportParams = 29 | js.Dynamic 30 | .literal( 31 | name = "HMAC", 32 | hash = hash 33 | ) 34 | .asInstanceOf[HmacImportParams] 35 | } 36 | 37 | @js.native 38 | private[bobcats] sealed trait HmacKeyGenParams extends js.Any 39 | 40 | private[bobcats] object HmacKeyGenParams { 41 | def apply(hash: String): HmacKeyGenParams = 42 | js.Dynamic 43 | .literal( 44 | name = "HMAC", 45 | hash = hash 46 | ) 47 | .asInstanceOf[HmacKeyGenParams] 48 | } 49 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/browser/SubtleCrypto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.browser 18 | 19 | import scala.scalajs.js 20 | 21 | @js.native 22 | private[bobcats] trait SubtleCrypto extends js.Any { 23 | 24 | def digest( 25 | algorithm: String, 26 | data: js.typedarray.ArrayBuffer): js.Promise[js.typedarray.ArrayBuffer] = js.native 27 | 28 | def exportKey(format: String, key: CryptoKey): js.Promise[js.typedarray.ArrayBuffer] = 29 | js.native 30 | 31 | def generateKey( 32 | algorithm: HmacKeyGenParams, 33 | extractable: Boolean, 34 | keyUsages: js.Array[String]): js.Promise[HmacCryptoKey] = js.native 35 | 36 | def importKey( 37 | format: String, 38 | keyData: js.typedarray.Uint8Array, 39 | algorithm: HmacImportParams, 40 | extractable: Boolean, 41 | keyUsages: js.Array[String] 42 | ): js.Promise[HmacCryptoKey] = js.native 43 | 44 | def sign( 45 | algorithm: String, 46 | key: CryptoKey, 47 | data: js.typedarray.ArrayBuffer): js.Promise[js.typedarray.ArrayBuffer] = js.native 48 | 49 | } 50 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/browser/crypto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.browser 18 | 19 | import scala.scalajs.js 20 | import scala.scalajs.js.annotation.JSGlobal 21 | 22 | @js.native 23 | @JSGlobal 24 | private[bobcats] object crypto extends js.Any { 25 | 26 | def subtle: SubtleCrypto = js.native 27 | 28 | def getRandomValues(typedArray: js.typedarray.Uint8Array): js.typedarray.Uint8Array = 29 | js.native 30 | 31 | } 32 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/node/Hash.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.node 18 | 19 | import scala.scalajs.js 20 | 21 | @js.native 22 | private[bobcats] trait Hash extends js.Any { 23 | 24 | def digest(): js.typedarray.Uint8Array = js.native 25 | 26 | def update(data: js.typedarray.Uint8Array): Unit = js.native 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/node/Hmac.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.node 18 | 19 | import scala.scalajs.js 20 | 21 | @js.native 22 | private[bobcats] trait Hmac extends js.Any { 23 | 24 | def digest(): js.typedarray.Uint8Array = js.native 25 | 26 | def update(data: js.typedarray.Uint8Array): Unit = js.native 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/node/KeyObject.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.node 18 | 19 | import scala.scalajs.js 20 | 21 | @js.native 22 | private[bobcats] trait SymmetricKeyObject extends js.Any { 23 | def `export`(): js.typedarray.Uint8Array = js.native 24 | } 25 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/node/crypto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade.node 18 | 19 | import scala.scalajs.js 20 | 21 | // https://nodejs.org/api/crypto.html 22 | @js.native 23 | private[bobcats] trait crypto extends js.Any { 24 | 25 | def getHashes(): js.Array[String] = js.native 26 | 27 | def createHash(algorithm: String): Hash = js.native 28 | 29 | def createHmac(algorithm: String, key: js.typedarray.Uint8Array): Hmac = js.native 30 | 31 | def createSecretKey(key: js.typedarray.Uint8Array): SymmetricKeyObject = js.native 32 | 33 | def generateKey( 34 | `type`: String, 35 | options: GenerateKeyOptions, 36 | callback: js.Function2[js.Error, SymmetricKeyObject, Unit]): Unit = js.native 37 | 38 | def generateKeySync(`type`: String, options: GenerateKeyOptions): SymmetricKeyObject = 39 | js.native 40 | 41 | def randomBytes(size: Int): js.typedarray.Uint8Array = js.native 42 | 43 | def randomBytes( 44 | size: Int, 45 | callback: js.UndefOr[js.Function2[js.Error, js.typedarray.Uint8Array, Unit]]): Unit = 46 | js.native 47 | 48 | def timingSafeEqual( 49 | a: js.typedarray.Uint8Array, 50 | b: js.typedarray.Uint8Array 51 | ): Boolean = js.native 52 | 53 | } 54 | 55 | @js.native 56 | private[bobcats] trait GenerateKeyOptions extends js.Any 57 | private[bobcats] object GenerateKeyOptions { 58 | def apply(length: Int): GenerateKeyOptions = 59 | js.Dynamic.literal(length = length).asInstanceOf[GenerateKeyOptions] 60 | } 61 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/node/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats.facade 18 | 19 | import scala.scalajs.js 20 | 21 | package object node { 22 | 23 | private[bobcats] def crypto: crypto = require[crypto]("crypto") 24 | 25 | private[node] def require[A <: js.Any](module: String): A = 26 | js.Dynamic.global.require(module).asInstanceOf[A] 27 | 28 | } 29 | -------------------------------------------------------------------------------- /core/js/src/main/scala/bobcats/facade/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scala.util.Try 20 | import scala.scalajs.js 21 | 22 | package object facade { 23 | 24 | private[bobcats] val isNodeJSRuntime: Boolean = Try( 25 | js.Dynamic.global.process.release.name.asInstanceOf[String]).toOption.exists(_ == "node") 26 | 27 | @inline private[bobcats] def isBrowserRuntime: Boolean = !isNodeJSRuntime 28 | 29 | } 30 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/CryptoPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.{Async, Resource, Sync} 20 | 21 | private[bobcats] trait CryptoCompanionPlatform { 22 | 23 | def forSync[F[_]](implicit F: Sync[F]): Resource[F, Crypto[F]] = 24 | Resource.eval(F.delay { 25 | val providers = Providers.get() 26 | new UnsealedCrypto[F] { 27 | override def hash: Hash[F] = Hash.forProviders(providers) 28 | override def hmac: Hmac[F] = Hmac.forProviders(providers) 29 | } 30 | }) 31 | 32 | def forAsync[F[_]: Async]: Resource[F, Crypto[F]] = forSync 33 | } 34 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/Hash1Platform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import java.security.{MessageDigest, Provider} 20 | import scodec.bits.ByteVector 21 | import cats.effect.{Async, Resource, Sync} 22 | import fs2.{Chunk, Pipe, Stream} 23 | 24 | private final class JavaSecurityDigest[F[_]](algorithm: String, provider: Provider)( 25 | implicit F: Sync[F]) 26 | extends UnsealedHash1[F] { 27 | 28 | override def digest(data: ByteVector): F[ByteVector] = F.pure { 29 | val h = MessageDigest.getInstance(algorithm, provider) 30 | h.update(data.toByteBuffer) 31 | ByteVector.view(h.digest()) 32 | } 33 | 34 | override val pipe: Pipe[F, Byte, Byte] = 35 | in => 36 | Stream 37 | .eval(F.delay(MessageDigest.getInstance(algorithm, provider))) 38 | .flatMap { digest => 39 | in.chunks.fold(digest) { (h, data) => 40 | h.update(data.toByteBuffer) 41 | h 42 | } 43 | } 44 | .flatMap { h => Stream.chunk(Chunk.array(h.digest())) } 45 | 46 | override def toString = s"JavaSecurityDigest(${algorithm}, ${provider.getName})" 47 | } 48 | 49 | private[bobcats] trait Hash1CompanionPlatform { 50 | 51 | /** 52 | * Get a hash for a specific name used by the Java security providers. 53 | */ 54 | private[bobcats] def fromJavaSecurityName[F[_]](name: String)( 55 | implicit F: Sync[F]): F[Hash1[F]] = 56 | F.delay { 57 | // `Security#getProviders` is a mutable array, so cache the `Provider` 58 | val p = Providers.get().messageDigest(name) match { 59 | case Left(e) => throw e 60 | case Right(p) => p 61 | } 62 | new JavaSecurityDigest(name, p)(F) 63 | } 64 | 65 | def forSync[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = 66 | Resource.eval(fromJavaSecurityName[F](algorithm.toStringJava)) 67 | 68 | def forAsync[F[_]: Async](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = forSync( 69 | algorithm) 70 | 71 | } 72 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/HashPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import fs2.{Pipe, Stream} 20 | import cats.effect.kernel.{Async, Resource, Sync} 21 | import scodec.bits.ByteVector 22 | 23 | private[bobcats] trait HashCompanionPlatform { 24 | 25 | private[bobcats] def forProviders[F[_]](providers: Providers)( 26 | implicit F: Sync[F]): Hash[F] = { 27 | new UnsealedHash[F] { 28 | override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { 29 | val name = algorithm.toStringJava 30 | providers.messageDigest(name) match { 31 | case Left(e) => F.raiseError(e) 32 | case Right(p) => new JavaSecurityDigest(name, p).digest(data) 33 | } 34 | } 35 | 36 | override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = { 37 | val name = algorithm.toStringJava 38 | providers.messageDigest(name) match { 39 | case Left(e) => 40 | _ => Stream.eval(F.raiseError(e)) 41 | case Right(p) => new JavaSecurityDigest(name, p).pipe 42 | } 43 | } 44 | } 45 | } 46 | 47 | def forSync[F[_]](implicit F: Sync[F]): Resource[F, Hash[F]] = 48 | Resource.eval(F.delay(forProviders(Providers.get())(F))) 49 | def forAsync[F[_]: Async]: Resource[F, Hash[F]] = forSync 50 | } 51 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/HmacPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.Sync 20 | import scodec.bits.ByteVector 21 | 22 | import javax.crypto 23 | 24 | private[bobcats] trait HmacPlatform[F[_]] { 25 | def importJavaKey(key: crypto.SecretKey): F[SecretKey[HmacAlgorithm]] 26 | } 27 | 28 | private[bobcats] trait HmacCompanionPlatform { 29 | 30 | private[bobcats] def forProviders[F[_]](providers: Providers)(implicit F: Sync[F]): Hmac[F] = 31 | new UnsealedHmac[F] { 32 | 33 | override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = 34 | F.catchNonFatal { 35 | val alg = key.algorithm.toStringJava 36 | val provider = providers.mac(alg) match { 37 | case Left(e) => throw e 38 | case Right(p) => p 39 | } 40 | val mac = crypto.Mac.getInstance(key.algorithm.toStringJava, provider) 41 | val sk = key.toJava 42 | mac.init(sk) 43 | mac.update(data.toByteBuffer) 44 | ByteVector.view(mac.doFinal()) 45 | } 46 | 47 | override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = 48 | F.delay { 49 | val key = crypto.KeyGenerator.getInstance(algorithm.toStringJava).generateKey() 50 | SecretKeySpec(ByteVector.view(key.getEncoded()), algorithm) 51 | } 52 | 53 | override def importKey[A <: HmacAlgorithm]( 54 | key: ByteVector, 55 | algorithm: A): F[SecretKey[A]] = 56 | F.pure(SecretKeySpec(key, algorithm)) 57 | 58 | override def importJavaKey(key: crypto.SecretKey): F[SecretKey[HmacAlgorithm]] = 59 | F.fromOption( 60 | for { 61 | algorithm <- HmacAlgorithm.fromStringJava(key.getAlgorithm()) 62 | key <- Option(key.getEncoded()) 63 | } yield SecretKeySpec(ByteVector.view(key), algorithm), 64 | new InvalidKeyException 65 | ) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/KeyPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import java.security 20 | import javax.crypto 21 | 22 | private[bobcats] trait KeyPlatform { 23 | def toJava: security.Key 24 | } 25 | 26 | private[bobcats] trait PublicKeyPlatform { 27 | def toJava: security.PublicKey 28 | } 29 | 30 | private[bobcats] trait PrivateKeyPlatform { 31 | def toJava: security.PrivateKey 32 | } 33 | 34 | private[bobcats] trait SecretKeyPlatform { 35 | def toJava: crypto.SecretKey 36 | } 37 | 38 | private[bobcats] trait SecretKeySpecPlatform[+A <: Algorithm] { self: SecretKeySpec[A] => 39 | def toJava: crypto.spec.SecretKeySpec = 40 | new crypto.spec.SecretKeySpec(key.toArray, algorithm.toStringJava) 41 | } 42 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/Providers.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import java.security.{NoSuchAlgorithmException, Provider, Security} 20 | 21 | private[bobcats] final class Providers(val ps: Array[Provider]) extends AnyVal { 22 | 23 | private def provider(service: String, name: String): Option[Provider] = 24 | ps.find(provider => provider.getService(service, name) != null) 25 | 26 | def messageDigest(name: String): Either[NoSuchAlgorithmException, Provider] = 27 | provider("MessageDigest", name).toRight( 28 | new NoSuchAlgorithmException(s"${name} MessageDigest not available")) 29 | 30 | def mac(name: String): Either[NoSuchAlgorithmException, Provider] = 31 | provider("Mac", name).toRight(new NoSuchAlgorithmException(s"${name} Mac not available")) 32 | 33 | } 34 | 35 | private[bobcats] object Providers { 36 | def get(): Providers = new Providers(Security.getProviders().clone()) 37 | } 38 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/SecureEqPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scodec.bits.ByteVector 20 | 21 | private[bobcats] trait SecureEqCompanionPlatform { this: SecureEq.type => 22 | 23 | implicit val secureEqForByteVector: SecureEq[ByteVector] = new ByteVectorSecureEq 24 | 25 | } 26 | -------------------------------------------------------------------------------- /core/jvm/src/main/scala/bobcats/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package object bobcats { 18 | type GeneralSecurityException = java.security.GeneralSecurityException 19 | type KeyException = java.security.KeyException 20 | type InvalidKeyException = java.security.InvalidKeyException 21 | } 22 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/Context.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import openssl._ 20 | import openssl.crypto._ 21 | import scala.scalanative.unsafe._ 22 | 23 | import cats.effect.kernel.{Resource, Sync} 24 | 25 | private[bobcats] object Context { 26 | def apply[F[_]](implicit F: Sync[F]): Resource[F, Ptr[OSSL_LIB_CTX]] = 27 | Resource.make(F.delay(OSSL_LIB_CTX_new()))(ctx => F.delay(OSSL_LIB_CTX_free(ctx))) 28 | } 29 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/CryptoPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.{Async, Resource, Sync} 20 | 21 | private[bobcats] trait CryptoCompanionPlatform { 22 | 23 | def forSync[F[_]](implicit F: Sync[F]): Resource[F, Crypto[F]] = 24 | Context[F].map { ctx => 25 | new UnsealedCrypto[F] { 26 | override def hash: Hash[F] = Hash.forContext(ctx) 27 | override def hmac: Hmac[F] = Hmac.forContext(ctx) 28 | } 29 | } 30 | 31 | def forAsync[F[_]: Async]: Resource[F, Crypto[F]] = forSync[F] 32 | } 33 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/Hash1Platform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scala.scalanative.annotation.alwaysinline 20 | import cats.effect.kernel.{Async, Resource, Sync} 21 | import scodec.bits.ByteVector 22 | import scalanative.unsafe._ 23 | import scalanative.unsigned._ 24 | import openssl._ 25 | import openssl.err._ 26 | import openssl.evp._ 27 | import fs2.{Chunk, Pipe, Stream} 28 | 29 | private[bobcats] final class NativeEvpDigest[F[_]](digest: Ptr[EVP_MD])(implicit F: Sync[F]) 30 | extends UnsealedHash1[F] { 31 | def digest(data: ByteVector): F[ByteVector] = { 32 | val d = data.toArrayUnsafe 33 | val ctx = EVP_MD_CTX_new() 34 | try { 35 | init(ctx, digest) 36 | update(ctx, d) 37 | F.pure(`final`(ctx, (ptr, len) => ByteVector.fromPtr(ptr, len.toLong))) 38 | } catch { 39 | case e: Error => F.raiseError(e) 40 | } finally { 41 | EVP_MD_CTX_free(ctx) 42 | } 43 | } 44 | 45 | private def update(ctx: Ptr[EVP_MD_CTX], data: Array[Byte]): Unit = { 46 | val len = data.length 47 | if (EVP_DigestUpdate(ctx, if (len == 0) null else data.at(0), len.toULong) != 1) { 48 | throw Error("EVP_DigestUpdate", ERR_get_error()) 49 | } 50 | } 51 | 52 | private def init(ctx: Ptr[EVP_MD_CTX], digest: Ptr[EVP_MD]): Unit = 53 | if (EVP_DigestInit_ex(ctx, digest, null) != 1) { 54 | throw Error("EVP_DigestInit_ex", ERR_get_error()) 55 | } 56 | 57 | @alwaysinline private def `final`[A](ctx: Ptr[EVP_MD_CTX], f: (Ptr[Byte], Int) => A): A = { 58 | val md = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE.toUInt) 59 | val s = stackalloc[CInt]() 60 | if (EVP_DigestFinal_ex(ctx, md, s) != 1) { 61 | throw Error("EVP_DigestFinal_ex", ERR_get_error()) 62 | } 63 | f(md.asInstanceOf[Ptr[Byte]], s(0)) 64 | } 65 | 66 | private val context: Stream[F, Ptr[EVP_MD_CTX]] = 67 | Stream.bracket(F.delay { 68 | val ctx = EVP_MD_CTX_new() 69 | init(ctx, digest) 70 | ctx 71 | })(ctx => F.delay(EVP_MD_CTX_free(ctx))) 72 | 73 | def pipe: Pipe[F, Byte, Byte] = { in => 74 | context.flatMap { ctx => 75 | // Most of the calls throw, so wrap in a `delay` 76 | in.chunks 77 | .evalMap { chunk => F.delay(update(ctx, chunk.toByteVector.toArrayUnsafe)) } 78 | .drain ++ Stream.eval(F.delay(`final`(ctx, Chunk.fromBytePtr))) 79 | }.unchunks 80 | } 81 | } 82 | 83 | import java.security.NoSuchAlgorithmException 84 | 85 | private[bobcats] trait Hash1CompanionPlatform { 86 | 87 | private[bobcats] def evpAlgorithm(algorithm: HashAlgorithm): CString = { 88 | import HashAlgorithm._ 89 | algorithm match { 90 | case MD5 => c"MD5" 91 | case SHA1 => c"SHA1" 92 | case SHA256 => c"SHA256" 93 | case SHA512 => c"SHA512" 94 | } 95 | } 96 | 97 | private[bobcats] def evpFetch(ctx: Ptr[OSSL_LIB_CTX], name: CString): Ptr[EVP_MD] = { 98 | val md = EVP_MD_fetch(ctx, name, null) 99 | if (md == null) { 100 | throw new NoSuchAlgorithmException( 101 | s"${fromCString(name)} Message Digest not available", 102 | Error("EVP_MD_fetch", ERR_get_error()) 103 | ) 104 | } 105 | md 106 | } 107 | 108 | /** 109 | * Create a hash for a particular name used by `libcrypto`. 110 | */ 111 | private[bobcats] def fromCryptoName[F[_]](name: CString)( 112 | implicit F: Sync[F]): Resource[F, Hash1[F]] = 113 | Resource 114 | .make(F.delay(evpFetch(null, name)))(md => F.delay(EVP_MD_free(md))) 115 | .map(new NativeEvpDigest(_)) 116 | 117 | def forSync[F[_]: Sync](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = 118 | fromCryptoName(evpAlgorithm(algorithm)) 119 | 120 | def forAsync[F[_]: Async](algorithm: HashAlgorithm): Resource[F, Hash1[F]] = forSync( 121 | algorithm) 122 | } 123 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/HashPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.{Async, Resource, Sync} 20 | import scodec.bits.ByteVector 21 | import scalanative.unsafe._ 22 | import openssl._ 23 | import openssl.evp._ 24 | import fs2.{Pipe, Stream} 25 | 26 | private[bobcats] trait HashCompanionPlatform { 27 | 28 | private[bobcats] def forContext[F[_]](ctx: Ptr[OSSL_LIB_CTX])(implicit F: Sync[F]): Hash[F] = 29 | new UnsealedHash[F] { 30 | 31 | override def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] = { 32 | val md = Hash1.evpFetch(ctx, Hash1.evpAlgorithm(algorithm)) 33 | // Note, this is eager currently which is why the cleanup is working 34 | val digest = new NativeEvpDigest(md).digest(data) 35 | EVP_MD_free(md) 36 | digest 37 | } 38 | 39 | override def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] = 40 | in => { 41 | val alg = Hash1.evpAlgorithm(algorithm) 42 | Stream 43 | .bracket(F.delay(Hash1.evpFetch(ctx, alg)))(md => F.delay(EVP_MD_free(md))) 44 | .flatMap { md => in.through(new NativeEvpDigest(md).pipe) } 45 | } 46 | } 47 | 48 | def forSync[F[_]: Sync]: Resource[F, Hash[F]] = Context[F].map(forContext[F]) 49 | def forAsync[F[_]: Async]: Resource[F, Hash[F]] = forSync 50 | 51 | } 52 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/HmacPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.kernel.{Async, Resource, Sync} 20 | import scodec.bits.ByteVector 21 | 22 | private[bobcats] trait HmacPlatform[F[_]] {} 23 | 24 | import scala.scalanative.unsafe._ 25 | 26 | import openssl._ 27 | import openssl.evp._ 28 | import openssl.params._ 29 | import openssl.err._ 30 | import scala.scalanative.libc._ 31 | import scala.scalanative.unsigned._ 32 | 33 | private[bobcats] trait HmacCompanionPlatform { 34 | 35 | def forAsync[F[_]: Async]: Resource[F, Hmac[F]] = forSync 36 | def forSync[F[_]: Sync]: Resource[F, Hmac[F]] = Context[F].map(forContext[F]) 37 | 38 | private[bobcats] def forContext[F[_]](ctx: Ptr[OSSL_LIB_CTX])(implicit F: Sync[F]): Hmac[F] = 39 | new UnsealedHmac[F] { 40 | 41 | /** 42 | * See [[https://www.openssl.org/docs/man3.1/man7/EVP_MAC-HMAC.html]] 43 | */ 44 | override def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] = { 45 | key match { 46 | case SecretKeySpec(key, algorithm) => 47 | val mdName = Hash1.evpAlgorithm(algorithm.hashAlgorithm) 48 | val mdLen = string.strlen(mdName) 49 | F.catchNonFatal { 50 | val oneshot = stackalloc[CInt]() 51 | oneshot(0) = 1 52 | val params = stackalloc[OSSL_PARAM](3.toUInt) 53 | OSSL_MAC_PARAM_DIGEST(params(0), mdName, mdLen) 54 | OSSL_MAC_PARAM_DIGEST_ONESHOT(params(1), oneshot) 55 | OSSL_PARAM_END(params(2)) 56 | val mac = EVP_MAC_fetch(ctx, c"HMAC", null) 57 | 58 | if (mac == null) { 59 | throw Error("EVP_MAC_fetch", ERR_get_error()) 60 | } else { 61 | val ctx = EVP_MAC_CTX_new(mac) 62 | try { 63 | val k = key.toArrayUnsafe 64 | val d = data.toArrayUnsafe 65 | if (EVP_MAC_init( 66 | ctx, 67 | k.at(0).asInstanceOf[Ptr[CUnsignedChar]], 68 | k.length.toULong, 69 | params 70 | ) != 1) { 71 | throw Error("EVP_MAC_init", ERR_get_error()) 72 | } 73 | val out = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE.toUInt) 74 | val outl = stackalloc[CSize]() 75 | 76 | if (EVP_MAC_update( 77 | ctx, 78 | d.at(0).asInstanceOf[Ptr[CUnsignedChar]], 79 | d.length.toULong) != 1) { 80 | throw Error("EVP_MAC_update", ERR_get_error()) 81 | } 82 | 83 | if (EVP_MAC_final(ctx, out, outl, EVP_MAX_MD_SIZE.toULong) != 1) { 84 | throw Error("EVP_MAC_final", ERR_get_error()) 85 | } 86 | ByteVector.fromPtr(out.asInstanceOf[Ptr[Byte]], outl(0).toLong) 87 | } finally { 88 | EVP_MAC_CTX_free(ctx) 89 | } 90 | } 91 | } 92 | case _ => F.raiseError[ByteVector](new InvalidKeyException) 93 | } 94 | } 95 | 96 | /** 97 | * See [[https://www.openssl.org/docs/man3.0/man7/EVP_RAND-CTR-DRBG.html]] 98 | */ 99 | override def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] = { 100 | F.delay { 101 | // See NIST SP 800-90A 102 | val rand = EVP_RAND_fetch(null, c"CTR-DRBG", null) 103 | if (rand == null) { 104 | throw Error("EVP_RAND_fetch", ERR_get_error()) 105 | } else { 106 | val ctx = EVP_RAND_CTX_new(rand, null) 107 | val params = stackalloc[OSSL_PARAM](2.toUInt) 108 | val out = stackalloc[CUnsignedChar](EVP_MAX_MD_SIZE.toUInt) 109 | val cipher = c"AES-256-CTR" 110 | 111 | OSSL_DBRG_PARAM_CIPHER(params(0), cipher, string.strlen(cipher)) 112 | OSSL_PARAM_END(params(1)) 113 | try { 114 | 115 | val strength = 128.toUInt 116 | val len = algorithm.minimumKeyLength 117 | 118 | if (EVP_RAND_instantiate(ctx, strength, 0, null, 0.toULong, params) != 1) { 119 | throw Error("EVP_RAND_instantiate", ERR_get_error()) 120 | } 121 | if (EVP_RAND_generate(ctx, out, len.toULong, strength, 0, null, 0.toULong) != 1) { 122 | throw Error("EVP_RAND_generate", ERR_get_error()) 123 | } 124 | val key = ByteVector.fromPtr(out.asInstanceOf[Ptr[Byte]], len.toLong) 125 | SecretKeySpec(key, algorithm) 126 | } finally { 127 | EVP_RAND_CTX_free(ctx) 128 | } 129 | } 130 | } 131 | } 132 | 133 | override def importKey[A <: HmacAlgorithm]( 134 | key: ByteVector, 135 | algorithm: A): F[SecretKey[A]] = F.pure(SecretKeySpec(key, algorithm)) 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/KeyPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | private[bobcats] trait KeyPlatform {} 20 | 21 | private[bobcats] trait PublicKeyPlatform {} 22 | 23 | private[bobcats] trait PrivateKeyPlatform {} 24 | 25 | private[bobcats] trait SecretKeyPlatform {} 26 | 27 | private[bobcats] trait SecretKeySpecPlatform[+A <: Algorithm] { self: SecretKeySpec[A] => } 28 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/SecureEqPlatform.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scodec.bits.ByteVector 20 | 21 | private[bobcats] trait SecureEqCompanionPlatform { this: SecureEq.type => 22 | 23 | implicit val secureEqForByteVector: SecureEq[ByteVector] = 24 | new SecureEq[ByteVector] { 25 | 26 | import scala.scalanative.unsafe._ 27 | import scala.scalanative.unsigned._ 28 | import openssl.crypto._ 29 | 30 | override def eqv(x: ByteVector, y: ByteVector): Boolean = { 31 | val xArr = x.toArrayUnsafe 32 | val len = xArr.length 33 | len == y.length && (len == 0 || CRYPTO_memcmp( 34 | xArr.at(0), 35 | y.toArrayUnsafe.at(0), 36 | len.toULong) == 0) 37 | } 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/exceptions.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | class KeyException private[bobcats] (msg: String) 20 | extends java.security.GeneralSecurityException(msg, null) 21 | final class InvalidKeyException private[bobcats] () extends KeyException(null) 22 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/openssl/crypto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | package openssl 19 | 20 | import scala.scalanative.unsafe._ 21 | 22 | @extern 23 | @link("crypto") 24 | private[bobcats] object crypto { 25 | 26 | /** 27 | * See [[https://www.openssl.org/docs/man3.1/man3/OSSL_LIB_CTX_new.html]] 28 | */ 29 | def OSSL_LIB_CTX_new(): Ptr[OSSL_LIB_CTX] = extern 30 | 31 | /** 32 | * See [[https://www.openssl.org/docs/man3.1/man3/OSSL_LIB_CTX_free.html]] 33 | */ 34 | def OSSL_LIB_CTX_free(ctx: Ptr[OSSL_LIB_CTX]): Unit = extern 35 | 36 | /** 37 | * See [[https://www.openssl.org/docs/man3.1/man3/CRYPTO_memcmp.html]] 38 | */ 39 | def CRYPTO_memcmp(a: Ptr[Byte], b: Ptr[Byte], len: CSize): CInt = extern 40 | 41 | } 42 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/openssl/err.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | package openssl 19 | 20 | import scala.scalanative.unsafe._ 21 | import scala.scalanative.unsigned._ 22 | 23 | @extern 24 | @link("crypto") 25 | private[bobcats] object err { 26 | 27 | def ERR_get_error(): ULong = extern 28 | def ERR_func_error_string(e: ULong): CString = extern 29 | def ERR_reason_error_string(e: ULong): CString = extern 30 | } 31 | 32 | private[bobcats] final class Error(private[openssl] message: String) 33 | extends GeneralSecurityException(message, null) 34 | 35 | private[bobcats] object Error { 36 | 37 | import err._ 38 | 39 | def apply(func: String, err: ULong): Error = { 40 | if (err == 0.toULong) { 41 | new Error(func) 42 | } else { 43 | val reason = fromCString(ERR_reason_error_string(err)) 44 | new Error(func + ":" + err + ":" + reason) 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/openssl/evp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | package openssl 19 | 20 | import scala.scalanative.unsafe._ 21 | 22 | @extern 23 | @link("crypto") 24 | private[bobcats] object evp { 25 | 26 | type EVP_MD_CTX 27 | type EVP_MD 28 | type ENGINE 29 | 30 | final val EVP_MAX_MD_SIZE = 64 31 | 32 | /** 33 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_fetch.html]] 34 | */ 35 | def EVP_MD_fetch( 36 | ctx: Ptr[OSSL_LIB_CTX], 37 | algorithm: CString, 38 | properties: CString): Ptr[EVP_MD] = extern 39 | 40 | /** 41 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_free.html]] 42 | */ 43 | def EVP_MD_free(ctx: Ptr[EVP_MD]): Unit = extern 44 | 45 | /** 46 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_CTX_new.html]] 47 | */ 48 | def EVP_MD_CTX_new(): Ptr[EVP_MD_CTX] = extern 49 | 50 | /** 51 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_CTX_reset.html]] 52 | */ 53 | def EVP_MD_CTX_reset(ctx: Ptr[EVP_MD_CTX]): CInt = extern 54 | 55 | /** 56 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_CTX_free.html]] 57 | */ 58 | def EVP_MD_CTX_free(ctx: Ptr[EVP_MD_CTX]): Unit = extern 59 | 60 | /** 61 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_get_digestbyname.html]] 62 | */ 63 | def EVP_get_digestbyname(s: CString): Ptr[EVP_MD] = extern 64 | 65 | /** 66 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MD_get0_name.html]] 67 | */ 68 | def EVP_MD_get0_name(md: Ptr[EVP_MD]): CString = extern 69 | 70 | /** 71 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_DigestInit_ex.html]] 72 | */ 73 | def EVP_DigestInit_ex(ctx: Ptr[EVP_MD_CTX], `type`: Ptr[EVP_MD], engine: Ptr[ENGINE]): CInt = 74 | extern 75 | 76 | /** 77 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_DigestUpdate.html]] 78 | */ 79 | def EVP_DigestUpdate(ctx: Ptr[EVP_MD_CTX], d: Ptr[Byte], count: CSize): CInt = extern 80 | 81 | /** 82 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_DigestFinal_ex.html]] 83 | */ 84 | def EVP_DigestFinal_ex(ctx: Ptr[EVP_MD_CTX], md: Ptr[CUnsignedChar], s: Ptr[CInt]): CInt = 85 | extern 86 | 87 | type EVP_MAC 88 | type EVP_MAC_CTX 89 | 90 | /** 91 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MAC_fetch.html]] 92 | */ 93 | def EVP_MAC_fetch( 94 | libctx: Ptr[OSSL_LIB_CTX], 95 | algoritm: CString, 96 | properties: CString): Ptr[EVP_MAC] = extern 97 | 98 | /** 99 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MAC_CTX_new.html]] 100 | */ 101 | def EVP_MAC_CTX_new(mac: Ptr[EVP_MAC]): Ptr[EVP_MAC_CTX] = extern 102 | 103 | /** 104 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MAC_CTX_free.html]] 105 | */ 106 | def EVP_MAC_CTX_free(ctx: Ptr[EVP_MAC_CTX]): Unit = extern 107 | 108 | /** 109 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MAC_init.html]] 110 | */ 111 | def EVP_MAC_init( 112 | ctx: Ptr[EVP_MAC_CTX], 113 | key: Ptr[CUnsignedChar], 114 | keylen: CSize, 115 | params: Ptr[OSSL_PARAM]): CInt = extern 116 | 117 | /** 118 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MAC_update.html]] 119 | */ 120 | def EVP_MAC_update( 121 | ctx: Ptr[EVP_MAC_CTX], 122 | data: Ptr[CUnsignedChar], 123 | datalen: CSize 124 | ): CInt = extern 125 | 126 | /** 127 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_MAC_final.html]] 128 | */ 129 | def EVP_MAC_final( 130 | ctx: Ptr[EVP_MAC_CTX], 131 | out: Ptr[CUnsignedChar], 132 | outl: Ptr[CSize], 133 | outsize: CSize 134 | ): CInt = extern 135 | 136 | type EVP_RAND 137 | type EVP_RAND_CTX 138 | 139 | /** 140 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_RAND_fetch.html]] 141 | */ 142 | def EVP_RAND_fetch( 143 | libctx: Ptr[OSSL_LIB_CTX], 144 | algoritm: CString, 145 | properties: CString): Ptr[EVP_RAND] = extern 146 | 147 | /** 148 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_RAND_CTX_new.html]] 149 | */ 150 | def EVP_RAND_CTX_new(rand: Ptr[EVP_RAND], parent: Ptr[EVP_RAND_CTX]): Ptr[EVP_RAND_CTX] = 151 | extern 152 | 153 | /** 154 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_RAND_CTX_free.html]] 155 | */ 156 | def EVP_RAND_CTX_free(ctx: Ptr[EVP_RAND_CTX]): Unit = extern 157 | 158 | /** 159 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_RAND_instantiate.html]] 160 | */ 161 | def EVP_RAND_instantiate( 162 | ctx: Ptr[EVP_RAND_CTX], 163 | strength: CUnsignedInt, 164 | prediction_resistance: Int, 165 | pstr: CString, 166 | pstr_len: CSize, 167 | params: Ptr[OSSL_PARAM]): CInt = extern 168 | 169 | /** 170 | * See [[https://www.openssl.org/docs/man3.1/man3/EVP_RAND_generate.html]] 171 | */ 172 | def EVP_RAND_generate( 173 | ctx: Ptr[EVP_RAND_CTX], 174 | out: Ptr[CUnsignedChar], 175 | outlen: CSize, 176 | strength: CUnsignedInt, 177 | prediction_resistance: Int, 178 | addin: Ptr[CUnsignedChar], 179 | addin_len: CSize): CInt = extern 180 | 181 | } 182 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/openssl/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scala.scalanative.annotation.alwaysinline 20 | import scala.scalanative.unsafe._ 21 | 22 | package object openssl { 23 | 24 | private[bobcats] type OSSL_LIB_CTX 25 | 26 | /** 27 | * See [[https://www.openssl.org/docs/man3.1/man3/OSSL_PARAM.html]] 28 | */ 29 | private[bobcats] type OSSL_PARAM = CStruct5[CString, CUnsignedChar, Ptr[Byte], CSize, CSize] 30 | private[bobcats] object OSSL_PARAM { 31 | @alwaysinline private[bobcats] def init( 32 | param: OSSL_PARAM, 33 | key: CString, 34 | dataType: CUnsignedChar, 35 | data: Ptr[Byte], 36 | dataSize: CSize, 37 | returnSize: CSize): Unit = { 38 | param._1 = key 39 | param._2 = dataType 40 | param._3 = data 41 | param._4 = dataSize 42 | param._5 = returnSize 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/openssl/params.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | package openssl 19 | 20 | import scala.scalanative.unsafe._ 21 | import scala.scalanative.annotation.alwaysinline 22 | import scalanative.unsigned._ 23 | 24 | private[bobcats] object params { 25 | final val OSSL_PARAM_INTEGER = 1.toUByte 26 | final val OSSL_PARAM_UNSIGNED_INTEGER = 2.toUByte 27 | final val OSSL_PARAM_REAL = 3.toUByte 28 | final val OSSL_PARAM_UTF8_STRING = 4.toUByte 29 | final val OSSL_PARAM_OCTET_STRING = 5.toUByte 30 | 31 | /** 32 | * Compiles to `(size_t) - 1`. 33 | */ 34 | final val OSSL_PARAM_UNMODIFIED: CSize = -1.toULong 35 | 36 | @alwaysinline def OSSL_PARAM_END(param: OSSL_PARAM): Unit = 37 | OSSL_PARAM.init(param, null, 0.toUByte, null, 0.toULong, 0.toULong) 38 | @alwaysinline def OSSL_PARAM_octet_string( 39 | param: OSSL_PARAM, 40 | key: CString, 41 | data: Ptr[Byte], 42 | data_size: CSize): Unit = 43 | OSSL_PARAM.init(param, key, OSSL_PARAM_OCTET_STRING, data, data_size, OSSL_PARAM_UNMODIFIED) 44 | 45 | @alwaysinline def OSSL_PARAM_int(param: OSSL_PARAM, key: CString, data: Ptr[CInt]): Unit = 46 | OSSL_PARAM.init( 47 | param, 48 | key, 49 | OSSL_PARAM_INTEGER, 50 | data.asInstanceOf[Ptr[Byte]], 51 | sizeof[CInt], 52 | OSSL_PARAM_UNMODIFIED) 53 | 54 | @alwaysinline def OSSL_PARAM_utf8_string( 55 | param: OSSL_PARAM, 56 | key: CString, 57 | data: Ptr[Byte], 58 | data_size: CSize): Unit = 59 | OSSL_PARAM.init(param, key, OSSL_PARAM_UTF8_STRING, data, data_size, OSSL_PARAM_UNMODIFIED) 60 | 61 | @alwaysinline def OSSL_DBRG_PARAM_CIPHER( 62 | param: OSSL_PARAM, 63 | cipher: CString, 64 | cipher_len: CSize): Unit = 65 | OSSL_PARAM_utf8_string(param, c"cipher", cipher, cipher_len) 66 | 67 | @alwaysinline def OSSL_MAC_PARAM_DIGEST( 68 | param: OSSL_PARAM, 69 | digest: CString, 70 | digest_len: CSize): Unit = 71 | OSSL_PARAM_utf8_string(param, c"digest", digest, digest_len) 72 | 73 | @alwaysinline def OSSL_MAC_PARAM_DIGEST_ONESHOT(param: OSSL_PARAM, oneshot: Ptr[CInt]): Unit = 74 | OSSL_PARAM_int(param, c"digest-oneshot", oneshot) 75 | 76 | } 77 | -------------------------------------------------------------------------------- /core/native/src/main/scala/bobcats/package.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package object bobcats { 18 | type GeneralSecurityException = java.security.GeneralSecurityException 19 | } 20 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Algorithm.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | sealed trait Algorithm { 20 | private[bobcats] def toStringJava: String 21 | private[bobcats] def toStringNodeJS: String 22 | private[bobcats] def toStringWebCrypto: String 23 | } 24 | 25 | sealed trait HashAlgorithm extends Algorithm 26 | object HashAlgorithm { 27 | 28 | case object MD5 extends HashAlgorithm { 29 | private[bobcats] override def toStringJava: String = "MD5" 30 | private[bobcats] override def toStringNodeJS: String = "md5" 31 | private[bobcats] override def toStringWebCrypto: String = 32 | throw new UnsupportedOperationException 33 | } 34 | 35 | case object SHA1 extends HashAlgorithm { 36 | private[bobcats] override def toStringJava: String = "SHA-1" 37 | private[bobcats] override def toStringNodeJS: String = "sha1" 38 | private[bobcats] override def toStringWebCrypto: String = "SHA-1" 39 | } 40 | 41 | case object SHA256 extends HashAlgorithm { 42 | private[bobcats] override def toStringJava: String = "SHA-256" 43 | private[bobcats] override def toStringNodeJS: String = "sha256" 44 | private[bobcats] override def toStringWebCrypto: String = "SHA-256" 45 | } 46 | 47 | case object SHA512 extends HashAlgorithm { 48 | private[bobcats] override def toStringJava: String = "SHA-512" 49 | private[bobcats] override def toStringNodeJS: String = "sha512" 50 | private[bobcats] override def toStringWebCrypto: String = "SHA-512" 51 | } 52 | } 53 | 54 | sealed trait HmacAlgorithm extends Algorithm { 55 | 56 | import HmacAlgorithm._ 57 | 58 | private[bobcats] def minimumKeyLength: Int 59 | private[bobcats] def hashAlgorithm: HashAlgorithm = this match { 60 | case SHA1 => HashAlgorithm.SHA1 61 | case SHA256 => HashAlgorithm.SHA256 62 | case SHA512 => HashAlgorithm.SHA512 63 | } 64 | } 65 | object HmacAlgorithm { 66 | 67 | private[bobcats] def fromStringJava(algorithm: String): Option[HmacAlgorithm] = 68 | algorithm match { 69 | case "HmacSHA1" => Some(SHA1) 70 | case "HmacSHA256" => Some(SHA256) 71 | case "HmacSHA512" => Some(SHA512) 72 | case _ => None 73 | } 74 | 75 | case object SHA1 extends HmacAlgorithm { 76 | private[bobcats] override def toStringJava: String = "HmacSHA1" 77 | private[bobcats] override def toStringNodeJS: String = "sha1" 78 | private[bobcats] override def toStringWebCrypto: String = "SHA-1" 79 | private[bobcats] override def minimumKeyLength: Int = 20 80 | } 81 | 82 | case object SHA256 extends HmacAlgorithm { 83 | private[bobcats] override def toStringJava: String = "HmacSHA256" 84 | private[bobcats] override def toStringNodeJS: String = "sha256" 85 | private[bobcats] override def toStringWebCrypto: String = "SHA-256" 86 | private[bobcats] override def minimumKeyLength: Int = 32 87 | } 88 | 89 | case object SHA512 extends HmacAlgorithm { 90 | private[bobcats] override def toStringJava: String = "HmacSHA512" 91 | private[bobcats] override def toStringNodeJS: String = "sha512" 92 | private[bobcats] override def toStringWebCrypto: String = "SHA-512" 93 | private[bobcats] override def minimumKeyLength: Int = 64 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Crypto.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | sealed trait Crypto[F[_]] { 20 | def hash: Hash[F] 21 | def hmac: Hmac[F] 22 | } 23 | 24 | private[bobcats] trait UnsealedCrypto[F[_]] extends Crypto[F] 25 | 26 | object Crypto extends CryptoCompanionPlatform { 27 | def apply[F[_]](implicit crypto: Crypto[F]): crypto.type = crypto 28 | } 29 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Hash.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scodec.bits.ByteVector 20 | import fs2.Pipe 21 | 22 | sealed trait Hash[F[_]] { 23 | def digest(algorithm: HashAlgorithm, data: ByteVector): F[ByteVector] 24 | def digestPipe(algorithm: HashAlgorithm): Pipe[F, Byte, Byte] 25 | } 26 | 27 | private[bobcats] trait UnsealedHash[F[_]] extends Hash[F] 28 | 29 | object Hash extends HashCompanionPlatform { 30 | def apply[F[_]](implicit hash: Hash[F]): hash.type = hash 31 | } 32 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Hash1.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import fs2.Pipe 20 | import scodec.bits.ByteVector 21 | 22 | /** 23 | * Hash for a single algorithm. 24 | * 25 | * Use this class if you have a specific `HashAlgorithm` known in advance or you're using a 26 | * customized algorithm not covered by the `HashAlgorithm` class. 27 | */ 28 | sealed trait Hash1[F[_]] { 29 | def digest(data: ByteVector): F[ByteVector] 30 | def pipe: Pipe[F, Byte, Byte] 31 | } 32 | 33 | private[bobcats] trait UnsealedHash1[F[_]] extends Hash1[F] 34 | 35 | object Hash1 extends Hash1CompanionPlatform 36 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Hmac.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scodec.bits.ByteVector 20 | 21 | sealed trait Hmac[F[_]] extends HmacPlatform[F] { 22 | def digest(key: SecretKey[HmacAlgorithm], data: ByteVector): F[ByteVector] 23 | def generateKey[A <: HmacAlgorithm](algorithm: A): F[SecretKey[A]] 24 | def importKey[A <: HmacAlgorithm](key: ByteVector, algorithm: A): F[SecretKey[A]] 25 | } 26 | 27 | private[bobcats] trait UnsealedHmac[F[_]] extends Hmac[F] 28 | 29 | object Hmac extends HmacCompanionPlatform { 30 | 31 | def apply[F[_]](implicit hmac: Hmac[F]): hmac.type = hmac 32 | 33 | } 34 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Hotp.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.Functor 20 | import cats.syntax.functor._ 21 | import scodec.bits.ByteVector 22 | 23 | object Hotp { 24 | private val powersOfTen = 25 | Array(1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000, 1000000000) 26 | 27 | def generate[F[_]]( 28 | key: SecretKey[HmacAlgorithm.SHA1.type], 29 | movingFactor: Long 30 | )(implicit F: Functor[F], H: Hmac[F]): F[Int] = 31 | generate(key, movingFactor, digits = 6) 32 | 33 | def generate[F[_]]( 34 | key: SecretKey[HmacAlgorithm.SHA1.type], 35 | movingFactor: Long, 36 | digits: Int 37 | )(implicit F: Functor[F], H: Hmac[F]): F[Int] = { 38 | require(digits >= 6, s"digits must be at least 6, was $digits") 39 | require(digits < 10, s"digits must be less than 10, was $digits") 40 | 41 | H.digest(key, ByteVector.fromLong(movingFactor)).map { hmac => 42 | val offset = hmac.last & 0xf 43 | 44 | val binaryCode = ((hmac.get(offset.longValue) & 0x7f) << 24) | 45 | ((hmac.get((offset + 1).longValue) & 0xff) << 16) | 46 | ((hmac.get((offset + 2).longValue) & 0xff) << 8) | 47 | (hmac.get((offset + 3).longValue) & 0xff) 48 | 49 | binaryCode % powersOfTen(digits) 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/Key.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import scodec.bits.ByteVector 20 | 21 | sealed trait Key[+A <: Algorithm] extends KeyPlatform { 22 | def algorithm: A 23 | } 24 | 25 | sealed trait PublicKey[+A <: Algorithm] extends Key[A] with PublicKeyPlatform 26 | sealed trait PrivateKey[+A <: Algorithm] extends Key[A] with PrivateKeyPlatform 27 | sealed trait SecretKey[+A <: Algorithm] extends Key[A] with SecretKeyPlatform 28 | 29 | final case class SecretKeySpec[+A <: Algorithm](key: ByteVector, algorithm: A) 30 | extends SecretKey[A] 31 | with SecretKeySpecPlatform[A] 32 | -------------------------------------------------------------------------------- /core/shared/src/main/scala/bobcats/SecureEq.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /* 18 | * Copyright (c) 2000-2021 The Legion of the Bouncy Castle Inc. (https://www.bouncycastle.org) 19 | * 20 | * Permission is hereby granted, free of charge, to any person obtaining a copy of this software 21 | * and associated documentation files (the "Software"), to deal in the Software without restriction, 22 | * including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 23 | * and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 24 | * subject to the following conditions: 25 | * 26 | * The above copyright notice and this permission notice shall be included in all copies or substantial 27 | * portions of the Software. 28 | */ 29 | 30 | package bobcats 31 | 32 | import cats.kernel.Eq 33 | import scodec.bits.ByteVector 34 | 35 | trait SecureEq[A] extends Eq[A] { 36 | 37 | /** 38 | * A constant time equals comparison - does not terminate early if test will fail. For best 39 | * results always pass the expected value as the first parameter. 40 | */ 41 | override def eqv(expected: A, supplied: A): Boolean 42 | } 43 | 44 | object SecureEq extends SecureEqCompanionPlatform { 45 | 46 | def apply[A](implicit eq: SecureEq[A]): eq.type = eq 47 | 48 | private[bobcats] final class ByteVectorSecureEq extends SecureEq[ByteVector] { 49 | 50 | override def eqv(expected: ByteVector, supplied: ByteVector): Boolean = 51 | (expected eq supplied) || { 52 | val expectedLen = expected.size 53 | val suppliedLen = supplied.size 54 | 55 | val len = Math.min(expectedLen, suppliedLen) 56 | var nonEqual = expected.length ^ supplied.length; 57 | 58 | var i = 0L 59 | while (i != len) { 60 | nonEqual |= (expected(i) ^ supplied(i)) 61 | i += 1 62 | } 63 | i = len 64 | while (i < suppliedLen) { 65 | nonEqual |= (supplied(i) ^ ~supplied(i)) 66 | i += 1 67 | } 68 | 69 | nonEqual == 0 70 | } 71 | 72 | } 73 | 74 | } 75 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/bobcats/CryptoSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.IO 20 | import munit.CatsEffectSuite 21 | 22 | abstract class CryptoSuite extends CatsEffectSuite { 23 | 24 | private val cryptoFixture = ResourceSuiteLocalFixture( 25 | "crypto", 26 | Crypto.forAsync[IO] 27 | ) 28 | 29 | protected def runtime = BuildInfo.runtime 30 | protected def isBrowser = Set("Firefox", "Chrome").contains(runtime) 31 | 32 | override def munitFixtures = List(cryptoFixture) 33 | 34 | implicit protected def crypto: Crypto[IO] = cryptoFixture() 35 | implicit protected def hash: Hash[IO] = crypto.hash 36 | implicit protected def hmac: Hmac[IO] = crypto.hmac 37 | 38 | } 39 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/bobcats/HashSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.IO 20 | import scodec.bits._ 21 | import fs2.{Chunk, Stream} 22 | 23 | class HashSuite extends CryptoSuite { 24 | 25 | import HashAlgorithm._ 26 | 27 | val data = ByteVector.encodeAscii("The quick brown fox jumps over the lazy dog").toOption.get 28 | 29 | case class TestCase(algorithm: HashAlgorithm, data: ByteVector, digest: ByteVector) 30 | 31 | val testCases = List( 32 | TestCase(SHA1, data, hex"2fd4e1c67a2d28fced849ee1bb76e7391b93eb12"), 33 | TestCase(SHA1, ByteVector.empty, hex"da39a3ee5e6b4b0d3255bfef95601890afd80709"), 34 | TestCase( 35 | SHA256, 36 | data, 37 | hex"d7a8fbb307d7809469ca9abcb0082e4f8d5651e46d3cdb762d02d0bf37c9e592"), 38 | TestCase( 39 | SHA256, 40 | ByteVector.empty, 41 | hex"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"), 42 | TestCase( 43 | SHA512, 44 | data, 45 | hex"07e547d9586f6a73f73fbac0435ed76951218fb7d0c8d788a309d785436bbb642e93a252a954f23912547d1e8a3b5ed6e1bfd7097821233fa0538f3db854fee6"), 46 | TestCase( 47 | SHA512, 48 | ByteVector.empty, 49 | hex"cf83e1357eefb8bdf1542850d66d8007d620e4050b5715dc83f4a921d36ce9ce47d0d13c5d85f2b0ff8318d2877eec2f63b931bd47417a81a538327af927da3e" 50 | ), 51 | TestCase(MD5, data, hex"9e107d9d372bb6826bd81d3542a419d6") 52 | ) 53 | 54 | val supportedAlgorithms = { 55 | val all = Set[HashAlgorithm](MD5, SHA1, SHA256, SHA512) 56 | val browser = Set[HashAlgorithm](SHA1, SHA256, SHA512) 57 | Map( 58 | "JVM" -> all, 59 | "NodeJS" -> all, 60 | "Native" -> all, 61 | "Chrome" -> browser, 62 | "Firefox" -> browser 63 | ) 64 | } 65 | 66 | testCases.zipWithIndex.foreach { 67 | case (TestCase(alg, data, digest), counter) => 68 | test(s"Hash[IO].digest for ${alg} test case ${counter}") { 69 | assume( 70 | supportedAlgorithms(runtime).contains(alg), 71 | s"${runtime} does not support ${alg}") 72 | Hash[IO].digest(alg, data).assertEquals(digest) 73 | } 74 | test(s"Hash1[IO].digest for ${alg} test case ${counter}") { 75 | assume( 76 | supportedAlgorithms(runtime).contains(alg), 77 | s"${runtime} does not support ${alg}") 78 | Hash1.forAsync[IO](alg).use(_.digest(data)).assertEquals(digest) 79 | } 80 | test(s"Hash[IO].digestPipe for ${alg} test case ${counter}") { 81 | assume(!isBrowser, s"${runtime} does not support streaming") 82 | Stream 83 | .chunk(Chunk.byteVector(data)) 84 | .through(Hash[IO].digestPipe(alg)) 85 | .compile 86 | .to(ByteVector) 87 | .assertEquals(digest) 88 | } 89 | test(s"Hash1[IO].pipe for ${alg} test case ${counter}") { 90 | assume(!isBrowser, s"${runtime} does not support streaming") 91 | Hash1 92 | .forAsync[IO](alg) 93 | .use { hash1 => 94 | Stream.chunk(Chunk.byteVector(data)).through(hash1.pipe).compile.to(ByteVector) 95 | } 96 | .assertEquals(digest) 97 | } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/bobcats/HmacSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.IO 20 | import scodec.bits._ 21 | 22 | class HmacSuite extends CryptoSuite { 23 | 24 | import HmacAlgorithm._ 25 | 26 | case class TestCase(key: ByteVector, data: ByteVector, digest: ByteVector) 27 | 28 | // Test cases from RFC2022: https://datatracker.ietf.org/doc/html/rfc2202 29 | val sha1TestCases = List( 30 | TestCase( 31 | hex"0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b0b", 32 | ByteVector.encodeAscii("Hi There").toOption.get, 33 | hex"b617318655057264e28bc0b6fb378c8ef146be00" 34 | ), 35 | TestCase( 36 | ByteVector.encodeAscii("Jefe").toOption.get, 37 | ByteVector.encodeAscii("what do ya want for nothing?").toOption.get, 38 | hex"effcdf6ae5eb2fa2d27416d5f184df9c259a7c79" 39 | ), 40 | TestCase( 41 | hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", 42 | ByteVector.fromHex("dd" * 50).get, 43 | hex"125d7342b9ac11cd91a39af48aa17b4f63f175d3" 44 | ), 45 | TestCase( 46 | hex"0102030405060708090a0b0c0d0e0f10111213141516171819", 47 | ByteVector.fromHex("cd" * 50).get, 48 | hex"4c9007f4026250c6bc8414f9bf50c86c2d7235da" 49 | ), 50 | TestCase( 51 | hex"0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c0c", 52 | ByteVector.encodeAscii("Test With Truncation").toOption.get, 53 | hex"4c1a03424b55e07fe7f27be1d58bb9324a9a5a04" 54 | ), 55 | TestCase( 56 | ByteVector.fromHex("aa" * 80).get, 57 | ByteVector 58 | .encodeAscii("Test Using Larger Than Block-Size Key - Hash Key First") 59 | .toOption 60 | .get, 61 | hex"aa4ae5e15272d00e95705637ce8a3b55ed402112" 62 | ), 63 | TestCase( 64 | ByteVector.fromHex("aa" * 80).get, 65 | ByteVector 66 | .encodeAscii( 67 | "Test Using Larger Than Block-Size Key and Larger Than One Block-Size Data") 68 | .toOption 69 | .get, 70 | hex"e8e99d0f45237d786d6bbaa7965c7808bbff1a91" 71 | ) 72 | ) 73 | 74 | val key = ByteVector.encodeAscii("key").toOption.get 75 | val data = ByteVector.encodeAscii("The quick brown fox jumps over the lazy dog").toOption.get 76 | 77 | def testHmac(algorithm: HmacAlgorithm, expected: ByteVector) = 78 | test(s"$algorithm") { 79 | Hmac[IO].digest(SecretKeySpec(key, algorithm), data).assertEquals(expected) 80 | } 81 | 82 | def testHmacSha1(testCases: List[TestCase]) = 83 | testCases.zipWithIndex.foreach { 84 | case (TestCase(key, data, expected), idx) => 85 | test(s"SHA1 RFC2022 test case ${idx + 1}") { 86 | Hmac[IO].digest(SecretKeySpec(key, SHA1), data).assertEquals(expected) 87 | } 88 | } 89 | 90 | testHmacSha1(sha1TestCases) 91 | testHmac(SHA1, hex"de7c9b85b8b78aa6bc8a7a36f70a90701c9db4d9") 92 | testHmac(SHA256, hex"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8") 93 | testHmac( 94 | SHA512, 95 | hex"b42af09057bac1e2d41708e48a902e09b5ff7f12ab428a4fe86653c73dd248fb82f948a549f7b791a5b41915ee4d1ec3935357e4e2317250d0372afa2ebeeb3a") 96 | 97 | def testGenerateKey(algorithm: HmacAlgorithm) = 98 | test(s"generate key for ${algorithm}") { 99 | Hmac[IO].generateKey(algorithm).map { 100 | case SecretKeySpec(key, keyAlgorithm) => 101 | assertEquals(algorithm, keyAlgorithm) 102 | assert(key.size >= algorithm.minimumKeyLength) 103 | } 104 | } 105 | 106 | List(SHA1, SHA256, SHA512).foreach(testGenerateKey) 107 | } 108 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/bobcats/HotpSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.effect.IO 20 | import scodec.bits._ 21 | 22 | class HotpSuite extends CryptoSuite { 23 | 24 | val key = hex"3132333435363738393031323334353637383930" 25 | 26 | val expectedValues = List( 27 | 755224, 287082, 359152, 969429, 338314, 254676, 287922, 162583, 399871, 520489 28 | ) 29 | 30 | expectedValues.zipWithIndex.foreach { 31 | case (expected, counter) => 32 | test(s"RFC4226 test case ${counter}") { 33 | Hotp 34 | .generate[IO](SecretKeySpec(key, HmacAlgorithm.SHA1), counter.toLong, digits = 6) 35 | .map { obtained => assertEquals(obtained, expected) } 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /core/shared/src/test/scala/bobcats/SecureEqSuite.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | package bobcats 18 | 19 | import cats.kernel.laws.discipline.EqTests 20 | import munit.DisciplineSuite 21 | import org.scalacheck.Arbitrary 22 | import org.scalacheck.Cogen 23 | import org.scalacheck.Prop.forAll 24 | import scodec.bits.ByteVector 25 | 26 | class SecureEqSuite extends DisciplineSuite { 27 | 28 | implicit val arbitraryByteVector: Arbitrary[ByteVector] = Arbitrary( 29 | Arbitrary.arbitrary[Vector[Byte]].map(ByteVector(_))) 30 | 31 | implicit val cogenByteVector: Cogen[ByteVector] = 32 | Cogen[Vector[Byte]].contramap(_.toIndexedSeq.toVector) 33 | 34 | checkAll("SecureEq[ByteVector]", EqTests(SecureEq[ByteVector]).eqv) 35 | 36 | property("non-trivial reflexivity") { 37 | forAll { (bytes: Vector[Byte]) => 38 | SecureEq[ByteVector].eqv(ByteVector(bytes), ByteVector(bytes)) 39 | } 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /flake.lock: -------------------------------------------------------------------------------- 1 | { 2 | "nodes": { 3 | "devshell": { 4 | "inputs": { 5 | "flake-utils": "flake-utils", 6 | "nixpkgs": "nixpkgs" 7 | }, 8 | "locked": { 9 | "lastModified": 1678957337, 10 | "narHash": "sha256-Gw4nVbuKRdTwPngeOZQOzH/IFowmz4LryMPDiJN/ah4=", 11 | "owner": "numtide", 12 | "repo": "devshell", 13 | "rev": "3e0e60ab37cd0bf7ab59888f5c32499d851edb47", 14 | "type": "github" 15 | }, 16 | "original": { 17 | "owner": "numtide", 18 | "repo": "devshell", 19 | "type": "github" 20 | } 21 | }, 22 | "flake-utils": { 23 | "locked": { 24 | "lastModified": 1642700792, 25 | "narHash": "sha256-XqHrk7hFb+zBvRg6Ghl+AZDq03ov6OshJLiSWOoX5es=", 26 | "owner": "numtide", 27 | "repo": "flake-utils", 28 | "rev": "846b2ae0fc4cc943637d3d1def4454213e203cba", 29 | "type": "github" 30 | }, 31 | "original": { 32 | "owner": "numtide", 33 | "repo": "flake-utils", 34 | "type": "github" 35 | } 36 | }, 37 | "flake-utils_2": { 38 | "inputs": { 39 | "systems": "systems" 40 | }, 41 | "locked": { 42 | "lastModified": 1681202837, 43 | "narHash": "sha256-H+Rh19JDwRtpVPAWp64F+rlEtxUWBAQW28eAi3SRSzg=", 44 | "owner": "numtide", 45 | "repo": "flake-utils", 46 | "rev": "cfacdce06f30d2b68473a46042957675eebb3401", 47 | "type": "github" 48 | }, 49 | "original": { 50 | "owner": "numtide", 51 | "repo": "flake-utils", 52 | "type": "github" 53 | } 54 | }, 55 | "nixpkgs": { 56 | "locked": { 57 | "lastModified": 1677383253, 58 | "narHash": "sha256-UfpzWfSxkfXHnb4boXZNaKsAcUrZT9Hw+tao1oZxd08=", 59 | "owner": "NixOS", 60 | "repo": "nixpkgs", 61 | "rev": "9952d6bc395f5841262b006fbace8dd7e143b634", 62 | "type": "github" 63 | }, 64 | "original": { 65 | "owner": "NixOS", 66 | "ref": "nixpkgs-unstable", 67 | "repo": "nixpkgs", 68 | "type": "github" 69 | } 70 | }, 71 | "nixpkgs_2": { 72 | "locked": { 73 | "lastModified": 1682109806, 74 | "narHash": "sha256-d9g7RKNShMLboTWwukM+RObDWWpHKaqTYXB48clBWXI=", 75 | "owner": "nixos", 76 | "repo": "nixpkgs", 77 | "rev": "2362848adf8def2866fabbffc50462e929d7fffb", 78 | "type": "github" 79 | }, 80 | "original": { 81 | "owner": "nixos", 82 | "ref": "nixpkgs-unstable", 83 | "repo": "nixpkgs", 84 | "type": "github" 85 | } 86 | }, 87 | "root": { 88 | "inputs": { 89 | "flake-utils": [ 90 | "typelevel-nix", 91 | "flake-utils" 92 | ], 93 | "nixpkgs": [ 94 | "typelevel-nix", 95 | "nixpkgs" 96 | ], 97 | "typelevel-nix": "typelevel-nix" 98 | } 99 | }, 100 | "systems": { 101 | "locked": { 102 | "lastModified": 1681028828, 103 | "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 104 | "owner": "nix-systems", 105 | "repo": "default", 106 | "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 107 | "type": "github" 108 | }, 109 | "original": { 110 | "owner": "nix-systems", 111 | "repo": "default", 112 | "type": "github" 113 | } 114 | }, 115 | "typelevel-nix": { 116 | "inputs": { 117 | "devshell": "devshell", 118 | "flake-utils": "flake-utils_2", 119 | "nixpkgs": "nixpkgs_2" 120 | }, 121 | "locked": { 122 | "lastModified": 1682350622, 123 | "narHash": "sha256-Ht9ohZDynerA+MS7KaEtAN2ESRXaEYGGmdy5WJuCv6o=", 124 | "owner": "typelevel", 125 | "repo": "typelevel-nix", 126 | "rev": "8055d3901ee73e0ef9d411496774877db7f46a3b", 127 | "type": "github" 128 | }, 129 | "original": { 130 | "owner": "typelevel", 131 | "repo": "typelevel-nix", 132 | "type": "github" 133 | } 134 | } 135 | }, 136 | "root": "root", 137 | "version": 7 138 | } 139 | -------------------------------------------------------------------------------- /flake.nix: -------------------------------------------------------------------------------- 1 | { 2 | description = "Provision a dev environment"; 3 | 4 | inputs = { 5 | typelevel-nix.url = "github:typelevel/typelevel-nix"; 6 | nixpkgs.follows = "typelevel-nix/nixpkgs"; 7 | flake-utils.follows = "typelevel-nix/flake-utils"; 8 | }; 9 | 10 | outputs = { self, nixpkgs, flake-utils, typelevel-nix }: 11 | flake-utils.lib.eachDefaultSystem (system: 12 | let 13 | pkgs = import nixpkgs { 14 | inherit system; 15 | overlays = [ typelevel-nix.overlay ]; 16 | }; 17 | 18 | mkShell = jdk: pkgs.devshell.mkShell { 19 | imports = [ typelevel-nix.typelevelShell ]; 20 | name = "bobcats"; 21 | typelevelShell = { 22 | jdk.package = jdk; 23 | nodejs.enable = true; 24 | native.enable = true; 25 | nodejs.package = pkgs.nodejs-18_x; 26 | native.libraries = [ pkgs.openssl ]; 27 | }; 28 | }; 29 | in 30 | rec { 31 | devShell = mkShell pkgs.jdk8; 32 | 33 | devShells = { 34 | "temurin@8" = mkShell pkgs.temurin-bin-8; 35 | "temurin@11" = mkShell pkgs.temurin-bin-11; 36 | "temurin@17" = mkShell pkgs.temurin-bin-17; 37 | }; 38 | } 39 | ); 40 | } 41 | -------------------------------------------------------------------------------- /project/JSEnv.scala: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2021 Typelevel 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | sealed abstract class JSEnv 18 | object JSEnv { 19 | case object Chrome extends JSEnv 20 | case object Firefox extends JSEnv 21 | case object NodeJS extends JSEnv 22 | } 23 | -------------------------------------------------------------------------------- /project/build.properties: -------------------------------------------------------------------------------- 1 | sbt.version=1.11.1 2 | -------------------------------------------------------------------------------- /project/plugins.sbt: -------------------------------------------------------------------------------- 1 | libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" 2 | 3 | addSbtPlugin("org.typelevel" % "sbt-typelevel" % "0.8.0") 4 | addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.18.2") 5 | addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.13.1") 6 | addSbtPlugin("org.scala-native" % "sbt-scala-native" % "0.4.17") 7 | addSbtPlugin("org.portable-scala" % "sbt-scala-native-crossproject" % "1.3.2") 8 | --------------------------------------------------------------------------------