From 3f04d2f61a3db5515204b14ea603836a9a899dd0 Mon Sep 17 00:00:00 2001 From: chradek <51000525+chradek@users.noreply.github.com> Date: Thu, 21 Jan 2021 09:06:46 -0800 Subject: [PATCH] [core-crypto] initial core-crypto with sha256 hmac/hash functions (#13232) * [core-crypto] initial core-crypto with sha256 hmac/hash functions * [core-crypto] add entry in ci.yml * fix api review quotes * add webworker tests * [core-crypto] add supported protocols to README key concepts section * [core-crypto] use rollup config from dev-tool for node and browser builds * [core-crypto] update typedoc dep * update pnpm-lock.yaml * [core-crypto] fix README validation --- common/config/rush/pnpm-lock.yaml | 53 +++++++- rush.json | 5 + sdk/core/ci.yml | 2 + sdk/core/core-crypto/CHANGELOG.md | 3 + sdk/core/core-crypto/LICENSE | 21 +++ sdk/core/core-crypto/README.md | 38 ++++++ sdk/core/core-crypto/api-extractor.json | 31 +++++ sdk/core/core-crypto/karma.conf.js | 121 ++++++++++++++++++ sdk/core/core-crypto/package.json | 121 ++++++++++++++++++ .../core-crypto/review/core-crypto.api.md | 16 +++ sdk/core/core-crypto/rollup.config.js | 3 + sdk/core/core-crypto/rollup.test.config.js | 10 ++ .../core-crypto/rollup.webworker.config.js | 53 ++++++++ sdk/core/core-crypto/src/index.ts | 4 + sdk/core/core-crypto/src/sha256.browser.ts | 79 ++++++++++++ sdk/core/core-crypto/src/sha256.ts | 36 ++++++ .../core-crypto/src/utils/base64.browser.ts | 43 +++++++ sdk/core/core-crypto/src/utils/hex.ts | 24 ++++ .../core-crypto/src/utils/utf8.browser.ts | 34 +++++ .../test/internal/browser/base64.spec.ts | 43 +++++++ .../test/internal/browser/utf8.spec.ts | 21 +++ .../core-crypto/test/internal/hex.spec.ts | 30 +++++ .../public/browser/webworker-runner.spec.ts | 93 ++++++++++++++ .../test/public/browser/webworker.ts | 48 +++++++ .../core-crypto/test/public/sha256.spec.ts | 54 ++++++++ sdk/core/core-crypto/tsconfig.json | 8 ++ sdk/core/core-crypto/tsdoc.json | 4 + 27 files changed, 995 insertions(+), 3 deletions(-) create mode 100644 sdk/core/core-crypto/CHANGELOG.md create mode 100644 sdk/core/core-crypto/LICENSE create mode 100644 sdk/core/core-crypto/README.md create mode 100644 sdk/core/core-crypto/api-extractor.json create mode 100644 sdk/core/core-crypto/karma.conf.js create mode 100644 sdk/core/core-crypto/package.json create mode 100644 sdk/core/core-crypto/review/core-crypto.api.md create mode 100644 sdk/core/core-crypto/rollup.config.js create mode 100644 sdk/core/core-crypto/rollup.test.config.js create mode 100644 sdk/core/core-crypto/rollup.webworker.config.js create mode 100644 sdk/core/core-crypto/src/index.ts create mode 100644 sdk/core/core-crypto/src/sha256.browser.ts create mode 100644 sdk/core/core-crypto/src/sha256.ts create mode 100644 sdk/core/core-crypto/src/utils/base64.browser.ts create mode 100644 sdk/core/core-crypto/src/utils/hex.ts create mode 100644 sdk/core/core-crypto/src/utils/utf8.browser.ts create mode 100644 sdk/core/core-crypto/test/internal/browser/base64.spec.ts create mode 100644 sdk/core/core-crypto/test/internal/browser/utf8.spec.ts create mode 100644 sdk/core/core-crypto/test/internal/hex.spec.ts create mode 100644 sdk/core/core-crypto/test/public/browser/webworker-runner.spec.ts create mode 100644 sdk/core/core-crypto/test/public/browser/webworker.ts create mode 100644 sdk/core/core-crypto/test/public/sha256.spec.ts create mode 100644 sdk/core/core-crypto/tsconfig.json create mode 100644 sdk/core/core-crypto/tsdoc.json diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 4e6fdd2e46fe..e37bcb0dd06f 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -14,6 +14,7 @@ dependencies: '@rush-temp/core-asynciterator-polyfill': file:projects/core-asynciterator-polyfill.tgz '@rush-temp/core-auth': file:projects/core-auth.tgz '@rush-temp/core-client': file:projects/core-client.tgz + '@rush-temp/core-crypto': file:projects/core-crypto.tgz '@rush-temp/core-http': file:projects/core-http.tgz '@rush-temp/core-https': file:projects/core-https.tgz '@rush-temp/core-lro': file:projects/core-lro.tgz @@ -8454,7 +8455,7 @@ packages: dev: false name: '@rush-temp/communication-sms' resolution: - integrity: sha512-8AaJWwsqZoaoqcj6q/5B1BcWghScqy4YMI+rn3NuZ04ujnHLwCF41WKzdnKJTJmEv0FsgsR9IYHVr2macD/V/Q== + integrity: sha512-iPR48yPnG/If6ZId+OQ7B4DWgVYwHgvkOFoH+gNuJouU4SK1hWaPPdsXmYwqEnNdfpZ2ArsGX6PZBogp1BRLCQ== tarball: file:projects/communication-sms.tgz version: 0.0.0 file:projects/core-amqp.tgz: @@ -8612,6 +8613,51 @@ packages: integrity: sha512-ObPSjl0HbyDEzPqJAkS0muADwyxzKA0s9uBjHC0+a73GDavfv49K9+aiWOXQrpwhopDZsSU+cmuifF9IW5OTCg== tarball: file:projects/core-client.tgz version: 0.0.0 + file:projects/core-crypto.tgz: + dependencies: + '@microsoft/api-extractor': 7.7.11 + '@rollup/plugin-commonjs': 11.0.2_rollup@1.32.1 + '@rollup/plugin-json': 4.1.0_rollup@1.32.1 + '@rollup/plugin-multi-entry': 3.0.1_rollup@1.32.1 + '@rollup/plugin-node-resolve': 8.4.0_rollup@1.32.1 + '@rollup/plugin-replace': 2.3.4_rollup@1.32.1 + '@types/chai': 4.2.14 + '@types/mocha': 7.0.2 + '@types/node': 8.10.66 + '@types/sinon': 9.0.10 + assert: 1.5.0 + chai: 4.2.0 + cross-env: 7.0.3 + downlevel-dts: 0.4.0 + eslint: 7.17.0 + karma: 5.2.3 + karma-chrome-launcher: 3.1.0 + karma-coverage: 2.0.3 + karma-edge-launcher: 0.4.2_karma@5.2.3 + karma-env-preprocessor: 0.1.1 + karma-firefox-launcher: 1.3.0 + karma-ie-launcher: 1.0.0_karma@5.2.3 + karma-junit-reporter: 2.0.1_karma@5.2.3 + karma-mocha: 2.0.1 + karma-mocha-reporter: 2.2.5_karma@5.2.3 + karma-remap-istanbul: 0.6.0_karma@5.2.3 + mocha: 7.2.0 + mocha-junit-reporter: 1.23.3_mocha@7.2.0 + prettier: 1.19.1 + rimraf: 3.0.2 + rollup: 1.32.1 + rollup-plugin-sourcemaps: 0.4.2_rollup@1.32.1 + rollup-plugin-terser: 5.3.1_rollup@1.32.1 + sinon: 9.2.3 + tslib: 2.1.0 + typedoc: 0.15.2 + typescript: 4.1.2 + dev: false + name: '@rush-temp/core-crypto' + resolution: + integrity: sha512-qsLCHcp2BZqBHgtS6AVV48Pwu3H/usiUVM6nD2cIp60Q6C6Ns5kRybU1Y6NxkZDCN3vAIg7yxbbsD85DUv2oVw== + tarball: file:projects/core-crypto.tgz + version: 0.0.0 file:projects/core-http.tgz: dependencies: '@azure/core-tracing': 1.0.0-preview.9 @@ -9181,7 +9227,7 @@ packages: dev: false name: '@rush-temp/event-hubs' resolution: - integrity: sha512-LLX+FChWFahLsRLvxrBb42dCCzWYk2JOnNVJQlkebc69Omv+JL6UaAFfbcVWVXqCKprfD4Pj2GRrz/Szkqw1/g== + integrity: sha512-BU8R9nu5LezKcVnNKOHFeCI3Sc0NEeZe1RJR29MS7z98POmnzd4BjIibG6E5/bIUfiVYeZPQiUMyV9psNYYB2g== tarball: file:projects/event-hubs.tgz version: 0.0.0 file:projects/event-processor-host.tgz: @@ -9973,7 +10019,7 @@ packages: dev: false name: '@rush-temp/service-bus' resolution: - integrity: sha512-ZDEFdlVkTQN8KDPdUpiljVqEOASBhUTFzRWPcLQDdNKuTVxaSNSSk3FLMRcCmSA2cGzpDEUBYCUOxPPx0qeh+Q== + integrity: sha512-PvMjOlJpWARH1pN3kXRw+CbGA1Fa1/DwWTS8V5gNWpVkkATedt4peJZSqSaTvgwFgWKCpB7of0HzR90hW6RlIQ== tarball: file:projects/service-bus.tgz version: 0.0.0 file:projects/storage-blob-changefeed.tgz: @@ -10602,6 +10648,7 @@ specifiers: '@rush-temp/core-asynciterator-polyfill': file:./projects/core-asynciterator-polyfill.tgz '@rush-temp/core-auth': file:./projects/core-auth.tgz '@rush-temp/core-client': file:./projects/core-client.tgz + '@rush-temp/core-crypto': file:./projects/core-crypto.tgz '@rush-temp/core-http': file:./projects/core-http.tgz '@rush-temp/core-https': file:./projects/core-https.tgz '@rush-temp/core-lro': file:./projects/core-lro.tgz diff --git a/rush.json b/rush.json index c303f613b036..ce3974754dd1 100644 --- a/rush.json +++ b/rush.json @@ -402,6 +402,11 @@ "projectFolder": "sdk/core/core-client", "versionPolicyName": "core" }, + { + "packageName": "@azure/core-crypto", + "projectFolder": "sdk/core/core-crypto", + "versionPolicyName": "core" + }, { "packageName": "@azure/core-http", "projectFolder": "sdk/core/core-http", diff --git a/sdk/core/ci.yml b/sdk/core/ci.yml index a170ca3285f5..3829c83f5b5d 100644 --- a/sdk/core/ci.yml +++ b/sdk/core/ci.yml @@ -46,6 +46,8 @@ extends: safeName: azurecoreauth - name: azure-core-client safeName: azurecoreclient + - name: azure-core-crypto + safeName: azurecorecrypto - name: azure-core-http safeName: azurecorehttp - name: azure-core-https diff --git a/sdk/core/core-crypto/CHANGELOG.md b/sdk/core/core-crypto/CHANGELOG.md new file mode 100644 index 000000000000..dd06ac6d9985 --- /dev/null +++ b/sdk/core/core-crypto/CHANGELOG.md @@ -0,0 +1,3 @@ +# Release History + +## 1.0.0 (UNRELEASED) diff --git a/sdk/core/core-crypto/LICENSE b/sdk/core/core-crypto/LICENSE new file mode 100644 index 000000000000..ea8fb1516028 --- /dev/null +++ b/sdk/core/core-crypto/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2020 Microsoft + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/sdk/core/core-crypto/README.md b/sdk/core/core-crypto/README.md new file mode 100644 index 000000000000..d11dd6607564 --- /dev/null +++ b/sdk/core/core-crypto/README.md @@ -0,0 +1,38 @@ +# Azure Core Crypto client library for JavaScript (Experimental) + +This library is primarily intended to contain cryptographic helper functions for use by the Azure SDK for JavaScript. + +## Getting started + +### Requirements + +- [Node.js](https://nodejs.org) version > 8.x + +### Installation + +This package is not meant to be consumed directly by end users. + +## Key concepts + +This package currently supports the following protocols: + +- SHA-256 HMAC +- SHA-256 HASH + +## Examples + +Examples can be found in the `samples` folder. + +## Next steps + +TODO: ??? + +## Troubleshooting + +If you run into issues while using this library, please feel free to [file an issue](https://github.com/Azure/azure-sdk-for-js/issues/new). + +## Contributing + +If you'd like to contribute to this library, please read the [contributing guide](https://github.com/Azure/azure-sdk-for-js/blob/master/CONTRIBUTING.md) to learn more about how to build and test the code. + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-js%2Fsdk%2Fcore%2Fcore-crypto%2FREADME.png) diff --git a/sdk/core/core-crypto/api-extractor.json b/sdk/core/core-crypto/api-extractor.json new file mode 100644 index 000000000000..9625253284e5 --- /dev/null +++ b/sdk/core/core-crypto/api-extractor.json @@ -0,0 +1,31 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/api-extractor/v7/api-extractor.schema.json", + "mainEntryPointFilePath": "types/latest/src/index.d.ts", + "docModel": { + "enabled": true + }, + "apiReport": { + "enabled": true, + "reportFolder": "./review" + }, + "dtsRollup": { + "enabled": true, + "untrimmedFilePath": "", + "publicTrimmedFilePath": "./types/latest/core-crypto.d.ts" + }, + "messages": { + "tsdocMessageReporting": { + "default": { + "logLevel": "none" + } + }, + "extractorMessageReporting": { + "ae-missing-release-tag": { + "logLevel": "none" + }, + "ae-unresolved-link": { + "logLevel": "none" + } + } + } +} diff --git a/sdk/core/core-crypto/karma.conf.js b/sdk/core/core-crypto/karma.conf.js new file mode 100644 index 000000000000..e5de1f25e016 --- /dev/null +++ b/sdk/core/core-crypto/karma.conf.js @@ -0,0 +1,121 @@ +// https://github.com/karma-runner/karma-chrome-launcher +process.env.CHROME_BIN = require("puppeteer").executablePath(); + +module.exports = function(config) { + config.set({ + // base path that will be used to resolve all patterns (eg. files, exclude) + basePath: "./", + + // frameworks to use + // available frameworks: https://npmjs.org/browse/keyword/karma-adapter + frameworks: ["mocha"], + + plugins: [ + "karma-mocha", + "karma-mocha-reporter", + "karma-chrome-launcher", + "karma-edge-launcher", + "karma-firefox-launcher", + "karma-ie-launcher", + "karma-env-preprocessor", + "karma-coverage", + "karma-remap-istanbul", + "karma-junit-reporter" + ], + + // list of files / patterns to load in the browser + files: [ + // Uncomment the cdn link below for the polyfill service to support IE11 missing features + // Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys + // "https://cdn.polyfill.io/v2/polyfill.js?features=Symbol,Promise,String.prototype.startsWith,String.prototype.endsWith,String.prototype.repeat,String.prototype.includes,Array.prototype.includes,Object.keys|always", + "dist-test/index.browser.js", + "dist-test/webworker.js" + ], + + // list of files / patterns to exclude + exclude: [], + + // preprocess matching files before serving them to the browser + // available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor + preprocessors: { + "dist-test/index.browser.js": ["env"], + // IMPORTANT: COMMENT following line if you want to debug in your browsers!! + // Preprocess source file to calculate code coverage, however this will make source file unreadable + "test-browser/index.js": ["coverage"] + }, + + // inject following environment values into browser testing with window.__env__ + // environment values MUST be exported or set with same console running "karma start" + // https://www.npmjs.com/package/karma-env-preprocessor + // EXAMPLE: envPreprocessor: ["ACCOUNT_NAME", "ACCOUNT_SAS"], + + // test results reporter to use + // possible values: 'dots', 'progress' + // available reporters: https://npmjs.org/browse/keyword/karma-reporter + reporters: ["mocha", "coverage", "karma-remap-istanbul", "junit"], + + coverageReporter: { + // specify a common output directory + dir: "coverage-browser/", + reporters: [{ type: "json", subdir: ".", file: "coverage.json" }] + }, + + remapIstanbulReporter: { + src: "coverage-browser/coverage.json", + reports: { + lcovonly: "coverage-browser/lcov.info", + html: "coverage-browser/html/report", + "text-summary": null, + cobertura: "./coverage-browser/cobertura-coverage.xml" + } + }, + + junitReporter: { + outputDir: "", // results will be saved as $outputDir/$browserName.xml + outputFile: "test-results.browser.xml", // if included, results will be saved as $outputDir/$browserName/$outputFile + suite: "", // suite will become the package name attribute in xml testsuite element + useBrowserName: false, // add browser name to report and classes names + nameFormatter: undefined, // function (browser, result) to customize the name attribute in xml testcase element + classNameFormatter: undefined, // function (browser, result) to customize the classname attribute in xml testcase element + properties: {} // key value pair of properties to add to the section of the report + }, + + // web server port + port: 9876, + + // enable / disable colors in the output (reporters and logs) + colors: true, + + // level of logging + // possible values: config.LOG_DISABLE || config.LOG_ERROR || config.LOG_WARN || config.LOG_INFO || config.LOG_DEBUG + logLevel: config.LOG_INFO, + + // enable / disable watching file and executing tests whenever any file changes + autoWatch: false, + + // start these browsers + // available browser launchers: https://npmjs.org/browse/keyword/karma-launcher + // 'ChromeHeadless', 'Chrome', 'Firefox', 'Edge', 'IE' + browsers: ["ChromeHeadless"], + + // Continuous Integration mode + // if true, Karma captures browsers, runs the tests and exits + singleRun: true, + + // Concurrency level + // how many browser should be started simultaneous + concurrency: 1, + + browserNoActivityTimeout: 600000, + browserDisconnectTimeout: 10000, + browserDisconnectTolerance: 3, + + client: { + mocha: { + // change Karma's debug.html to the mocha web reporter + reporter: "html", + timeout: "600000" + } + } + }); +}; diff --git a/sdk/core/core-crypto/package.json b/sdk/core/core-crypto/package.json new file mode 100644 index 000000000000..df02f7384c97 --- /dev/null +++ b/sdk/core/core-crypto/package.json @@ -0,0 +1,121 @@ +{ + "name": "@azure/core-crypto", + "version": "1.0.0", + "description": "Core library for exposing crypto helpers.", + "sdk-type": "client", + "main": "dist/index.js", + "module": "dist-esm/src/index.js", + "browser": { + "./dist-esm/src/sha256.js": "./dist-esm/src/sha256.browser.js" + }, + "types": "types/latest/core-crypto.d.ts", + "typesVersions": { + "<3.6": { + "types/latest/*": [ + "types/3.1/*" + ] + } + }, + "scripts": { + "audit": "node ../../../common/scripts/rush-audit.js && rimraf node_modules package-lock.json && npm i --package-lock-only 2>&1 && npm audit", + "build:browser": "npm run build:ts && cross-env ONLY_BROWSER=true rollup -c 2>&1", + "build:node": "npm run build:ts && cross-env ONLY_NODE=true rollup -c 2>&1", + "build:samples": "echo Skipped.", + "build:test": "npm run build:ts && npm run bundle:test", + "build:test:browser": "npm run build:ts && npm run bundle:test:browser", + "build:test:node": "npm run build:ts && npm run bundle:test:node", + "build:ts": "tsc -p .", + "build:types": "downlevel-dts types/latest/ types/3.1/", + "build": "npm run build:ts && rollup -c 2>&1 && api-extractor run --local && npm run build:types", + "bundle:test": "rollup -c rollup.test.config.js 2>&1", + "bundle:test:browser": "cross-env ONLY_BROWSER=true rollup -c rollup.test.config.js 2>&1", + "bundle:test:node": "cross-env ONLY_NODE=true rollup -c rollup.test.config.js 2>&1", + "check-format": "prettier --list-different \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "clean": "rimraf dist dist-* types *.tgz *.log", + "execute:samples": "echo skipped", + "extract-api": "npm run build:ts && api-extractor run --local", + "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\" \"*.{js,json}\"", + "integration-test:browser": "echo skipped", + "integration-test:node": "echo skipped", + "integration-test": "npm run integration-test:node && npm run integration-test:browser", + "lint:fix": "eslint package.json api-extractor.json src test --ext .ts --fix --fix-type [problem,suggestion]", + "lint": "eslint package.json api-extractor.json src test --ext .ts", + "pack": "npm pack 2>&1", + "prebuild": "npm run clean", + "test:browser": "npm run build:test:browser && npm run unit-test:browser && npm run integration-test:browser", + "test:node": "npm run build:test:node && npm run unit-test:node && npm run integration-test:node", + "test": "npm run clean && npm run build:ts && npm run bundle:test:node && npm run unit-test:node && npm run bundle:test:browser && npm run unit-test:browser && npm run integration-test:node && npm run integration-test:browser", + "unit-test:browser": "karma start --single-run", + "unit-test:node": "cross-env TS_NODE_FILES=true mocha -r esm -r ts-node/register --timeout 50000 --reporter ../../../common/tools/mocha-multi-reporter.js --colors --exclude \"test/**/borwser/*.spec.ts\" \"test/*/*.spec.ts\"", + "unit-test": "npm run unit-test:node && npm run unit-test:browser", + "docs": "typedoc --excludePrivate --excludeNotExported --excludeExternals --mode file --out ./dist/docs ./src" + }, + "files": [ + "dist/", + "dist-esm/src/", + "types/latest/core-crypto.d.ts", + "types/3.1/core-crypto.d.ts", + "README.md", + "LICENSE" + ], + "repository": "github:Azure/azure-sdk-for-js", + "keywords": [ + "azure", + "cloud" + ], + "author": "Microsoft Corporation", + "license": "MIT", + "bugs": { + "url": "https://github.com/Azure/azure-sdk-for-js/issues" + }, + "engines": { + "node": ">=8.0.0" + }, + "homepage": "https://github.com/Azure/azure-sdk-for-js/blob/master/sdk/core/core-crypto/", + "sideEffects": false, + "prettier": "@azure/eslint-plugin-azure-sdk/prettier.json", + "dependencies": { + "@azure/abort-controller": "^1.0.0", + "tslib": "^2.0.0" + }, + "devDependencies": { + "@azure/dev-tool": "^1.0.0", + "@microsoft/api-extractor": "7.7.11", + "@rollup/plugin-commonjs": "11.0.2", + "@rollup/plugin-json": "^4.0.0", + "@rollup/plugin-multi-entry": "^3.0.0", + "@rollup/plugin-node-resolve": "^8.0.0", + "@rollup/plugin-replace": "^2.2.0", + "@types/chai": "^4.1.6", + "@types/mocha": "^7.0.2", + "@types/node": "^8.0.0", + "@types/sinon": "^9.0.4", + "@azure/eslint-plugin-azure-sdk": "^3.0.0", + "assert": "^1.4.1", + "chai": "^4.2.0", + "downlevel-dts": "~0.4.0", + "cross-env": "^7.0.2", + "eslint": "^7.15.0", + "karma": "^5.1.0", + "karma-chrome-launcher": "^3.0.0", + "karma-coverage": "^2.0.0", + "karma-edge-launcher": "^0.4.2", + "karma-env-preprocessor": "^0.1.1", + "karma-firefox-launcher": "^1.1.0", + "karma-ie-launcher": "^1.0.0", + "karma-junit-reporter": "^2.0.1", + "karma-mocha": "^2.0.1", + "karma-mocha-reporter": "^2.2.5", + "karma-remap-istanbul": "^0.6.0", + "mocha": "^7.1.1", + "mocha-junit-reporter": "^1.18.0", + "prettier": "^1.16.4", + "rimraf": "^3.0.0", + "rollup": "^1.16.3", + "rollup-plugin-sourcemaps": "^0.4.2", + "rollup-plugin-terser": "^5.1.1", + "sinon": "^9.0.2", + "typescript": "4.1.2", + "typedoc": "0.15.2" + } +} diff --git a/sdk/core/core-crypto/review/core-crypto.api.md b/sdk/core/core-crypto/review/core-crypto.api.md new file mode 100644 index 000000000000..636bb267c6db --- /dev/null +++ b/sdk/core/core-crypto/review/core-crypto.api.md @@ -0,0 +1,16 @@ +## API Report File for "@azure/core-crypto" + +> Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). + +```ts + +// @public +export function computeSha256Hash(content: string, encoding: "base64" | "hex"): Promise; + +// @public +export function computeSha256Hmac(key: string, stringToSign: string, encoding: "base64" | "hex"): Promise; + + +// (No @packageDocumentation comment for this package) + +``` diff --git a/sdk/core/core-crypto/rollup.config.js b/sdk/core/core-crypto/rollup.config.js new file mode 100644 index 000000000000..5d7deee44c14 --- /dev/null +++ b/sdk/core/core-crypto/rollup.config.js @@ -0,0 +1,3 @@ +import { makeConfig } from "@azure/dev-tool/shared-config/rollup"; + +export default makeConfig(require("./package.json")); diff --git a/sdk/core/core-crypto/rollup.test.config.js b/sdk/core/core-crypto/rollup.test.config.js new file mode 100644 index 000000000000..cbbb96c676d1 --- /dev/null +++ b/sdk/core/core-crypto/rollup.test.config.js @@ -0,0 +1,10 @@ +import { makeConfig } from "@azure/dev-tool/shared-config/rollup"; +import * as base from "./rollup.webworker.config"; + +const inputs = makeConfig(require("./package.json")); + +if (!process.env.ONLY_NODE) { + inputs.push(base.webworkerConfig()); +} + +export default inputs; diff --git a/sdk/core/core-crypto/rollup.webworker.config.js b/sdk/core/core-crypto/rollup.webworker.config.js new file mode 100644 index 000000000000..5136f65926f8 --- /dev/null +++ b/sdk/core/core-crypto/rollup.webworker.config.js @@ -0,0 +1,53 @@ +import path from "path"; +import nodeResolve from "@rollup/plugin-node-resolve"; +import cjs from "@rollup/plugin-commonjs"; +import replace from "@rollup/plugin-replace"; +import sourcemaps from "rollup-plugin-sourcemaps"; + +export function webworkerConfig() { + const baseConfig = { + input: "dist-esm/test/public/browser/webworker.js", + output: { + file: "dist-test/webworker.js", + format: "iife", + sourcemap: true + }, + preserveSymlinks: false, + plugins: [ + sourcemaps(), + replace({ + delimiters: ["", ""], + values: { + // replace dynamic checks with if (false) since this is for + // browser only. Rollup's dead code elimination will remove + // any code guarded by if (isNode) { ... } + "if (isNode)": "if (false)" + } + }), + nodeResolve({ + mainFields: ["module", "browser"], + preferBuiltins: false + }), + cjs() + ] + }; + + baseConfig.onwarn = (warning) => { + if ( + warning.code === "CIRCULAR_DEPENDENCY" && + warning.importer.indexOf(path.normalize("node_modules/chai/lib") === 0) + ) { + // Chai contains circular references, but they are not fatal and can be ignored. + return; + } + + console.error(`(!) ${warning.message}`); + }; + + // Disable tree-shaking of test code. In rollup-plugin-node-resolve@5.0.0, rollup started respecting + // the "sideEffects" field in package.json. Since our package.json sets "sideEffects=false", this also + // applies to test code, which causes all tests to be removed by tree-shaking. + baseConfig.treeshake = false; + + return baseConfig; +} diff --git a/sdk/core/core-crypto/src/index.ts b/sdk/core/core-crypto/src/index.ts new file mode 100644 index 000000000000..a9d2aa3d159e --- /dev/null +++ b/sdk/core/core-crypto/src/index.ts @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +export { computeSha256Hash, computeSha256Hmac } from "./sha256"; diff --git a/sdk/core/core-crypto/src/sha256.browser.ts b/sdk/core/core-crypto/src/sha256.browser.ts new file mode 100644 index 000000000000..5dec1698b4fa --- /dev/null +++ b/sdk/core/core-crypto/src/sha256.browser.ts @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +import { base64ToBytes, bufferToBase64 } from "./utils/base64.browser"; +import { bufferToHex } from "./utils/hex"; +import { utf8ToBytes } from "./utils/utf8.browser"; + +let subtleCrypto: SubtleCrypto | undefined; + +/** + * Returns a cached reference to the Web API crypto.subtle object. + * @hidden + * @internal + */ +function getCrypto(): SubtleCrypto { + if (subtleCrypto) { + return subtleCrypto; + } + + if (!self.crypto || !self.crypto.subtle) { + throw new Error("Your browser environment does not support cryptography functions."); + } + + subtleCrypto = self.crypto.subtle; + return subtleCrypto; +} + +const importParams: HmacImportParams = { + name: "HMAC", + hash: { name: "SHA-256" } +}; + +/** + * Generates a SHA-256 HMAC signature. + * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. + * @param stringToSign - The data to be signed. + * @param encoding - The textual encoding to use for the returned HMAC digest. + */ +export async function computeSha256Hmac( + key: string, + stringToSign: string, + encoding: "base64" | "hex" +): Promise { + const crypto = getCrypto(); + const keyBytes = base64ToBytes(key); + const stringToSignBytes = utf8ToBytes(stringToSign); + + const cryptoKey = await crypto.importKey("raw", keyBytes, importParams, false, ["sign"]); + const signature = await crypto.sign(importParams, cryptoKey, stringToSignBytes); + + switch (encoding) { + case "base64": + return bufferToBase64(signature); + case "hex": + return bufferToHex(signature); + } +} + +/** + * Generates a SHA-256 hash. + * @param content - The data to be included in the hash. + * @param encoding - The textual encoding to use for the returned hash. + */ +export async function computeSha256Hash( + content: string, + encoding: "base64" | "hex" +): Promise { + const contentBytes = utf8ToBytes(content); + const digest = await getCrypto().digest("SHA-256", contentBytes); + + switch (encoding) { + case "base64": + return bufferToBase64(digest); + case "hex": + return bufferToHex(digest); + } +} diff --git a/sdk/core/core-crypto/src/sha256.ts b/sdk/core/core-crypto/src/sha256.ts new file mode 100644 index 000000000000..fb3ef6fe8d1c --- /dev/null +++ b/sdk/core/core-crypto/src/sha256.ts @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { createHash, createHmac } from "crypto"; + +/** + * Generates a SHA-256 HMAC signature. + * @param key - The HMAC key represented as a base64 string, used to generate the cryptographic HMAC hash. + * @param stringToSign - The data to be signed. + * @param encoding - The textual encoding to use for the returned HMAC digest. + */ +export async function computeSha256Hmac( + key: string, + stringToSign: string, + encoding: "base64" | "hex" +): Promise { + const decodedKey = Buffer.from(key, "base64"); + + return createHmac("sha256", decodedKey) + .update(stringToSign) + .digest(encoding); +} + +/** + * Generates a SHA-256 hash. + * @param content - The data to be included in the hash. + * @param encoding - The textual encoding to use for the returned hash. + */ +export async function computeSha256Hash( + content: string, + encoding: "base64" | "hex" +): Promise { + return createHash("sha256") + .update(content) + .digest(encoding); +} diff --git a/sdk/core/core-crypto/src/utils/base64.browser.ts b/sdk/core/core-crypto/src/utils/base64.browser.ts new file mode 100644 index 000000000000..b48a16c54b86 --- /dev/null +++ b/sdk/core/core-crypto/src/utils/base64.browser.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +/** + * Converts a base64 string into a byte array. + * @param content - The base64 string to convert. + * @hidden + * @internal + */ +export function base64ToBytes(content: string): Uint8Array { + if (typeof atob !== "function") { + throw new Error(`Your browser environment is missing the global "atob" function.`); + } + + const binary = atob(content); + const bytes = new Uint8Array(binary.length); + for (let i = 0; i < binary.length; i++) { + bytes[i] = binary.charCodeAt(i); + } + + return bytes; +} + +/** + * Converts an ArrayBuffer to base64 string. + * @param buffer - Raw binary data. + * @hidden + * @internal + */ +export function bufferToBase64(buffer: ArrayBuffer): string { + if (typeof btoa !== "function") { + throw new Error(`Your browser environment is missing the global "btoa" function.`); + } + + const bytes = new Uint8Array(buffer); + let binary = ""; + for (const byte of bytes) { + binary += String.fromCharCode(byte); + } + return btoa(binary); +} diff --git a/sdk/core/core-crypto/src/utils/hex.ts b/sdk/core/core-crypto/src/utils/hex.ts new file mode 100644 index 000000000000..39968254b2fe --- /dev/null +++ b/sdk/core/core-crypto/src/utils/hex.ts @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/** + * Converts an ArrayBuffer to a hexadecimal string. + * @param buffer - Raw binary data. + * @hidden + * @internal + */ +export function bufferToHex(buffer: ArrayBuffer): string { + const bytes = new Uint8Array(buffer); + return Array.prototype.map.call(bytes, byteToHex).join(""); +} + +/** + * Converts a byte to a hexadecimal string. + * @param byte - An integer representation of a byte. + * @hidden + * @internal + */ +function byteToHex(byte: number): string { + const hex = byte.toString(16); + return hex.length === 2 ? hex : `0${hex}`; +} diff --git a/sdk/core/core-crypto/src/utils/utf8.browser.ts b/sdk/core/core-crypto/src/utils/utf8.browser.ts new file mode 100644 index 000000000000..5008d69cf3be --- /dev/null +++ b/sdk/core/core-crypto/src/utils/utf8.browser.ts @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +/// + +let encoder: TextEncoder | undefined; + +/** + * Returns a cached TextEncoder. + * @hidden + * @internal + */ +function getTextEncoder(): TextEncoder { + if (encoder) { + return encoder; + } + + if (typeof TextEncoder === "undefined") { + throw new Error(`Your browser environment is missing "TextEncoder".`); + } + + encoder = new TextEncoder(); + return encoder; +} + +/** + * Converts a utf8 string into a byte array. + * @param content - The utf8 string to convert. + * @hidden + * @internal + */ +export function utf8ToBytes(content: string): Uint8Array { + return getTextEncoder().encode(content); +} diff --git a/sdk/core/core-crypto/test/internal/browser/base64.spec.ts b/sdk/core/core-crypto/test/internal/browser/base64.spec.ts new file mode 100644 index 000000000000..6f23bd287271 --- /dev/null +++ b/sdk/core/core-crypto/test/internal/browser/base64.spec.ts @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import assert from "assert"; + +import { base64ToBytes, bufferToBase64 } from "../../../src/utils/base64.browser"; + +describe("Base64", function() { + describe("base64ToBytes", function() { + it("converts a base64 string to bytes", function() { + const input = "YXp1cmU="; // 'azure' in utf8. + + const output = base64ToBytes(input); + assert.deepEqual( + output, + new Uint8Array([97, 122, 117, 114, 101]), + "Incorrect conversion of base64 to bytes." + ); + }); + }); + + describe("bufferToBase64", function() { + it("converts an ArrayBuffer to a base64 string", function() { + const input = new Uint8Array([97, 122, 117, 114, 101]).buffer; // 'azure' in utf8. + + const output = bufferToBase64(input); + assert.deepEqual(output, "YXp1cmU=", "Incorrect conversion of bytes to base64."); + }); + + it("has proper padding", function() { + const scenarios = [ + { bytes: new Uint8Array([65]), expected: "QQ==" }, + { bytes: new Uint8Array([65, 90]), expected: "QVo=" }, + { bytes: new Uint8Array([65, 66, 67]), expected: "QUJD" } + ]; + + for (const scenario of scenarios) { + const output = bufferToBase64(scenario.bytes.buffer); + assert.equal(output, scenario.expected, "Incorrect conversion of bytes to base64."); + } + }); + }); +}); diff --git a/sdk/core/core-crypto/test/internal/browser/utf8.spec.ts b/sdk/core/core-crypto/test/internal/browser/utf8.spec.ts new file mode 100644 index 000000000000..caedfe149c37 --- /dev/null +++ b/sdk/core/core-crypto/test/internal/browser/utf8.spec.ts @@ -0,0 +1,21 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import assert from "assert"; + +import { utf8ToBytes } from "../../../src/utils/utf8.browser"; + +describe("utf-8", function() { + describe("utf8ToBytes", function() { + it("converts a utf-8 string to bytes", function() { + const input = "azure"; // 'azure' in utf8. + + const output = utf8ToBytes(input); + assert.deepEqual( + output, + new Uint8Array([97, 122, 117, 114, 101]), + "Incorrect conversion of utf-8 to bytes." + ); + }); + }); +}); diff --git a/sdk/core/core-crypto/test/internal/hex.spec.ts b/sdk/core/core-crypto/test/internal/hex.spec.ts new file mode 100644 index 000000000000..be05588c0c66 --- /dev/null +++ b/sdk/core/core-crypto/test/internal/hex.spec.ts @@ -0,0 +1,30 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import assert from "assert"; + +import { bufferToHex } from "../../src/utils/hex"; + +function generateValidBytes(): Uint8Array { + const bytes = new Uint8Array(256); + return bytes.map((_, index) => index); +} + +describe("Hex", function() { + describe("bufferToHex", function() { + it("encodes a byte", function() { + const bytes = generateValidBytes(); + bytes.forEach((_, index) => { + const hex = bufferToHex(bytes.buffer.slice(index, index + 1)); + assert.equal(hex.length, 2, "Unexpected length for hex value."); + assert.equal(parseInt(hex, 16), index, "Unexpected hex value."); + }); + }); + + it("encodes bytes", function() { + const bytes = new Uint8Array([97, 122, 117, 114, 101]); // 'azure' in utf8 + const hex = bufferToHex(bytes.buffer); + assert.equal(hex, "617a757265", "Unexpected hex value."); + }); + }); +}); diff --git a/sdk/core/core-crypto/test/public/browser/webworker-runner.spec.ts b/sdk/core/core-crypto/test/public/browser/webworker-runner.spec.ts new file mode 100644 index 000000000000..53237d38373f --- /dev/null +++ b/sdk/core/core-crypto/test/public/browser/webworker-runner.spec.ts @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import assert from "assert"; + +import { HashMessage, HmacMessage } from "./webworker"; + +describe("SHA-256 (WebWorker)", function() { + const worker = new Worker("/base/dist-test/webworker.js"); + + function postMessage(message: HashMessage | HmacMessage): Promise { + return new Promise((resolve) => { + worker.postMessage(message); + const listener = (event: MessageEvent): void => { + worker.removeEventListener("message", listener); + resolve(event.data); + }; + worker.addEventListener("message", listener); + }); + } + + after(function() { + worker.terminate(); + }); + + describe("Hash", function() { + it("base64 encoding", async function() { + const hash = await postMessage({ + type: "hash", + algorithm: "sha256", + content: "azure", + encoding: "base64" + }); + + assert.equal( + hash, + "efQ8O5wj+98pO9uoDas4uhlPwdp5GHCRs9N2Da2EC/g=", + "The computed hash does not match the expected value." + ); + }); + + it("hex encoding", async function() { + const hash = await postMessage({ + type: "hash", + algorithm: "sha256", + content: "azure", + encoding: "hex" + }); + + assert.equal( + hash, + "79f43c3b9c23fbdf293bdba80dab38ba194fc1da79187091b3d3760dad840bf8", + "The computed hash does not match the expected value." + ); + }); + }); + + describe("HMAC", function() { + const base64EncodedKey = "c2VjcmV0"; // 'secret' in utf8. + + it("base64 encoding", async function() { + const hmac = await postMessage({ + type: "hmac", + algorithm: "sha256", + key: base64EncodedKey, + stringToSign: "azure", + encoding: "base64" + }); + + assert.equal( + hmac, + "AJ/qUoDtgLeA1A5ND2AS3uF7hsSe9O7imtXlkAj8VR4=", + "The computed hmac does not match the expected value." + ); + }); + + it("hex encoding", async function() { + const hmac = await postMessage({ + type: "hmac", + algorithm: "sha256", + key: base64EncodedKey, + stringToSign: "azure", + encoding: "hex" + }); + + assert.equal( + hmac, + "009fea5280ed80b780d40e4d0f6012dee17b86c49ef4eee29ad5e59008fc551e", + "The computed hmac does not match the expected value." + ); + }); + }); +}); diff --git a/sdk/core/core-crypto/test/public/browser/webworker.ts b/sdk/core/core-crypto/test/public/browser/webworker.ts new file mode 100644 index 000000000000..5b6b9c27fcce --- /dev/null +++ b/sdk/core/core-crypto/test/public/browser/webworker.ts @@ -0,0 +1,48 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import { computeSha256Hash, computeSha256Hmac } from "../../../src/index"; + +export interface HashMessage { + type: "hash"; + algorithm: "sha256"; + encoding: "base64" | "hex"; + content: string; +} + +export interface HmacMessage { + type: "hmac"; + algorithm: "sha256"; + encoding: "base64" | "hex"; + key: string; + stringToSign: string; +} + +onmessage = async (event: MessageEvent) => { + let result: string | undefined; + switch (event.data.type) { + case "hash": + result = await handleHashMessage(event.data); + break; + case "hmac": + result = await handleHmacMessage(event.data); + break; + } + + // https://github.com/microsoft/TypeScript/issues/20595 + ((self as unknown) as Worker).postMessage(result); +}; + +function handleHashMessage(message: HashMessage): Promise { + if (message.algorithm === "sha256") { + return computeSha256Hash(message.content, message.encoding); + } + return Promise.resolve(""); +} + +function handleHmacMessage(message: HmacMessage): Promise { + if (message.algorithm === "sha256") { + return computeSha256Hmac(message.key, message.stringToSign, message.encoding); + } + return Promise.resolve(""); +} diff --git a/sdk/core/core-crypto/test/public/sha256.spec.ts b/sdk/core/core-crypto/test/public/sha256.spec.ts new file mode 100644 index 000000000000..a0809592a380 --- /dev/null +++ b/sdk/core/core-crypto/test/public/sha256.spec.ts @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. + +import assert from "assert"; + +import { computeSha256Hash, computeSha256Hmac } from "../../src/index"; + +describe("SHA-256", function() { + describe("Hash", function() { + it("base64 encoding", async function() { + const hash = await computeSha256Hash("azure", "base64"); + + assert.equal( + hash, + "efQ8O5wj+98pO9uoDas4uhlPwdp5GHCRs9N2Da2EC/g=", + "The computed hash does not match the expected value." + ); + }); + + it("hex encoding", async function() { + const hash = await computeSha256Hash("azure", "hex"); + + assert.equal( + hash, + "79f43c3b9c23fbdf293bdba80dab38ba194fc1da79187091b3d3760dad840bf8", + "The computed hash does not match the expected value." + ); + }); + }); + + describe("HMAC", function() { + const base64EncodedKey = "c2VjcmV0"; // 'secret' in utf8. + + it("base64 encoding", async function() { + const hmac = await computeSha256Hmac(base64EncodedKey, "azure", "base64"); + + assert.equal( + hmac, + "AJ/qUoDtgLeA1A5ND2AS3uF7hsSe9O7imtXlkAj8VR4=", + "The computed hmac does not match the expected value." + ); + }); + + it("hex encoding", async function() { + const hmac = await computeSha256Hmac(base64EncodedKey, "azure", "hex"); + + assert.equal( + hmac, + "009fea5280ed80b780d40e4d0f6012dee17b86c49ef4eee29ad5e59008fc551e", + "The computed hmac does not match the expected value." + ); + }); + }); +}); diff --git a/sdk/core/core-crypto/tsconfig.json b/sdk/core/core-crypto/tsconfig.json new file mode 100644 index 000000000000..4a53cc511418 --- /dev/null +++ b/sdk/core/core-crypto/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../../tsconfig.package", + "compilerOptions": { + "outDir": "./dist-esm", + "declarationDir": "./types/latest" + }, + "exclude": ["node_modules", "types", "temp", "browser", "dist", "dist-esm", "./samples/**/*.ts"] +} diff --git a/sdk/core/core-crypto/tsdoc.json b/sdk/core/core-crypto/tsdoc.json new file mode 100644 index 000000000000..81c5a8a2aa2f --- /dev/null +++ b/sdk/core/core-crypto/tsdoc.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://developer.microsoft.com/json-schemas/tsdoc/v0/tsdoc.schema.json", + "extends": ["../../../tsdoc.json"] +}