diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63b61d2b..2275d8c3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: subcommands: | python3 scripts/download_wasm.py npm install - kms-version: 4.6.0 + kms-version: 4.7.0 findex-cloud-version: 0.3.1 lint: @@ -33,7 +33,7 @@ jobs: with: subcommands: | npm test - kms-version: 4.6.0 + kms-version: 4.7.0 kms-jwe-key: '{"kty": "OKP","d": "MPEVJwdRqGM_qhJOUb5hR0Xr9EvwMLZGnkf-eDj5fU8","use": "enc","crv": "X25519","kid": "DX3GC+Fx3etxfRJValQNbqaB0gs=","x": "gdF-1TtAjsFqNWr9nwhGUlFG38qrDUqYgcILgtYrpTY","alg": "ECDH-ES"}' findex-cloud-version: 0.3.1 @@ -50,7 +50,7 @@ jobs: extension: so destination: linux-x86-64 os: ubuntu-20.04 - kms-version: 4.6.0 + kms-version: 4.7.0 findex-cloud-version: 0.3.1 copy_fresh_build: false copy_regression_files: | @@ -64,7 +64,7 @@ jobs: with: branch: develop target: x86_64-unknown-linux-gnu - kms-version: 4.6.0 + kms-version: 4.7.0 copy_fresh_build: false copy_regression_files: | cp ./cloudproof_js/non_regression_vector.json tests/data/cover_crypt/non_regression/js_non_regression_vector.json @@ -95,7 +95,7 @@ jobs: sleep 5 cd ../test node chrome.mjs http://localhost:8090 http://kms:9998 - kms-version: 4.6.0 + kms-version: 4.7.0 findex-cloud-version: 0.3.1 example_reactjs: @@ -110,7 +110,7 @@ jobs: sleep 5 cd ../test node chrome.mjs http://localhost:8090 http://kms:9998 - kms-version: 4.6.0 + kms-version: 4.7.0 findex-cloud-version: 0.3.1 example_browser: @@ -123,7 +123,7 @@ jobs: python3 -m http.server & sleep 3 node test.mjs - kms-version: 4.6.0 + kms-version: 4.7.0 findex-cloud-version: 0.3.1 example_webpack: @@ -143,7 +143,7 @@ jobs: cd examples/nodejs npm install node test.mjs 10 - kms-version: 4.6.0 + kms-version: 4.7.0 findex-cloud-version: 0.3.1 example_imdb: diff --git a/.github/workflows/js.yml b/.github/workflows/js.yml index 04e31225..788177d1 100644 --- a/.github/workflows/js.yml +++ b/.github/workflows/js.yml @@ -23,9 +23,9 @@ jobs: node_modules dist src/pkg - key: ${{ runner.os }}-node-${{ github.ref_name }}-${{ github.head_ref }}-${{ hashFiles('package.json') }} + key: ${{ runner.os }}-node-${{ hashFiles('package.json') }}-${{ hashFiles('src/**', '!.git') }} restore-keys: | - ${{ runner.os }}-node-${{ github.ref_name }}-${{ github.head_ref }}-${{ hashFiles('package.json') }} + ${{ runner.os }}-node-${{ hashFiles('package.json') }}-${{ hashFiles('src/**', '!.git') }} - run: ${{ inputs.subcommands }} env: diff --git a/.github/workflows/js_in_docker.yml b/.github/workflows/js_in_docker.yml index 5c70522e..9516d117 100644 --- a/.github/workflows/js_in_docker.yml +++ b/.github/workflows/js_in_docker.yml @@ -60,9 +60,9 @@ jobs: node_modules dist src/pkg - key: ${{ runner.os }}-docker-node-${{ github.ref_name }}-${{ github.head_ref }}-${{ hashFiles('package.json') }} + key: ${{ runner.os }}-docker-node-${{ hashFiles('package.json') }}-${{ hashFiles('src/**', '!.git') }} restore-keys: | - ${{ runner.os }}-docker-node-${{ github.ref_name }}-${{ github.head_ref }}-${{ hashFiles('package.json') }} + ${{ runner.os }}-docker-node-${{ hashFiles('package.json') }}-${{ hashFiles('src/**', '!.git') }} - name: Test run: ${{ inputs.subcommands }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6b5a72f0..ad670f7c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -7,7 +7,7 @@ # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks -exclude: dist|tests/data|tests/findex.test.ts|src/pkg|examples/full_text_search/data +exclude: dist|tests/data|tests/findex.test.ts|src/pkg|examples/full_text_search/data/|docker-compose.yml repos: - repo: https://github.com/compilerla/conventional-pre-commit rev: v2.3.0 diff --git a/CHANGELOG.md b/CHANGELOG.md index 2684fa49..3a4e07e6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,27 +2,43 @@ All notable changes to this project will be documented in this file. +## [9.4.0] - 2023-10-03 + +### Features + +- KMS support: + - Grant/deny access rights + - Add bulk encryption/decryption for Covercrypt + - Import key specifying the decryption key + +### Ci + +- Rebuild GitHub cache on changes ([#137]) + ## [9.3.0] - 2023-09-22 ### Features -- Support import of Certificate and PrivateKey (with tags) -- Export object wrapped -- Import of a wrapped key -- Use 4.6.0 KMS version +- KMS support: + - Support import of Certificate and PrivateKey (with tags) + - Export object wrapped + - Import of a wrapped key + - Use 4.6.0 KMS version ## [9.2.0] - 2023-08-22 ### Features -- Kms JWE encryption (#124) +- KMS support: + - Kms JWE encryption (#124) + - Add KMS tags support (for creation and searching objects) - Upgrade to cloudproof 2.2.1 -- Add KMS tags support (for creation and searching objects) ### Testing -- KMS Locate several keys with same tag -- Locate by tags and object type +- KMS support: + - Locate several keys with same tag + - Locate by tags and object type ## [9.1.1] - 2023-08-09 diff --git a/README.md b/README.md index 6ffd1221..012c41c9 100644 --- a/README.md +++ b/README.md @@ -84,3 +84,4 @@ From the version 8.0.0, `cloudproof_js` depends on [cloudproof_rust](https://git | 9.1.0,9.1.1 | 2.1.0 | 4.3.0 | | 9.2.0 | 2.2.1 | 4.5.0 | | 9.3.0 | 2.2.3 | 4.6.0 | +| 9.4.0 | 2.2.4 | 4.7.0 | diff --git a/docker-compose.yml b/docker-compose.yml index 4c51166d..e8a3469c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,20 +1,23 @@ --- # docker-compose.yml -version: '3' +version: "3" services: kms: container_name: kms - image: ghcr.io/cosmian/kms:4.6.0 + image: ghcr.io/cosmian/kms:4.7.0 ports: - 9998:9998 environment: - JWK_PRIVATE_KEY: '{"kty": "OKP","d": "MPEVJwdRqGM_qhJOUb5hR0Xr9EvwMLZGnkf-eDj5fU8","use": "enc","crv": "X25519","kid": "DX3GC+Fx3etxfRJValQNbqaB0gs=","x": + JWK_PRIVATE_KEY: + '{"kty": "OKP","d": "MPEVJwdRqGM_qhJOUb5hR0Xr9EvwMLZGnkf-eDj5fU8","use": "enc","crv": "X25519","kid": "DX3GC+Fx3etxfRJValQNbqaB0gs=","x": "gdF-1TtAjsFqNWr9nwhGUlFG38qrDUqYgcILgtYrpTY","alg": "ECDH-ES"}' + AUTH0_TOKEN_1: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6ImNsaWVudDEiLCJuYW1lIjoiY2xpZW50MUBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci84Njg5YWE1ODlmZmRkMGYzMDYzNTg3NGUzMjhhYjg3OT9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRmNsLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTEwLTAyVDEyOjA4OjA1LjQwMFoiLCJlbWFpbCI6ImNsaWVudDFAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjk2MjQ4NDkwLCJleHAiOjE2OTY2MDg0OTAsInN1YiI6ImF1dGgwfDY1MTU2OGY0ZjZjYzc4NzhmNzY0ZWQ0NyIsInNpZCI6Ik1zZlZMNHZqOVNBYnU4a3Q4ZEtJcFU3bjlxamlqeWM3Iiwibm9uY2UiOiJZakkzU2pWVllWVkpkRVp5U0c5cE0wRnhRVWgxVHpSVGRscHlSRkJLUlhweVJXMDVTM1JDT1cxMmJRPT0ifQ.rlX67uWmZwwI8-p2eCpvws3mvmQACEQlwM74LcuvbfT8tL7HjAd5YBWaI40Ty_RwpzAbJ5xS6oEO414xZid7TfEJcSRc_faHAuNzBtd050Alf4wf2kDqqjOJMvsRLS98Jtosiq9yE020BANoa_11HO5ji_ucsDaZmJLHoEPW1rNdrj6L-2eYS2vW49QFspg3-edSHeCZtTpCZ1gp_xd95afuKt_npQDLvyPr399FEv0gnZ0qLh9NGs77ltdBsEC4NrlBT8TLnYUHpQgaBserzI2AMhf2ui9DxaXULxuARB-mo4OR2MRrtcFKTYuNETv6DEPOyBLu40RLVVZEO_nc0g" + AUTH0_TOKEN_2: "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IjVVU1FrSVlULW9QMWZrcjQtNnRrciJ9.eyJuaWNrbmFtZSI6ImNsaWVudDIiLCJuYW1lIjoiY2xpZW50MkBjb3NtaWFuLmNvbSIsInBpY3R1cmUiOiJodHRwczovL3MuZ3JhdmF0YXIuY29tL2F2YXRhci83MjZhNDZkMDdkZjY0Y2VjMDc4ZTQzNzUyYjg5MjFmMz9zPTQ4MCZyPXBnJmQ9aHR0cHMlM0ElMkYlMkZjZG4uYXV0aDAuY29tJTJGYXZhdGFycyUyRmNsLnBuZyIsInVwZGF0ZWRfYXQiOiIyMDIzLTEwLTAyVDEyOjA4OjQ5LjY1M1oiLCJlbWFpbCI6ImNsaWVudDJAY29zbWlhbi5jb20iLCJlbWFpbF92ZXJpZmllZCI6ZmFsc2UsImlzcyI6Imh0dHBzOi8va21zLWNvc21pYW4uZXUuYXV0aDAuY29tLyIsImF1ZCI6IkszaXhldXhuVDVrM0Roa0tocWhiMXpYbjlFNjJGRXdJIiwiaWF0IjoxNjk2MjQ4NTM1LCJleHAiOjE2OTY2MDg1MzUsInN1YiI6ImF1dGgwfDY1MDQ2YzdlNWEwYWZmYzg3MjAxZWE1ZiIsInNpZCI6IlVCS0EyS3VlUFB4TGJnQUJ6WkItQVo4dnByeXlqSlhMIiwibm9uY2UiOiJRalV4ZEg0d2VXUmZTV05JTkVkaWVHOVVhRzFJZVZaaVZFZHRkM0pMVnk1dFZuVTFPR0ZCWDBaYU9BPT0ifQ.MGgJNRvIkJ7C11KvoIKMomkfI4nXz_-5svsSmKSk7uxv1KV0shPvn7iZXOM1EZ9K0OJHlqZLMtDyCy0l5SLDN_FKTXZGnvUbPg2wSQAD6BjocrqbZvuzqvRkcielMy9pCG8O1v4wmHBP0gKHKwIWrYD0rKqI8_vg639CksKBjefJtyvr4FvMMcuB9SmmC-nLNEFJcv9bF2OsKLB-JGbbtylbDyf2leMGORPjtxaVXQE3nuCjbaP8W8EF6TPn7GlmcRMS5UUeD4vqpIz5JniOUd_keZZkKqAZUMUG6I8Po75xQpXrcxn0AWwCgsH2tO9TObAzhPGZH_8mnADF517_IQ" findex_cloud: - image: ghcr.io/cosmian/findex_cloud:0.1.0 + image: ghcr.io/cosmian/findex_cloud:0.3.1 ports: - - 8080:8080 + - 9090:9090 redis: image: redis:latest ports: diff --git a/examples/nodejs_search_imdb/index_with_stats.mjs b/examples/nodejs_search_imdb/index_with_stats.mjs index 027325e9..72a2e648 100755 --- a/examples/nodejs_search_imdb/index_with_stats.mjs +++ b/examples/nodejs_search_imdb/index_with_stats.mjs @@ -1,21 +1,21 @@ -import fs from "fs" -import readline from "readline" +import Database from "better-sqlite3" import { - IndexedValue, - Location, - Keyword, Findex, FindexCloud, FindexKey, + IndexedValue, + Keyword, Label, - generateAliases, + Location, callbacksExamplesBetterSqlite3, + generateAliases, } from "cloudproof_js" -import path from "path" -import { fileURLToPath } from "url" import { randomBytes } from "crypto" -import Database from "better-sqlite3" +import fs from "fs" import fetch, { Headers, Request, Response } from "node-fetch" +import path from "path" +import readline from "readline" +import { fileURLToPath } from "url" globalThis.fetch = fetch globalThis.Headers = Headers @@ -50,7 +50,7 @@ const { upsert: upsertCloud, search: searchCloud } = await FindexCloud() const masterKey = new FindexKey(randomBytes(16)) const label = new Label(randomBytes(10)) const findexCloudToken = process.env.FINDEX_CLOUD_TOKEN -const baseUrl = "http://127.0.0.1:8080" +const baseUrl = "http://127.0.0.1:9090" // Init databases const dbClear = new Database(":memory:") diff --git a/examples/test/chrome.mjs b/examples/test/chrome.mjs index a3d17ead..17cf2f1e 100644 --- a/examples/test/chrome.mjs +++ b/examples/test/chrome.mjs @@ -5,7 +5,7 @@ const host = process.argv[2] || undefined const kmsHost = process.argv[3] || undefined if (!host) { - console.error("Please provide host: chome.mjs http://localhost:8080") + console.error("Please provide host: chrome.mjs http://localhost:9090") exit(1) } diff --git a/package.json b/package.json index 7015168a..8f6ad6d4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloudproof_js", - "version": "9.3.0", + "version": "9.4.0", "license": "MIT", "description": "Cosmian Cloudproof javascript client library", "author": "Bruno Grieder, Pauline Hochard, Emmanuel Coste, Thibaud Dauce", @@ -61,7 +61,7 @@ "eslint-plugin-n": "^15.2.5", "eslint-plugin-promise": "^6.0.1", "prettier": "2.7.1", - "puppeteer": "^19.2.0", + "puppeteer": "^19.11.1", "redis": "^4.3.1", "rollup": "^3.3.0", "typescript": "^4.9.0", diff --git a/scripts/download_wasm.py b/scripts/download_wasm.py index 030f8bcb..aea51e6a 100644 --- a/scripts/download_wasm.py +++ b/scripts/download_wasm.py @@ -73,6 +73,6 @@ def download_wasm(version: str) -> bool: if __name__ == '__main__': - RET = download_wasm('v2.2.3') + RET = download_wasm('v2.2.4') if RET is False and getenv('GITHUB_ACTIONS'): download_wasm('last_build/feature/findex_5_0_0') diff --git a/src/kms/kms.ts b/src/kms/kms.ts index f694f5f3..8bdb7663 100644 --- a/src/kms/kms.ts +++ b/src/kms/kms.ts @@ -15,6 +15,7 @@ import { ReKeyKeyPair } from "./requests/ReKeyKeyPair" import { Revoke } from "./requests/Revoke" import { Attributes, + CryptographicParameters, Link, LinkType, VendorAttributes, @@ -43,6 +44,7 @@ import { KeyWrapType, RevocationReasonEnumeration, } from "./structs/types" +import { webassembly_split_encrypted_header } from "../pkg/cover_crypt/cloudproof_cover_crypt" // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars export interface KmsRequest { @@ -623,6 +625,7 @@ export class KmsClient { * Mark a CoverCrypt Secret Master Key as Revoked * @param {string} uniqueIdentifier the unique identifier of the key * @param {string} reason the explanation of the revocation + * @returns nothing */ public async revokeCoverCryptSecretMasterKey( uniqueIdentifier: string, @@ -635,6 +638,7 @@ export class KmsClient { * Mark a CoverCrypt Public Master Key as Revoked * @param {string} uniqueIdentifier the unique identifier of the key * @param {string} reason the explanation of the revocation + * @returns nothing */ public async revokeCoverCryptPublicMasterKey( uniqueIdentifier: string, @@ -784,13 +788,91 @@ export class KmsClient { return (await this.post(encrypt)).data } + /** + * Encrypt multiple data at once + * @param uniqueIdentifier the unique identifier of the public key + * @param accessPolicy the access policy to use for encryption + * @param data multiple data to encrypt + * @param {object} options Additional optional options to the encryption + * @param {Uint8Array} options.headerMetadata Data encrypted in the header + * @param {Uint8Array} options.authenticationData Data use to authenticate the encrypted value when decrypting (if use, should be use during decryption) + * @returns an array containing multiple ciphertexts + */ + public async coverCryptBulkEncrypt( + uniqueIdentifier: string, + accessPolicy: string, + data: Uint8Array[], + options: { + headerMetadata?: Uint8Array + authenticationData?: Uint8Array + } = {}, + ): Promise { + const accessPolicyBytes = new TextEncoder().encode(accessPolicy) + const accessPolicySize = encode(accessPolicyBytes.length) + + let headerMetadataSize = encode(0) + let headerMetadata = Uint8Array.from([]) + if (typeof options.headerMetadata !== "undefined") { + headerMetadataSize = encode(options.headerMetadata.length) + headerMetadata = options.headerMetadata + } + + const cryptographicParameters = new CryptographicParameters() + cryptographicParameters.cryptographicAlgorithm = + CryptographicAlgorithm.CoverCryptBulk + + let plaintext = encode(data.length) + + for (const chunk of data) { + plaintext = Uint8Array.from([ + ...plaintext, + ...encode(chunk.length), + ...chunk, + ]) + } + + const dataToEncrypt = Uint8Array.from([ + ...accessPolicySize, + ...accessPolicyBytes, + ...headerMetadataSize, + ...headerMetadata, + ...plaintext, + ]) + + const encrypt = new Encrypt( + uniqueIdentifier, + dataToEncrypt, + cryptographicParameters, + ) + if (typeof options.authenticationData !== "undefined") { + encrypt.authenticatedEncryptionAdditionalData = options.authenticationData + } + + const encryptedData = (await this.post(encrypt)).data + const { encryptedHeader, ciphertext } = + webassembly_split_encrypted_header(encryptedData) + + let { result: nbChunks, tail: tailCiphertext } = decode(ciphertext) + + const encryptedChunks = [] + for (let i = 0; i < nbChunks; i++) { + const { result: chunkSize, tail } = decode(tailCiphertext) + const chunk = tail.slice(0, chunkSize) + tailCiphertext = tail.slice(chunkSize) + + encryptedChunks.push(new Uint8Array([...encryptedHeader, ...chunk])) + } + + return encryptedChunks + } + /** * Decrypt some data * @param uniqueIdentifier the unique identifier of the private key * @param data to decrypt * @param {object} options Additional optional options to the encryption * @param {Uint8Array} options.authenticationData Data use to authenticate the encrypted value when decrypting (if use, should have been use during encryption) - * @returns the plaintext + * @returns the header metadata and the plaintext */ public async coverCryptDecrypt( uniqueIdentifier: string, @@ -813,6 +895,66 @@ export class KmsClient { return { headerMetadata, plaintext } } + /** + * Decrypt multiple data at once + * @param uniqueIdentifier the unique identifier of the private key + * @param data multiple data to decrypt + * @param {object} options Additional optional options to the encryption + * @param {Uint8Array} options.authenticationData Data use to authenticate the encrypted value when decrypting (if use, should have been use during encryption) + * @returns header metadata and an array containing multiple plaintexts + */ + public async coverCryptBulkDecrypt( + uniqueIdentifier: string, + data: Uint8Array[], + options: { + authenticationData?: Uint8Array + } = {}, + ): Promise<{ + headerMetadata: Uint8Array + plaintext: Uint8Array[] + }> { + const cryptographicParameters = new CryptographicParameters() + cryptographicParameters.cryptographicAlgorithm = + CryptographicAlgorithm.CoverCryptBulk + + let ciphertext = encode(data.length) + + for (const chunk of data) { + ciphertext = Uint8Array.from([ + ...ciphertext, + ...encode(chunk.length), + ...chunk, + ]) + } + + const decrypt = new Decrypt( + uniqueIdentifier, + ciphertext, + cryptographicParameters, + ) + if (typeof options.authenticationData !== "undefined") { + decrypt.authenticatedEncryptionAdditionalData = options.authenticationData + } + + const response = await this.post(decrypt) + + const { result: headerMetadataLength, tail } = decode(response.data) + const headerMetadata = tail.slice(0, headerMetadataLength) + const plaintext = tail.slice(headerMetadataLength) + + let { result: nbChunks, tail: tailPlaintext } = decode(plaintext) + const decryptedChunks = [] + for (let i = 0; i < nbChunks; i++) { + const { result: chunkSize, tail } = decode(tailPlaintext) + const chunk = tail.slice(0, chunkSize) + tailPlaintext = tail.slice(chunkSize) + + decryptedChunks.push(chunk) + } + + return { headerMetadata, plaintext: decryptedChunks } + } + /** * Rotate the given policy attributes. This will rekey in the KMS: * - the Master Keys @@ -884,6 +1026,7 @@ export class KmsClient { * @param uniqueIdentifier the unique identifier of the object to import * @param wrappedObject wrapped objectto import * @param unwrap boolean true if object must be unwrapped before importing + * @param encryptionKeyUniqueIdentifier if unwrap is true, uniqueIdentifier used to unwrap key can be overwritten with a specific one * @param replaceExisting boolean replacing if existing object * @returns imported object identifier */ @@ -891,6 +1034,7 @@ export class KmsClient { uniqueIdentifier: string, wrappedObject: KmsObject, unwrap: boolean, + encryptionKeyUniqueIdentifier: string | null = null, replaceExisting: boolean = false, ): Promise { if ( @@ -910,18 +1054,150 @@ export class KmsClient { const keyWrapType = unwrap ? KeyWrapType.NotWrapped : KeyWrapType.AsRegistered + const overWrittenWrappedObject = { ...wrappedObject } + if ( + unwrap && + encryptionKeyUniqueIdentifier != null && + overWrittenWrappedObject.value.keyBlock.keyWrappingData != null && + overWrittenWrappedObject.value.keyBlock.keyWrappingData + .encryptionKeyInformation != null + ) { + overWrittenWrappedObject.value.keyBlock.keyWrappingData.encryptionKeyInformation.uniqueIdentifier = + encryptionKeyUniqueIdentifier + } return await this.importObject( uniqueIdentifier, attributes, wrappedObject.type, - wrappedObject, + overWrittenWrappedObject, replaceExisting, keyWrapType, ) } + + private async manageAccess( + uniqueIdentifier: string, + userIdentifier: string, + operationType: KMIPOperations, + urlPath: string, + ): Promise { + const url = new URL(urlPath, this.url) + const body = { + unique_identifier: uniqueIdentifier, + user_id: userIdentifier, + operation_type: operationType, + } + const response = await fetch(url, { + method: "POST", + headers: this.headers, + body: JSON.stringify(body), + }) + if (!response.ok || response.status >= 400) { + throw new Error(`${urlPath} request failed (${response.status})`) + } + + return response + } + + /** + * Grant access to a KmsObject for a specific user + * @param uniqueIdentifier the unique identifier of the object to import + * @param userIdentifier the unique identifier of the user to grant access to + * @param operationType KMIP operation type to grant access for + * @returns response from KMS server + */ + public async grantAccess( + uniqueIdentifier: string, + userIdentifier: string, + operationType: KMIPOperations, + ): Promise { + return await this.manageAccess( + uniqueIdentifier, + userIdentifier, + operationType, + "/access/grant", + ) + } + + /** + * Revoke access to a KmsObject for a specific user + * @param uniqueIdentifier the unique identifier of the object to import + * @param userIdentifier the unique identifier of the user to revoke access to + * @param operationType KMIP operation type to revoke access for + * @returns response from KMS server + */ + public async revokeAccess( + uniqueIdentifier: string, + userIdentifier: string, + operationType: KMIPOperations, + ): Promise { + return await this.manageAccess( + uniqueIdentifier, + userIdentifier, + operationType, + "/access/revoke", + ) + } + + /** + * List access to a KmsObject + * @param uniqueIdentifier the unique identifier of the object to list access for + * @returns response from KMS server + */ + public async listAccess(uniqueIdentifier: string): Promise { + const listAccessUrl = new URL(`/access/list/${uniqueIdentifier}`, this.url) + const response = await fetch(listAccessUrl, { + method: "GET", + headers: this.headers, + }) + if (!response.ok || response.status >= 400) { + throw new Error(`list access request failed (${response.status})`) + } + + return response + } + + private async listObjects(urlPath: string): Promise { + const url = new URL(urlPath, this.url) + const response = await fetch(url, { + method: "GET", + headers: this.headers, + }) + if (!response.ok || response.status >= 400) { + throw new Error(`${urlPath} request failed (${response.status})`) + } + + return response + } + + /** + * List owned objects for a user + * @returns response from KMS server + */ + public async listOwnedObjects(): Promise { + return await this.listObjects("/access/owned") + } + + /** + * List objects a user has obtained access for + * @returns response from KMS server + */ + public async listObtainedObjects(): Promise { + return await this.listObjects("/access/obtained") + } } export enum SymmetricKeyAlgorithm { AES, ChaCha20, } + +export enum KMIPOperations { + get = "get", + export = "export", + encrypt = "encrypt", + decrypt = "decrypt", + import = "import", + revoke = "revoke", + destroy = "destroy", +} diff --git a/src/kms/structs/object_data_structures.ts b/src/kms/structs/object_data_structures.ts index ae5bf19f..0e945417 100644 --- a/src/kms/structs/object_data_structures.ts +++ b/src/kms/structs/object_data_structures.ts @@ -103,7 +103,7 @@ export enum CryptographicAlgorithm { TFHE = 0x8880_0002, ABE = 0x8880_0003, CoverCrypt = 0x8880_0004, - FPEFF1 = 0x8880_0005, + CoverCryptBulk = 0x8880_0005, } export class KeyBlock { diff --git a/tests/KMS.test.ts b/tests/KMS.test.ts index fba44a63..9eabce4e 100644 --- a/tests/KMS.test.ts +++ b/tests/KMS.test.ts @@ -5,6 +5,7 @@ import { Create, CryptographicAlgorithm, CryptographicUsageMask, + KMIPOperations, KeyFormatType, KeyValue, KmsClient, @@ -24,12 +25,26 @@ import { toTTLV, } from ".." -import { expect, test } from "vitest" +import { beforeAll, expect, test } from "vitest" import { NIST_P256_CERTIFICATE, NIST_P256_PRIVATE_KEY, } from "./data/certificates" +const kmsToken = process.env.AUTH0_TOKEN_1 +let client: KmsClient | undefined + +beforeAll(async () => { + client = new KmsClient( + `http://${process.env.KMS_HOST || "localhost"}:9998`, + kmsToken, + ) + if (!(await client.up())) { + console.error("No KMIP server. Skipping test") + client = undefined + } +}) + test("serialize/deserialize Create", async () => { await CoverCrypt() @@ -120,55 +135,49 @@ const CREATE_SYMMETRIC_KEY = `{ test( "KMS Import Master Keys", async () => { - const { Policy, PolicyAxis } = await CoverCrypt() - - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } - - const policy = new Policy([ - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], - false, - ), - ]) - - const [privateKeyUniqueIdentifier, publicKeyUniqueIdentifier] = - await client.createCoverCryptMasterKeyPair(policy) - - const publicKey = await client.retrieveCoverCryptPublicMasterKey( - publicKeyUniqueIdentifier, - ) - const privateKey = await client.retrieveCoverCryptSecretMasterKey( - privateKeyUniqueIdentifier, - ) - - const importedPublicKeyUniqueIdentifier = - await client.importCoverCryptPublicMasterKey( - `${publicKeyUniqueIdentifier}-imported`, - publicKey, + if (client !== undefined) { + const { Policy, PolicyAxis } = await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + const [privateKeyUniqueIdentifier, publicKeyUniqueIdentifier] = + await client.createCoverCryptMasterKeyPair(policy) + + const publicKey = await client.retrieveCoverCryptPublicMasterKey( + publicKeyUniqueIdentifier, ) - const importedPrivateKeyUniqueIdentifier = - await client.importCoverCryptSecretMasterKey( - `${privateKeyUniqueIdentifier}-imported`, - privateKey, + const privateKey = await client.retrieveCoverCryptSecretMasterKey( + privateKeyUniqueIdentifier, ) - await client.retrieveCoverCryptPublicMasterKey( - importedPublicKeyUniqueIdentifier, - ) - await client.retrieveCoverCryptSecretMasterKey( - importedPrivateKeyUniqueIdentifier, - ) + const importedPublicKeyUniqueIdentifier = + await client.importCoverCryptPublicMasterKey( + `${publicKeyUniqueIdentifier}-imported`, + publicKey, + ) + const importedPrivateKeyUniqueIdentifier = + await client.importCoverCryptSecretMasterKey( + `${privateKeyUniqueIdentifier}-imported`, + privateKey, + ) + + await client.retrieveCoverCryptPublicMasterKey( + importedPublicKeyUniqueIdentifier, + ) + await client.retrieveCoverCryptSecretMasterKey( + importedPrivateKeyUniqueIdentifier, + ) + } }, { timeout: 30 * 1000, @@ -178,38 +187,32 @@ test( test( "KMS With JWE encryption", async () => { - const { Policy, PolicyAxis } = await CoverCrypt() + if (client !== undefined) { + const { Policy, PolicyAxis } = await CoverCrypt() + + client.setEncryption({ + kty: "OKP", + use: "enc", + crv: "X25519", + kid: "DX3GC+Fx3etxfRJValQNbqaB0gs=", + x: "gdF-1TtAjsFqNWr9nwhGUlFG38qrDUqYgcILgtYrpTY", + alg: "ECDH-ES", + }) + + const policy = new Policy([ + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return + await client.createCoverCryptMasterKeyPair(policy) } - - client.setEncryption({ - kty: "OKP", - use: "enc", - crv: "X25519", - kid: "DX3GC+Fx3etxfRJValQNbqaB0gs=", - x: "gdF-1TtAjsFqNWr9nwhGUlFG38qrDUqYgcILgtYrpTY", - alg: "ECDH-ES", - }) - - const policy = new Policy([ - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], - false, - ), - ]) - - await client.createCoverCryptMasterKeyPair(policy) }, { timeout: 30 * 1000, @@ -219,34 +222,31 @@ test( test( "KMS Locate", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } - const TAG = (Math.random() * 100000000).toString() - const uniqueIdentifier = await client.createSymmetricKey( - SymmetricKeyAlgorithm.AES, - 256, - undefined, - [TAG], - ) - const uniqueIdentifier2 = await client.createSymmetricKey( - SymmetricKeyAlgorithm.AES, - 256, - undefined, - [TAG], - ) + if (client !== undefined) { + const TAG = (Math.random() * 100000000).toString() + const uniqueIdentifier = await client.createSymmetricKey( + SymmetricKeyAlgorithm.AES, + 256, + undefined, + [TAG], + ) + const uniqueIdentifier2 = await client.createSymmetricKey( + SymmetricKeyAlgorithm.AES, + 256, + undefined, + [TAG], + ) - const uniqueIdentifiers = await client.getUniqueIdentifiersByTags([TAG]) - expect(uniqueIdentifiers.length).toEqual(2) - expect(uniqueIdentifiers).toContain(uniqueIdentifier) - expect(uniqueIdentifiers).toContain(uniqueIdentifier2) + const uniqueIdentifiers = await client.getUniqueIdentifiersByTags([TAG]) + expect(uniqueIdentifiers.length).toEqual(2) + expect(uniqueIdentifiers).toContain(uniqueIdentifier) + expect(uniqueIdentifiers).toContain(uniqueIdentifier2) - const notExist = await client.getUniqueIdentifiersByTags(["TAG_NOT_EXIST"]) - expect(notExist.length).toEqual(0) + const notExist = await client.getUniqueIdentifiersByTags([ + "TAG_NOT_EXIST", + ]) + expect(notExist.length).toEqual(0) + } }, { timeout: 30 * 1000, @@ -256,49 +256,47 @@ test( test( "KMS Locate CoverCrypt IUD", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } - const TAG = (Math.random() * 100000000).toString() - - const { Policy, PolicyAxis } = await CoverCrypt() - - const policy = new Policy([ - new PolicyAxis( - "Security Level", - [ - { name: "Protected", isHybridized: false }, - { name: "Confidential", isHybridized: false }, - { name: "Top Secret", isHybridized: true }, - ], - true, - ), - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], - false, - ), - ]) - - const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy, [ - TAG, - ]) + if (client !== undefined) { + const TAG = (Math.random() * 100000000).toString() + + const { Policy, PolicyAxis } = await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Security Level", + [ + { name: "Protected", isHybridized: false }, + { name: "Confidential", isHybridized: false }, + { name: "Top Secret", isHybridized: true }, + ], + true, + ), + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair( + policy, + [TAG], + ) - const uniqueIdentifiers = await client.getUniqueIdentifiersByTags([TAG]) - expect(uniqueIdentifiers.length).toEqual(2) - expect(uniqueIdentifiers).toContain(mskID) - expect(uniqueIdentifiers).toContain(mpkID) + const uniqueIdentifiers = await client.getUniqueIdentifiersByTags([TAG]) + expect(uniqueIdentifiers.length).toEqual(2) + expect(uniqueIdentifiers).toContain(mskID) + expect(uniqueIdentifiers).toContain(mpkID) - const notExist = await client.getUniqueIdentifiersByTags(["TAG_NOT_EXIST"]) - expect(notExist.length).toEqual(0) + const notExist = await client.getUniqueIdentifiersByTags([ + "TAG_NOT_EXIST", + ]) + expect(notExist.length).toEqual(0) + } }, { timeout: 30 * 1000, @@ -308,52 +306,47 @@ test( test( "KMS Locate Covercrypt user decryption key", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } - const TAG = (Math.random() * 100000000).toString() - - const { Policy, PolicyAxis } = await CoverCrypt() - - const policy = new Policy([ - new PolicyAxis( - "Security Level", - [ - { name: "Protected", isHybridized: false }, - { name: "Confidential", isHybridized: false }, - { name: "Top Secret", isHybridized: true }, - ], - true, - ), - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], - false, - ), - ]) - - // create user decryption Key - const [mskID] = await client.createCoverCryptMasterKeyPair(policy) - const uniqueIdentifier = await client.createCoverCryptUserDecryptionKey( - "(Department::MKG || Department::FIN) && Security Level::Confidential", - mskID, - [TAG], - ) + if (client !== undefined) { + const TAG = (Math.random() * 100000000).toString() + + const { Policy, PolicyAxis } = await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Security Level", + [ + { name: "Protected", isHybridized: false }, + { name: "Confidential", isHybridized: false }, + { name: "Top Secret", isHybridized: true }, + ], + true, + ), + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + // create user decryption Key + const [mskID] = await client.createCoverCryptMasterKeyPair(policy) + const uniqueIdentifier = await client.createCoverCryptUserDecryptionKey( + "(Department::MKG || Department::FIN) && Security Level::Confidential", + mskID, + [TAG], + ) - // Locate by tags - const uniqueIdentifiersByTag = await client.getUniqueIdentifiersByTags([ - TAG, - ]) + // Locate by tags + const uniqueIdentifiersByTag = await client.getUniqueIdentifiersByTags([ + TAG, + ]) - expect(uniqueIdentifiersByTag).toContain(uniqueIdentifier) + expect(uniqueIdentifiersByTag).toContain(uniqueIdentifier) + } }, { timeout: 30 * 1000, @@ -363,64 +356,60 @@ test( test( "KMS Locate several keys with same tag", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } - const TAG = (Math.random() * 100000000).toString() - - const { Policy, PolicyAxis } = await CoverCrypt() - - const policy = new Policy([ - new PolicyAxis( - "Security Level", - [ - { name: "Protected", isHybridized: false }, - { name: "Confidential", isHybridized: false }, - { name: "Top Secret", isHybridized: true }, - ], - true, - ), - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], - false, - ), - ]) - - // create Covercrypt master key pair (1 & 2) - const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy, [ - TAG, - ]) - // create Covercrypt user decryption key (3) - const decryptionKeyID = await client.createCoverCryptUserDecryptionKey( - "(Department::MKG || Department::FIN) && Security Level::Confidential", - mskID, - [TAG], - ) - // Create symmetric key (4) - const symmetricKeyID = await client.createSymmetricKey( - SymmetricKeyAlgorithm.AES, - 256, - undefined, - [TAG], - ) + if (client !== undefined) { + const TAG = (Math.random() * 100000000).toString() + + const { Policy, PolicyAxis } = await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Security Level", + [ + { name: "Protected", isHybridized: false }, + { name: "Confidential", isHybridized: false }, + { name: "Top Secret", isHybridized: true }, + ], + true, + ), + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + // create Covercrypt master key pair (1 & 2) + const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair( + policy, + [TAG], + ) + // create Covercrypt user decryption key (3) + const decryptionKeyID = await client.createCoverCryptUserDecryptionKey( + "(Department::MKG || Department::FIN) && Security Level::Confidential", + mskID, + [TAG], + ) + // Create symmetric key (4) + const symmetricKeyID = await client.createSymmetricKey( + SymmetricKeyAlgorithm.AES, + 256, + undefined, + [TAG], + ) - // Locate by tags - const idByTag = await client.getUniqueIdentifiersByTags([TAG]) + // Locate by tags + const idByTag = await client.getUniqueIdentifiersByTags([TAG]) - expect(idByTag.length).toEqual(4) - expect(idByTag).toContain(mskID) - expect(idByTag).toContain(mpkID) - expect(idByTag).toContain(decryptionKeyID) - expect(idByTag).toContain(symmetricKeyID) + expect(idByTag.length).toEqual(4) + expect(idByTag).toContain(mskID) + expect(idByTag).toContain(mpkID) + expect(idByTag).toContain(decryptionKeyID) + expect(idByTag).toContain(symmetricKeyID) + } }, { timeout: 30 * 1000, @@ -430,63 +419,56 @@ test( test( "KMS Symmetric Key", async () => { - await CoverCrypt() - - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } + if (client !== undefined) { + await CoverCrypt() - // create - const uniqueIdentifier = await client.createSymmetricKey( - SymmetricKeyAlgorithm.AES, - 256, - ) - expect(uniqueIdentifier).toBeTypeOf("string") + // create + const uniqueIdentifier = await client.createSymmetricKey( + SymmetricKeyAlgorithm.AES, + 256, + ) + expect(uniqueIdentifier).toBeTypeOf("string") - // recover - const key: SymmetricKey = await client.retrieveSymmetricKey( - uniqueIdentifier, - ) - expect(key.keyBlock.cryptographicAlgorithm).toEqual( - CryptographicAlgorithm.AES, - ) - expect(key.keyBlock.cryptographicLength).toEqual(256) - expect(key.keyBlock.keyFormatType).toEqual( - KeyFormatType.TransparentSymmetricKey, - ) - expect(key.keyBlock.keyValue).not.toBeNull() - expect(key.keyBlock.keyValue).toBeInstanceOf(KeyValue) + // recover + const key: SymmetricKey = await client.retrieveSymmetricKey( + uniqueIdentifier, + ) + expect(key.keyBlock.cryptographicAlgorithm).toEqual( + CryptographicAlgorithm.AES, + ) + expect(key.keyBlock.cryptographicLength).toEqual(256) + expect(key.keyBlock.keyFormatType).toEqual( + KeyFormatType.TransparentSymmetricKey, + ) + expect(key.keyBlock.keyValue).not.toBeNull() + expect(key.keyBlock.keyValue).toBeInstanceOf(KeyValue) - const keyValue = key?.keyBlock?.keyValue as KeyValue - expect(keyValue.keyMaterial).toBeInstanceOf(TransparentSymmetricKey) + const keyValue = key?.keyBlock?.keyValue as KeyValue + expect(keyValue.keyMaterial).toBeInstanceOf(TransparentSymmetricKey) - const sk = keyValue.keyMaterial as TransparentSymmetricKey - expect(sk.key.length).toEqual(32) + const sk = keyValue.keyMaterial as TransparentSymmetricKey + expect(sk.key.length).toEqual(32) - // import - const uid = await client.importSymmetricKey( - uniqueIdentifier + "-1", - key.bytes(), - false, - ) - expect(uid).toEqual(uniqueIdentifier + "-1") + // import + const uid = await client.importSymmetricKey( + uniqueIdentifier + "-1", + key.bytes(), + false, + ) + expect(uid).toEqual(uniqueIdentifier + "-1") - // get - const key_ = await client.retrieveSymmetricKey(uid) - expect(key_.bytes()).toEqual(key.bytes()) + // get + const key_ = await client.retrieveSymmetricKey(uid) + expect(key_.bytes()).toEqual(key.bytes()) - // revoke - await client.revokeSymmetricKey(uniqueIdentifier, "revoked") - await client.revokeSymmetricKey(uid, "revoked") + // revoke + await client.revokeSymmetricKey(uniqueIdentifier, "revoked") + await client.revokeSymmetricKey(uid, "revoked") - // destroy - await client.destroySymmetricKey(uid) - await client.destroySymmetricKey(uniqueIdentifier) + // destroy + await client.destroySymmetricKey(uid) + await client.destroySymmetricKey(uniqueIdentifier) + } }, { timeout: 30 * 1000, @@ -580,6 +562,127 @@ test("KMS CoverCrypt Access Policy", async () => { test( "KMS CoverCrypt keys", + async () => { + if (client !== undefined) { + const { CoverCryptHybridDecryption, Policy, PolicyAxis } = + await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Security Level", + [ + { name: "Protected", isHybridized: false }, + { name: "Confidential", isHybridized: false }, + { name: "Top Secret", isHybridized: true }, + ], + true, + ), + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + // create master keys + const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy) + + // recover keys and policies + const msk = await client.retrieveCoverCryptSecretMasterKey(mskID) + const policyMsk = Policy.fromKey(msk) + + // Policies are compared under JSON format but comparison will become a raw bytes comparison since JSON format will be removed + const policyJson = JSON.parse(new TextDecoder().decode(policy.toBytes())) + const policyMskJson = JSON.parse( + new TextDecoder().decode(policyMsk.toBytes()), + ) + expect(policyJson).toEqual(policyMskJson) + const mpk = await client.retrieveCoverCryptPublicMasterKey(mpkID) + const policyMpk = Policy.fromKey(mpk) + const policyMpkJson = JSON.parse( + new TextDecoder().decode(policyMpk.toBytes()), + ) + expect(policyJson).toEqual(policyMpkJson) + + // create user decryption Key + const apb = + "(Department::MKG || Department::FIN) && Security Level::Confidential" + const udkID = await client.createCoverCryptUserDecryptionKey(apb, mskID) + const udk = await client.retrieveCoverCryptUserDecryptionKey(udkID) + expect(AccessPolicy.fromKey(udk).booleanAccessPolicy).toEqual(apb) + + // encryption + const plaintext = new TextEncoder().encode("abcdefgh") + const ciphertext = await client.coverCryptEncrypt( + mpkID, + "Department::FIN && Security Level::Confidential", + plaintext, + ) + + { + const { plaintext: cleartext } = await client.coverCryptDecrypt( + udkID, + ciphertext, + ) + expect(cleartext).toEqual(plaintext) + } + + // rotate + const rotatedPolicy = await client.rotateCoverCryptAttributes(mskID, [ + "Department::FIN", + "Department::MKG", + ]) + + const rotatedMsk = await client.retrieveCoverCryptSecretMasterKey(mskID) + expect(rotatedMsk.bytes()).not.toEqual(msk.bytes()) + const rotatedMpk = await client.retrieveCoverCryptPublicMasterKey(mpkID) + expect(rotatedMpk.bytes()).not.toEqual(mpk.bytes()) + expect(policy.toBytes()).not.toEqual(rotatedPolicy.toBytes()) + + // encryption + const plaintext2 = new TextEncoder().encode("abcdefgh") + const ciphertext2 = await client.coverCryptEncrypt( + mpkID, + "Department::FIN && Security Level::Confidential", + plaintext2, + ) + + // decryption + { + // Previous local key should not work anymore. + const decrypter2 = new CoverCryptHybridDecryption(udk) + expect(() => decrypter2.decrypt(ciphertext2)).toThrow() + } + + // decrypt with new fetches KMS key + { + const udk2 = await client.retrieveCoverCryptUserDecryptionKey(udkID) + const decrypter2 = new CoverCryptHybridDecryption(udk2) + const { plaintext: cleartext } = decrypter2.decrypt(ciphertext2) + expect(cleartext).toEqual(plaintext) + } + + // decrypt in KMS should still work + { + const { plaintext: cleartext } = await client.coverCryptDecrypt( + udkID, + ciphertext2, + ) + expect(cleartext).toEqual(plaintext) + } + } + }, + { + timeout: 30 * 1000, + }, +) + +test( + "KMS CoverCrypt keys with bulk encryption/decryption", async () => { const client = new KmsClient( `http://${process.env.KMS_HOST || "localhost"}:9998`, @@ -641,57 +744,23 @@ test( expect(AccessPolicy.fromKey(udk).booleanAccessPolicy).toEqual(apb) // encryption - const plaintext = new TextEncoder().encode("abcdefgh") - const ciphertext = await client.coverCryptEncrypt( - mpkID, - "Department::FIN && Security Level::Confidential", - plaintext, - ) - - { - const { plaintext } = await client.coverCryptDecrypt(udkID, ciphertext) - expect(plaintext).toEqual(plaintext) - } - - // rotate - const rotatedPolicy = await client.rotateCoverCryptAttributes(mskID, [ - "Department::FIN", - "Department::MKG", - ]) + const plaintext = [] + plaintext.push(new TextEncoder().encode("abcdefgh")) + plaintext.push(new TextEncoder().encode("azertyui")) + plaintext.push(new TextEncoder().encode("qsdfghjk")) - const rotatedMsk = await client.retrieveCoverCryptSecretMasterKey(mskID) - expect(rotatedMsk.bytes()).not.toEqual(msk.bytes()) - const rotatedMpk = await client.retrieveCoverCryptPublicMasterKey(mpkID) - expect(rotatedMpk.bytes()).not.toEqual(mpk.bytes()) - expect(policy.toBytes()).not.toEqual(rotatedPolicy.toBytes()) - - // encryption - const plaintext2 = new TextEncoder().encode("abcdefgh") - const ciphertext2 = await client.coverCryptEncrypt( + const ciphertext = await client.coverCryptBulkEncrypt( mpkID, "Department::FIN && Security Level::Confidential", - plaintext2, + plaintext, ) - // decryption - { - // Previous local key should not work anymore. - const decrypter2 = new CoverCryptHybridDecryption(udk) - expect(() => decrypter2.decrypt(ciphertext2)).toThrow() - } - - // decrypt with new fetches KMS key - { - const udk2 = await client.retrieveCoverCryptUserDecryptionKey(udkID) - const decrypter2 = new CoverCryptHybridDecryption(udk2) - const { plaintext } = decrypter2.decrypt(ciphertext2) - expect(plaintext).toEqual(plaintext) - } - - // decrypt in KMS should still work { - const { plaintext } = await client.coverCryptDecrypt(udkID, ciphertext2) - expect(plaintext).toEqual(plaintext) + const { plaintext: cleartext } = await client.coverCryptBulkDecrypt( + udkID, + ciphertext, + ) + expect(cleartext).toEqual(plaintext) } }, { @@ -702,84 +771,78 @@ test( test( "Key rotation security when importing with tempered access policy", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - const ret = await client.up() - - if (!(await client.up())) { - console.log("No KMIP server. Skipping test") - return - } - - const { Policy, PolicyAxis } = await CoverCrypt() - - const policy = new Policy([ - new PolicyAxis( - "Security", - [ - { name: "Simple", isHybridized: false }, - { name: "TopSecret", isHybridized: true }, - ], - true, - ), - ]) - - // create master keys - const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy) - const ciphertext = await client.coverCryptEncrypt( - mpkID, - "Security::TopSecret", - Uint8Array.from([1, 2, 3]), - ) + if (client !== undefined) { + const { Policy, PolicyAxis } = await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Security", + [ + { name: "Simple", isHybridized: false }, + { name: "TopSecret", isHybridized: true }, + ], + true, + ), + ]) + + // create master keys + const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy) + const ciphertext = await client.coverCryptEncrypt( + mpkID, + "Security::TopSecret", + Uint8Array.from([1, 2, 3]), + ) - const userKeyID = await client.createCoverCryptUserDecryptionKey( - "Security::Simple", - mskID, - ) - const userKey = await client.retrieveCoverCryptUserDecryptionKey(userKeyID) + const userKeyID = await client.createCoverCryptUserDecryptionKey( + "Security::Simple", + mskID, + ) + const userKey = await client.retrieveCoverCryptUserDecryptionKey( + userKeyID, + ) - const temperedUserKeyID = await client.importCoverCryptUserDecryptionKey( - `${userKeyID}-HACK`, - { bytes: userKey.bytes(), policy: "Security::TopSecret" }, - { - link: [new Link(LinkType.ParentLink, mskID)], - }, - ) - expect(temperedUserKeyID).toEqual(`${userKeyID}-HACK`) + const temperedUserKeyID = await client.importCoverCryptUserDecryptionKey( + `${userKeyID}-HACK`, + { bytes: userKey.bytes(), policy: "Security::TopSecret" }, + { + link: [new Link(LinkType.ParentLink, mskID)], + }, + ) + expect(temperedUserKeyID).toEqual(`${userKeyID}-HACK`) - await expect(async () => { - return await client.coverCryptDecrypt(userKeyID, ciphertext) - }).rejects.toThrow() + await expect(async () => { + return await client?.coverCryptDecrypt(userKeyID, ciphertext) + }).rejects.toThrow() - await expect(async () => { - return await client.coverCryptDecrypt(temperedUserKeyID, ciphertext) - }).rejects.toThrow() + await expect(async () => { + return await client?.coverCryptDecrypt(temperedUserKeyID, ciphertext) + }).rejects.toThrow() - await client.rotateCoverCryptAttributes(mskID, ["Security::TopSecret"]) + await client.rotateCoverCryptAttributes(mskID, ["Security::TopSecret"]) - await expect(async () => { - return await client.coverCryptDecrypt(userKeyID, ciphertext) - }).rejects.toThrow() + await expect(async () => { + return await client?.coverCryptDecrypt(userKeyID, ciphertext) + }).rejects.toThrow() - await expect(async () => { - return await client.coverCryptDecrypt(temperedUserKeyID, ciphertext) - }).rejects.toThrow() + await expect(async () => { + return await client?.coverCryptDecrypt(temperedUserKeyID, ciphertext) + }).rejects.toThrow() - const newCiphertext = await client.coverCryptEncrypt( - mpkID, - "Security::TopSecret", - Uint8Array.from([4, 5, 6]), - ) + const newCiphertext = await client.coverCryptEncrypt( + mpkID, + "Security::TopSecret", + Uint8Array.from([4, 5, 6]), + ) - await expect(async () => { - return await client.coverCryptDecrypt(userKeyID, newCiphertext) - }).rejects.toThrow() + await expect(async () => { + return await client?.coverCryptDecrypt(userKeyID, newCiphertext) + }).rejects.toThrow() - // TODO fix this bug, this should fail (cannot decrypt with the tempered user key) - // await expect(async () => { - // return await client.coverCryptDecrypt(temperedUserKeyID, newCiphertext); - // }).rejects.toThrow() + // TODO fix this bug, this should fail (cannot decrypt with the tempered user key) + // await expect(async () => { + // return await client.coverCryptDecrypt(temperedUserKeyID, newCiphertext); + // }).rejects.toThrow() + } }, { timeout: 30 * 1000, @@ -789,138 +852,238 @@ test( test( "Decrypt old ciphertext after rotation", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.log("No KMIP server. Skipping test") - return - } + if (client !== undefined) { + const { + CoverCryptHybridEncryption, + CoverCryptHybridDecryption, + Policy, + PolicyAxis, + } = await CoverCrypt() + + const policy = new Policy([ + new PolicyAxis( + "Security", + [ + { name: "Simple", isHybridized: false }, + { name: "TopSecret", isHybridized: true }, + ], + true, + ), + ]) + + // create master keys + const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy) + const oldPublicKey = await client.retrieveCoverCryptPublicMasterKey(mpkID) + const oldLocalEncryption = new CoverCryptHybridEncryption( + policy, + oldPublicKey, + ) - const { - CoverCryptHybridEncryption, - CoverCryptHybridDecryption, - Policy, - PolicyAxis, - } = await CoverCrypt() + const oldPlaintext = Uint8Array.from([1, 2, 3]) + const oldKmsCiphertext = await client.coverCryptEncrypt( + mpkID, + "Security::Simple", + oldPlaintext, + ) + const oldLocalCiphertext = oldLocalEncryption.encrypt( + "Security::Simple", + oldPlaintext, + ) - const policy = new Policy([ - new PolicyAxis( - "Security", - [ - { name: "Simple", isHybridized: false }, - { name: "TopSecret", isHybridized: true }, - ], - true, - ), - ]) + const userKeyID = await client.createCoverCryptUserDecryptionKey( + "Security::Simple", + mskID, + ) - // create master keys - const [mskID, mpkID] = await client.createCoverCryptMasterKeyPair(policy) - const oldPublicKey = await client.retrieveCoverCryptPublicMasterKey(mpkID) - const oldLocalEncryption = new CoverCryptHybridEncryption( - policy, - oldPublicKey, - ) + const oldUserKey = await client.retrieveCoverCryptUserDecryptionKey( + userKeyID, + ) + const oldLocalDecryption = new CoverCryptHybridDecryption(oldUserKey) - const oldPlaintext = Uint8Array.from([1, 2, 3]) - const oldKmsCiphertext = await client.coverCryptEncrypt( - mpkID, - "Security::Simple", - oldPlaintext, - ) - const oldLocalCiphertext = oldLocalEncryption.encrypt( - "Security::Simple", - oldPlaintext, - ) + expect( + (await client.coverCryptDecrypt(userKeyID, oldKmsCiphertext)).plaintext, + ).toEqual(oldPlaintext) + expect(oldLocalDecryption.decrypt(oldKmsCiphertext).plaintext).toEqual( + oldPlaintext, + ) - const userKeyID = await client.createCoverCryptUserDecryptionKey( - "Security::Simple", - mskID, - ) + expect( + (await client.coverCryptDecrypt(userKeyID, oldLocalCiphertext)) + .plaintext, + ).toEqual(oldPlaintext) + expect(oldLocalDecryption.decrypt(oldLocalCiphertext).plaintext).toEqual( + oldPlaintext, + ) - const oldUserKey = await client.retrieveCoverCryptUserDecryptionKey( - userKeyID, - ) - const oldLocalDecryption = new CoverCryptHybridDecryption(oldUserKey) + const newPolicy = await client.rotateCoverCryptAttributes(mskID, [ + "Security::Simple", + ]) + const newPublicKey = await client.retrieveCoverCryptPublicMasterKey(mpkID) + const newLocalEncryption = new CoverCryptHybridEncryption( + newPolicy, + newPublicKey, + ) + expect(newPublicKey.bytes()).not.toEqual(oldPublicKey.bytes()) - expect( - (await client.coverCryptDecrypt(userKeyID, oldKmsCiphertext)).plaintext, - ).toEqual(oldPlaintext) - expect(oldLocalDecryption.decrypt(oldKmsCiphertext).plaintext).toEqual( - oldPlaintext, - ) + const newPlaintext = Uint8Array.from([4, 5, 6]) + const newKmsCiphertext = await client.coverCryptEncrypt( + mpkID, + "Security::Simple", + newPlaintext, + ) + const newLocalCiphertext = newLocalEncryption.encrypt( + "Security::Simple", + newPlaintext, + ) - expect( - (await client.coverCryptDecrypt(userKeyID, oldLocalCiphertext)).plaintext, - ).toEqual(oldPlaintext) - expect(oldLocalDecryption.decrypt(oldLocalCiphertext).plaintext).toEqual( - oldPlaintext, - ) + const newUserKey = await client.retrieveCoverCryptUserDecryptionKey( + userKeyID, + ) + const newLocalDecryption = new CoverCryptHybridDecryption(newUserKey) - const newPolicy = await client.rotateCoverCryptAttributes(mskID, [ - "Security::Simple", - ]) - const newPublicKey = await client.retrieveCoverCryptPublicMasterKey(mpkID) - const newLocalEncryption = new CoverCryptHybridEncryption( - newPolicy, - newPublicKey, - ) - expect(newPublicKey.bytes()).not.toEqual(oldPublicKey.bytes()) + expect( + (await client.coverCryptDecrypt(userKeyID, oldKmsCiphertext)).plaintext, + ).toEqual(oldPlaintext) + expect( + (await client.coverCryptDecrypt(userKeyID, newKmsCiphertext)).plaintext, + ).toEqual(newPlaintext) - const newPlaintext = Uint8Array.from([4, 5, 6]) - const newKmsCiphertext = await client.coverCryptEncrypt( - mpkID, - "Security::Simple", - newPlaintext, - ) - const newLocalCiphertext = newLocalEncryption.encrypt( - "Security::Simple", - newPlaintext, - ) + expect(oldLocalDecryption.decrypt(oldKmsCiphertext).plaintext).toEqual( + oldPlaintext, + ) + expect(newLocalDecryption.decrypt(oldKmsCiphertext).plaintext).toEqual( + oldPlaintext, + ) - const newUserKey = await client.retrieveCoverCryptUserDecryptionKey( - userKeyID, - ) - const newLocalDecryption = new CoverCryptHybridDecryption(newUserKey) + expect(() => oldLocalDecryption.decrypt(newKmsCiphertext)).toThrow() + expect(newLocalDecryption.decrypt(newKmsCiphertext).plaintext).toEqual( + newPlaintext, + ) - expect( - (await client.coverCryptDecrypt(userKeyID, oldKmsCiphertext)).plaintext, - ).toEqual(oldPlaintext) - expect( - (await client.coverCryptDecrypt(userKeyID, newKmsCiphertext)).plaintext, - ).toEqual(newPlaintext) + expect( + (await client.coverCryptDecrypt(userKeyID, oldLocalCiphertext)) + .plaintext, + ).toEqual(oldPlaintext) + expect( + (await client.coverCryptDecrypt(userKeyID, newLocalCiphertext)) + .plaintext, + ).toEqual(newPlaintext) + + expect(oldLocalDecryption.decrypt(oldLocalCiphertext).plaintext).toEqual( + oldPlaintext, + ) + expect(newLocalDecryption.decrypt(oldLocalCiphertext).plaintext).toEqual( + oldPlaintext, + ) - expect(oldLocalDecryption.decrypt(oldKmsCiphertext).plaintext).toEqual( - oldPlaintext, - ) - expect(newLocalDecryption.decrypt(oldKmsCiphertext).plaintext).toEqual( - oldPlaintext, - ) + expect(() => oldLocalDecryption.decrypt(newLocalCiphertext)).toThrow() + expect(newLocalDecryption.decrypt(newLocalCiphertext).plaintext).toEqual( + newPlaintext, + ) + } + }, + { + timeout: 10 * 1000, + }, +) - expect(() => oldLocalDecryption.decrypt(newKmsCiphertext)).toThrow() - expect(newLocalDecryption.decrypt(newKmsCiphertext).plaintext).toEqual( - newPlaintext, - ) +test( + "KMS Export wrapped key and Import unwrapping key", + async () => { + if (client !== undefined) { + const { Policy, PolicyAxis } = await CoverCrypt() - expect( - (await client.coverCryptDecrypt(userKeyID, oldLocalCiphertext)).plaintext, - ).toEqual(oldPlaintext) - expect( - (await client.coverCryptDecrypt(userKeyID, newLocalCiphertext)).plaintext, - ).toEqual(newPlaintext) + const importedCertificateUniqueIdentifier = await client.importPem( + "my_cert_id", + new TextEncoder().encode(NIST_P256_CERTIFICATE), + ["certificate", "x509"], + true, + ) - expect(oldLocalDecryption.decrypt(oldLocalCiphertext).plaintext).toEqual( - oldPlaintext, - ) - expect(newLocalDecryption.decrypt(oldLocalCiphertext).plaintext).toEqual( - oldPlaintext, - ) + await client.importPem( + "my_private_key_id", + new TextEncoder().encode(NIST_P256_PRIVATE_KEY), + ["private key", "x509"], + true, + ) - expect(() => oldLocalDecryption.decrypt(newLocalCiphertext)).toThrow() - expect(newLocalDecryption.decrypt(newLocalCiphertext).plaintext).toEqual( - newPlaintext, - ) + const policy = new Policy([ + new PolicyAxis( + "Security Level", + [ + { name: "Protected", isHybridized: false }, + { name: "Confidential", isHybridized: false }, + { name: "Top Secret", isHybridized: true }, + ], + true, + ), + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + const [privateKeyUniqueIdentifier, _publicKeyUniqueIdentifier] = + await client.createCoverCryptMasterKeyPair(policy) + + const decryptionKeyUniqueIdentifier = + await client.createCoverCryptUserDecryptionKey( + "(Department::MKG || Department::FIN) && Security Level::Confidential", + privateKeyUniqueIdentifier, + ) + + const wrappedUserDecryptionKey = await client.getWrappedKey( + decryptionKeyUniqueIdentifier, + importedCertificateUniqueIdentifier, + ) + + const unwrappedKeyIdentifier = await client.importKey( + "unwrappedUserDecryptionKey", + wrappedUserDecryptionKey, + true, + null, + true, + ) + + const initialKey = await client.getObject(decryptionKeyUniqueIdentifier) + const unwrappedKey = await client.getObject(unwrappedKeyIdentifier) + + if ( + initialKey.type === "Certificate" || + initialKey.type === "CertificateRequest" || + initialKey.type === "OpaqueObject" + ) { + throw new Error(`The KmsObject ${initialKey.type} cannot be unwrapped.`) + } + if ( + !(initialKey.value.keyBlock.keyValue instanceof KeyValue) || + initialKey.value.keyBlock.keyValue.attributes == null + ) { + throw new Error(`KmsObject is missing the attributes property.`) + } + if ( + unwrappedKey.type === "Certificate" || + unwrappedKey.type === "CertificateRequest" || + unwrappedKey.type === "OpaqueObject" + ) { + throw new Error(`The KmsObject ${initialKey.type} cannot be unwrapped.`) + } + if ( + !(unwrappedKey.value.keyBlock.keyValue instanceof KeyValue) || + unwrappedKey.value.keyBlock.keyValue.attributes == null + ) { + throw new Error(`KmsObject is missing the attributes property.`) + } + + expect(initialKey.value.keyBlock.keyValue.keyMaterial).toEqual( + unwrappedKey.value.keyBlock.keyValue.keyMaterial, + ) + } }, { timeout: 10 * 1000, @@ -928,208 +1091,209 @@ test( ) test( - "KMS Export wrapped key and Import unwrapping key", + "KMS distribute userDecryptionKey between two users", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } + if (client !== undefined) { + const { Policy, PolicyAxis } = await CoverCrypt() - const { Policy, PolicyAxis } = await CoverCrypt() + const importedCertificateUniqueIdentifier = await client.importPem( + "my_cert_id", + new TextEncoder().encode(NIST_P256_CERTIFICATE), + ["certificate", "x509"], + true, + ) - const importedCertificateUniqueIdentifier = await client.importPem( - "my_cert_id", - new TextEncoder().encode(NIST_P256_CERTIFICATE), - ["certificate", "x509"], - true, - ) + await client.importPem( + "my_private_key_id", + new TextEncoder().encode(NIST_P256_PRIVATE_KEY), + ["private key", "x509"], + true, + ) - await client.importPem( - "my_private_key_id", - new TextEncoder().encode(NIST_P256_PRIVATE_KEY), - ["private key", "x509"], - true, - ) + const policy = new Policy([ + new PolicyAxis( + "Security Level", + [ + { name: "Protected", isHybridized: false }, + { name: "Confidential", isHybridized: false }, + { name: "Top Secret", isHybridized: true }, + ], + true, + ), + new PolicyAxis( + "Department", + [ + { name: "FIN", isHybridized: false }, + { name: "MKG", isHybridized: false }, + { name: "HR", isHybridized: false }, + ], + false, + ), + ]) + + const [privateKeyUniqueIdentifier, publicKeyUniqueIdentifier] = + await client.createCoverCryptMasterKeyPair(policy) + + const decryptionKeyUniqueIdentifier = + await client.createCoverCryptUserDecryptionKey( + "(Department::MKG || Department::FIN) && Security Level::Confidential", + privateKeyUniqueIdentifier, + ) + + const wrappedUserDecryptionKey1 = await client.getWrappedKey( + decryptionKeyUniqueIdentifier, + importedCertificateUniqueIdentifier, + ) - const policy = new Policy([ - new PolicyAxis( - "Security Level", - [ - { name: "Protected", isHybridized: false }, - { name: "Confidential", isHybridized: false }, - { name: "Top Secret", isHybridized: true }, - ], - true, - ), - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], + const wrappedUserDecryptionKeyCentral = await client.importKey( + "wrappedUserDecryptionKeyCentral", + wrappedUserDecryptionKey1, false, - ), - ]) - - const [privateKeyUniqueIdentifier, _publicKeyUniqueIdentifier] = - await client.createCoverCryptMasterKeyPair(policy) + null, + true, + ) - const decryptionKeyUniqueIdentifier = - await client.createCoverCryptUserDecryptionKey( - "(Department::MKG || Department::FIN) && Security Level::Confidential", - privateKeyUniqueIdentifier, + const fetchedWrappedUserDecryptionKey = await client.getObject( + wrappedUserDecryptionKeyCentral, ) - const wrappedUserDecryptionKey = await client.getWrappedKey( - decryptionKeyUniqueIdentifier, - importedCertificateUniqueIdentifier, - ) + const unwrappedKeyIdentifier = await client.importKey( + "unwrappedUserDecryptionKey2", + fetchedWrappedUserDecryptionKey, + true, + null, + true, + ) - const unwrappedKeyIdentifier = await client.importKey( - "unwrappedUserDecryptionKey", - wrappedUserDecryptionKey, - true, - true, - ) + const clearText = new TextEncoder().encode("abcdefgh") + const ciphertext = await client.coverCryptEncrypt( + publicKeyUniqueIdentifier, + "Department::FIN && Security Level::Confidential", + clearText, + ) - const initialKey = await client.getObject(decryptionKeyUniqueIdentifier) - const unwrappedKey = await client.getObject(unwrappedKeyIdentifier) + const { plaintext } = await client.coverCryptDecrypt( + unwrappedKeyIdentifier, + ciphertext, + ) - if ( - initialKey.type === "Certificate" || - initialKey.type === "CertificateRequest" || - initialKey.type === "OpaqueObject" - ) { - throw new Error(`The KmsObject ${initialKey.type} cannot be unwrapped.`) - } - if ( - !(initialKey.value.keyBlock.keyValue instanceof KeyValue) || - initialKey.value.keyBlock.keyValue.attributes == null - ) { - throw new Error(`KmsObject is missing the attributes property.`) - } - if ( - unwrappedKey.type === "Certificate" || - unwrappedKey.type === "CertificateRequest" || - unwrappedKey.type === "OpaqueObject" - ) { - throw new Error(`The KmsObject ${initialKey.type} cannot be unwrapped.`) - } - if ( - !(unwrappedKey.value.keyBlock.keyValue instanceof KeyValue) || - unwrappedKey.value.keyBlock.keyValue.attributes == null - ) { - throw new Error(`KmsObject is missing the attributes property.`) + expect(clearText).toEqual(plaintext) } - - expect(initialKey.value.keyBlock.keyValue.keyMaterial).toEqual( - unwrappedKey.value.keyBlock.keyValue.keyMaterial, - ) }, { - timeout: 10 * 1000, + timeout: 30 * 1000, }, ) test( - "KMS distribute userDecryptionKey between two users", + "Grant and revoke Access", async () => { - const client = new KmsClient( - `http://${process.env.KMS_HOST || "localhost"}:9998`, - ) - if (!(await client.up())) { - console.error("No KMIP server. Skipping test") - return - } + if (client !== undefined) { + const kmsToken2 = process.env.AUTH0_TOKEN_2 + + // Create a simple KmsObject + const keyId = await client.createSymmetricKey() + const key = await client.getObject(keyId) + const client2 = new KmsClient( + `http://${process.env.KMS_HOST || "localhost"}:9998`, + kmsToken2, + ) - const { Policy, PolicyAxis } = await CoverCrypt() + // Check that another user cannot get this object + try { + await client2.getObject(keyId) + } catch (error) { + expect(error).toMatch(/(Item not found)/i) + } + + // Grant access to another user, to get this object + await client.grantAccess(keyId, "client2@cosmian.com", KMIPOperations.get) + const fetchedKey = await client2.getObject(keyId) + expect(fetchedKey).toEqual(key) + + // List associated access to this object + const access = await client.listAccess(keyId) + expect(await access.text()).toEqual( + '[{"user_id":"client2@cosmian.com","operations":["get"]}]', + ) - const importedCertificateUniqueIdentifier = await client.importPem( - "my_cert_id", - new TextEncoder().encode(NIST_P256_CERTIFICATE), - ["certificate", "x509"], - true, - ) + // Revoke access to this user + await client.revokeAccess( + keyId, + "client2@cosmian.com", + KMIPOperations.get, + ) + try { + await client2.getObject(keyId) + } catch (error) { + expect(error).toMatch(/(Item not found)/i) + } + } + }, + { + timeout: 30 * 1000, + }, +) - await client.importPem( - "my_private_key_id", - new TextEncoder().encode(NIST_P256_PRIVATE_KEY), - ["private key", "x509"], - true, - ) +test( + "Overwrite KeyWrappingData when importing key", + async () => { + if (client !== undefined) { + const keyUid = await client.createSymmetricKey() - const policy = new Policy([ - new PolicyAxis( - "Security Level", - [ - { name: "Protected", isHybridized: false }, - { name: "Confidential", isHybridized: false }, - { name: "Top Secret", isHybridized: true }, - ], + const importedCertificateUniqueIdentifier = await client.importPem( + "my_cert_id", + new TextEncoder().encode(NIST_P256_CERTIFICATE), + ["certificate", "x509"], true, - ), - new PolicyAxis( - "Department", - [ - { name: "FIN", isHybridized: false }, - { name: "MKG", isHybridized: false }, - { name: "HR", isHybridized: false }, - ], - false, - ), - ]) - - const [privateKeyUniqueIdentifier, publicKeyUniqueIdentifier] = - await client.createCoverCryptMasterKeyPair(policy) - - const decryptionKeyUniqueIdentifier = - await client.createCoverCryptUserDecryptionKey( - "(Department::MKG || Department::FIN) && Security Level::Confidential", - privateKeyUniqueIdentifier, ) - const wrappedUserDecryptionKey1 = await client.getWrappedKey( - decryptionKeyUniqueIdentifier, - importedCertificateUniqueIdentifier, - ) + await client.importPem( + "my_private_key_id", + new TextEncoder().encode(NIST_P256_PRIVATE_KEY), + ["private key", "x509"], + true, + ) - const wrappedUserDecryptionKeyCentral = await client.importKey( - "wrappedUserDecryptionKeyCentral", - wrappedUserDecryptionKey1, - false, - true, - ) + const wrappedKey = await client.getWrappedKey( + keyUid, + importedCertificateUniqueIdentifier, + ) - const fetchedWrappedUserDecryptionKey = await client.getObject( - wrappedUserDecryptionKeyCentral, - ) + // Key can be unwrapped directly specifying the private key id (matching the certificate) + let unwrappedKeyUid = await client.importKey( + "unwrappedSymmetricKey", + wrappedKey, + true, + "my_private_key_id", + true, + ) - const unwrappedKeyIdentifier = await client.importKey( - "unwrappedUserDecryptionKey2", - fetchedWrappedUserDecryptionKey, - true, - true, - ) + const unwrappedKey = await client.getObject(unwrappedKeyUid) - const clearText = new TextEncoder().encode("abcdefgh") - const ciphertext = await client.coverCryptEncrypt( - publicKeyUniqueIdentifier, - "Department::FIN && Security Level::Confidential", - clearText, - ) + if ( + unwrappedKey.type === "Certificate" || + unwrappedKey.type === "CertificateRequest" || + unwrappedKey.type === "OpaqueObject" + ) { + throw new Error( + `The KmsObject ${unwrappedKey.type} cannot be unwrapped.`, + ) + } - const { plaintext } = await client.coverCryptDecrypt( - unwrappedKeyIdentifier, - ciphertext, - ) + expect(unwrappedKey.value.keyBlock.keyWrappingData).toEqual(null) - expect(clearText).toEqual(plaintext) + // Key can also be unwrapped indirectly using the certificate id. In that case, KMS will locate the private key if already imported + unwrappedKeyUid = await client.importKey( + "unwrappedSymmetricKey", + wrappedKey, + true, + "my_cert_id", + true, + ) + } }, { - timeout: 30 * 1000, + timeout: 10 * 1000, }, ) diff --git a/tests/cover_crypt.test.ts b/tests/cover_crypt.test.ts index 4655cbe6..c03fe3d8 100644 --- a/tests/cover_crypt.test.ts +++ b/tests/cover_crypt.test.ts @@ -103,7 +103,7 @@ test("Demo using Wasm only", async () => { const protectedMkgCleartext = new CoverCryptHybridDecryption( confidentialMkgUserKeyBytes, ).decrypt(protectedMkgCiphertext) - expect(protectedMkgCleartext.plaintext).toEqual(protectedMkgData) + expect(protectedMkgData).toEqual(protectedMkgCleartext.plaintext) try { // will throw @@ -264,7 +264,7 @@ test("Demo using KMS", async () => { confidentialMkgUserKeyUid, protectedMkgCiphertext, ) - expect(protectedMkgCleartext.plaintext).toEqual(protectedMkgData) + expect(protectedMkgData).toEqual(protectedMkgCleartext.plaintext) // .. however, it can neither decrypt a marketing message with higher security: try { @@ -351,7 +351,7 @@ test("Demo using KMS", async () => { ) expect(confidentialMkgData).toEqual(newConfidentialMkgCleartext.plaintext) - // However, the old, non-rekeyed `confidential marketing` user key can still decrypt the old `protected marketing` message + // However, the old, non-rekeyed `confidential marketing` user key can still decrypt the old `protected marketing` message // but **not** the new `confidential marketing` message: // protectedMkgCiphertext diff --git a/tests/findex.test.ts b/tests/findex.test.ts index 384a6d68..9c761e48 100644 --- a/tests/findex.test.ts +++ b/tests/findex.test.ts @@ -1,34 +1,34 @@ +import { fromByteArray, toByteArray } from "base64-js" +import Database from "better-sqlite3" +import { randomBytes } from "crypto" +import * as fs from "fs" +import * as os from "os" +import { createClient, defineScript } from "redis" +import { expect, test } from "vitest" import { FetchChains, FetchEntries, + Findex, + FindexCloud, + FindexKey, IndexedEntry, IndexedValue, - FindexKey, - SearchResults, + InsertChains, Keyword, + KeywordIndexEntry, Label, Location, + LocationIndexEntry, ProgressResults, - Findex, - FindexCloud, + SearchResults, UidsAndValues, UidsAndValuesToUpsert, - InsertChains, UpsertEntries, - LocationIndexEntry, - KeywordIndexEntry, - generateAliases, callbacksExamplesBetterSqlite3, callbacksExamplesInMemory, + generateAliases, } from ".." import { USERS } from "./data/users" -import { expect, test } from "vitest" -import { createClient, defineScript } from "redis" -import { randomBytes } from "crypto" -import Database from "better-sqlite3" -import * as fs from "fs" -import * as os from "os" -import { fromByteArray, toByteArray } from "base64-js" const FINDEX_TEST_KEY = "6hb1TznoNQFvCWisGWajkA==" const FINDEX_TEST_LABEL = "Some Label" @@ -226,7 +226,7 @@ async function runWithFindexCallbacks( // eslint-disable-next-line jsdoc/require-jsdoc async function runInFindexCloud(): Promise { const baseUrl = `http://${process.env.FINDEX_CLOUD_HOST ?? "127.0.0.1"}:${ - process.env.FINDEX_CLOUD_PORT ?? "8080" + process.env.FINDEX_CLOUD_PORT ?? "9090" }` let response