├── pk8.pem ├── .gitignore ├── screenshot-eslint.png ├── setup ├── setup.js └── environment.js ├── scripts └── build.sh ├── tsconfig-typecheck.json ├── .prettierrc.json ├── src ├── utils.ts ├── __tests__ │ ├── __snapshots__ │ │ └── index.test.ts.snap │ └── index.test.ts └── index.ts ├── public.pem ├── tsconfig.json ├── .eslintrc.json ├── key.pem ├── package.json ├── .github └── workflows │ └── main.yml ├── README.md ├── jest.config.js └── examples └── node-yaml.ts /pk8.pem: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SocialGouv/aes-gcm-rsa-oaep/HEAD/pk8.pem -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | npm-debug.log 4 | coverage 5 | yarn-error.log 6 | tmp 7 | coverage 8 | -------------------------------------------------------------------------------- /screenshot-eslint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SocialGouv/aes-gcm-rsa-oaep/HEAD/screenshot-eslint.png -------------------------------------------------------------------------------- /setup/setup.js: -------------------------------------------------------------------------------- 1 | const crypto = require('@peculiar/webcrypto'); 2 | 3 | // eslint-disable-next-line no-undef 4 | window.crypto = new crypto.Crypto(); 5 | -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash -ex 2 | 3 | yarn typecheck 4 | yarn lint 5 | yarn test 6 | 7 | rm -rf dist 8 | tsc -p . 9 | rm -fr dist/__tests__ 10 | -------------------------------------------------------------------------------- /tsconfig-typecheck.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "noEmit": true, 5 | "sourceMap": false, 6 | "incremental": false 7 | }, 8 | "include": ["src"] 9 | } 10 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "typescript", 3 | "singleQuote": true, 4 | "trailingComma": "es5", 5 | "printWidth": 120, 6 | "overrides": [ 7 | { 8 | "files": [".*", "*.json"], 9 | "options": { "parser": "json" } 10 | } 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const arrayBufferToB64 = (arrayBuffer: ArrayBuffer): string => 2 | btoa(String.fromCharCode.apply(null, new Uint8Array(arrayBuffer))); 3 | 4 | /** 5 | * Converts a string to an ArrayBuffer 6 | * @param str 7 | */ 8 | export function str2ab(str: string): ArrayBuffer { 9 | return new TextEncoder().encode(str); 10 | } 11 | -------------------------------------------------------------------------------- /public.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PUBLIC KEY----- 2 | MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr4Mmh83BbIgazKmxQOBx 3 | FYZPPMbhWp7gguC42a/27iI6leSc7fQxK2YGP7Cs9I8cByoYpCsUR8PqJD24TnWq 4 | VMmoT2xYVUTEyIIVdsrihqBfyJM5sHNRaQSclkbRbjWvwtoS/NAm9fSkBb2NGQLX 5 | q7brNkCe1zmFU8Q9GihrzloM/L9xI9QqYTEqfXVrJBYeB6KAnpqXQznQNtYZbYiX 6 | 6zthrLrbzXP8PJ1wgSQTx+ww6FUiXAwqLjMG8XO3PgBhLCoN7pP4spQc+7N6orBC 7 | 6TY65KquIwKaNJmf2K+FBYEXJvCmOXptI+pbLIV1tg/lHb876DoWDF73kxtgIuok 8 | PQIDAQAB 9 | -----END PUBLIC KEY----- 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist/", 4 | "sourceMap": true, 5 | "module": "commonjs", 6 | "moduleResolution": "node", 7 | "noEmitOnError": true, 8 | "target": "es5", 9 | "declaration": true, 10 | "removeComments": false, 11 | "noImplicitAny": true, 12 | "forceConsistentCasingInFileNames": true, 13 | "noUnusedLocals": true, 14 | "pretty": true, 15 | "allowSyntheticDefaultImports": true 16 | }, 17 | "include": ["src"] 18 | } 19 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "@typescript-eslint/parser", 3 | "plugins": ["typescript", "import"], 4 | "extends": [ 5 | "airbnb", 6 | "prettier", 7 | "typescript", 8 | "typescript/prettier", 9 | "plugin:import/errors", 10 | "plugin:import/warnings", 11 | "plugin:import/typescript" 12 | ], 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "import/no-unresolved": "error", 16 | "import/extensions": "off", 17 | "require-jsdoc": "off", 18 | "@typescript-eslint/explicit-function-return-type": "off" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /setup/environment.js: -------------------------------------------------------------------------------- 1 | // __test-utils__/custom-jest-environment.js 2 | // Stolen from: https://github.com/ipfs/jest-environment-aegir/blob/master/src/index.js 3 | // Overcomes error from jest internals.. this thing: https://github.com/facebook/jest/issues/6248 4 | 5 | // eslint-disable-next-line import/no-extraneous-dependencies 6 | const NodeEnvironment = require('jest-environment-jsdom'); 7 | 8 | // Jest definition of ArrayBuffer, Uint8Array and Uint32Array messes with type assertions, so we override them 9 | class MyEnvironment extends NodeEnvironment { 10 | constructor(config) { 11 | super({ 12 | ...config, 13 | globals: { 14 | ...config.globals, 15 | Uint32Array, 16 | Uint8Array, 17 | ArrayBuffer, 18 | TextEncoder, 19 | TextDecoder 20 | }, 21 | }); 22 | } 23 | 24 | // eslint-disable-next-line no-empty-function,class-methods-use-this 25 | async setup() {} 26 | 27 | // eslint-disable-next-line no-empty-function,class-methods-use-this 28 | async teardown() {} 29 | } 30 | 31 | module.exports = MyEnvironment; 32 | -------------------------------------------------------------------------------- /key.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN RSA PRIVATE KEY----- 2 | MIIEpgIBAAKCAQEAr4Mmh83BbIgazKmxQOBxFYZPPMbhWp7gguC42a/27iI6leSc 3 | 7fQxK2YGP7Cs9I8cByoYpCsUR8PqJD24TnWqVMmoT2xYVUTEyIIVdsrihqBfyJM5 4 | sHNRaQSclkbRbjWvwtoS/NAm9fSkBb2NGQLXq7brNkCe1zmFU8Q9GihrzloM/L9x 5 | I9QqYTEqfXVrJBYeB6KAnpqXQznQNtYZbYiX6zthrLrbzXP8PJ1wgSQTx+ww6FUi 6 | XAwqLjMG8XO3PgBhLCoN7pP4spQc+7N6orBC6TY65KquIwKaNJmf2K+FBYEXJvCm 7 | OXptI+pbLIV1tg/lHb876DoWDF73kxtgIuokPQIDAQABAoIBAQCeXgnjWj5g7wK3 8 | j5q4PozrbjCLV606NsfAcIN7MXLvdwVEAW+0qrW/QiT7TTESzxrsQAjSAWkgRGA+ 9 | aU6nxTZ3oSp01/9wmUey4OX7NaBm98jV1Dqmw7c+uoGMe8Q33MuGV33wjuXI4wEp 10 | iNsLKWxvrfLZFj/9VhK+/gBgXP7BxzLzpuXjPcULXrKsnL4qWQETLloLVX0WGJhv 11 | 77/hMdFl+AtQq2FD85jX4G0l5OZhUlGje4zRFQ/n4SSTI7HaL2gRFX9tz2w7uKaf 12 | /FkSpU/NjrhjpYDU+4lq6b92EHIz6z5447Sc8xAi0kyvgnu43FbByb5Nd9NKNI2y 13 | lp3dy4UhAoGBANj9PAIm9Eaud+yo4D6d6yqdz/dtvFdBLqngIW9ZGKCNFvF/B7bf 14 | 2ZECZ4vb97dmLsXt5CYKnyVbhS9BPaLwrzvSb2MHalx24E9dh3WDYhZrmg0XnJQc 15 | P3Ucni8BW+u12Z37yWOcxL8jbAanlxJzlkFi5MKp4GLCXqseCuz7SjTlAoGBAM8Q 16 | +SXBqtFrkDb0WQFKCUF7r8kRWsCipSH7OQxhZHGse9Yy5EQ+RZZO7aY+l1yKf94C 17 | 6rfNqG1EPCINT6w/s26jwQMFH+4m103xI4sdrQxD/N+YOTZ/JMf7X/tjxVk7BjI3 18 | /zkEHY0cL0iIalY9mswxd8zw5xN6CYvGFOKZzFR5AoGBAMUUm+BIiS15YSrt5154 19 | CBPY6f6NCLcnWL6p9zQu9BM+kkwdWGBcyDrQuENrMn68rFbRTprOouVHTpww4U6Q 20 | 1Fe9NbX4Ej6RKgJrrJCrF/fNG0ow2+IaFfjEWVfQIDDiJhk7ixqSVJBWF9Oje68i 21 | lKImtCeqK0cHyvwYeUvsoOWRAoGBAJdYQ5KKLC0nHmBcVlWv5HX9Tm17Bsb32hSt 22 | R3Q6Fy1SsazMw7TxgvEqvV6eLwmPnYgKv74aTjmW/xCwhARVBvQeMmvfqgfqUcIB 23 | N0ZuKVZmtwRCgcbIRXz5yZy3vr3Ke2vnK99jl6nU6OZt9rMUEfmfSn37shm1QBbu 24 | b8N0QDmZAoGBAJT5lCVp5AicUNlr9OlgLuybMUWo3iPKCoAtlRyXyoacVtMdUtjc 25 | wZ35VnDQRwTizIk1TPZQjOOqiSkOg6QfGr/QVohAMsVPaa/xxOJp8cM35tOHP0tB 26 | eJHb0tGmMPpU7HeJqKrvknHXX6kfwPs2B4PHo826xmHjTjUCBmwPGv/u 27 | -----END RSA PRIVATE KEY----- 28 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@socialgouv/aes-gcm-rsa-oaep", 3 | "description": "Kubeseal aes-gcm-rsa-oaep encryption implementaton in JavaScript", 4 | "version": "1.0.0", 5 | "author": { 6 | "name": "Clement Berthou", 7 | "email": "clement.berthou@benextcompany.com" 8 | }, 9 | "engines": { 10 | "node": ">=15" 11 | }, 12 | "scripts": { 13 | "test": "yarn jest --detectOpenHandles", 14 | "typecheck": "yarn tsc --project ./tsconfig-typecheck.json", 15 | "lint": "eslint 'src/**/*.{ts,tsx}'", 16 | "format": "prettier --write 'src/**/*.{ts,tsx}'", 17 | "build": "./scripts/build.sh", 18 | "prepublish": "npm run build" 19 | }, 20 | "main": "dist/index.js", 21 | "typings": "dist/index.d.ts", 22 | "license": "Apache-2.0", 23 | "files": [ 24 | "dist/" 25 | ], 26 | "devDependencies": { 27 | "@peculiar/webcrypto": "^1.1.4", 28 | "@types/jest": "^25.2.3", 29 | "@types/node": "^14.0.4", 30 | "@typescript-eslint/eslint-plugin": "^2.34.0", 31 | "@typescript-eslint/parser": "^2.34.0", 32 | "eslint": "^7.0.0", 33 | "eslint-config-airbnb": "^18.1.0", 34 | "eslint-config-prettier": "^6.11.0", 35 | "eslint-config-typescript": "^3.0.0", 36 | "eslint-formatter-pretty": "^3.0.1", 37 | "eslint-plugin-import": "^2.7.0", 38 | "eslint-plugin-jsx-a11y": "^6.0.2", 39 | "eslint-plugin-prettier": "^3.0.0", 40 | "eslint-plugin-react": "^7.20.0", 41 | "eslint-plugin-typescript": "^0.14.0", 42 | "jest": "^26.0.1", 43 | "jest-environment-jsdom": "^26.6.2", 44 | "jest-environment-node": "^26.6.2", 45 | "prettier": "^2.0.5", 46 | "ts-jest": "^26.0.0", 47 | "ts-node": "^8.10.1", 48 | "typescript": "^3.9.3", 49 | "typescript-eslint-parser": "^22.0.0" 50 | }, 51 | "dependencies": { 52 | "@types/node-forge": "^0.9.7", 53 | "hybrid-crypto-js": "^0.2.4", 54 | "node-forge": "^0.10.0" 55 | }, 56 | "publishConfig": { 57 | "access": "public" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Test and build 2 | on: 3 | - push 4 | - pull_request 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | 10 | strategy: 11 | matrix: 12 | node-version: 13 | - 15.x 14 | 15 | steps: 16 | - name: Set up Node.js ${{ matrix.node-version }} 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: ${{ matrix.node-version }} 20 | 21 | - uses: actions/checkout@v4 22 | 23 | - name: Installing 24 | run: yarn --frozen-lockfile --perfer-offline --link-duplicates 25 | 26 | - name: Send test coverage to codecov 27 | continue-on-error: true 28 | uses: codecov/codecov-action@v1 29 | 30 | - name: Build 31 | run: yarn build 32 | 33 | - name: Archive dist components 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: build 37 | path: dist/ 38 | 39 | release: 40 | needs: 41 | - build 42 | if: github.event_name == 'push' 43 | runs-on: ubuntu-latest 44 | steps: 45 | - name: Set up Node 46 | uses: actions/setup-node@v4 47 | with: 48 | node-version: 15.x 49 | 50 | - uses: actions/checkout@v4 51 | 52 | - name: Download dist from build job 53 | uses: actions/download-artifact@v4 54 | with: 55 | name: build 56 | path: dist 57 | 58 | - name: Semantic Release 59 | uses: cycjimmy/semantic-release-action@v3 60 | with: 61 | extra_plugins: | 62 | @semantic-release/changelog 63 | @semantic-release/exec 64 | @semantic-release/git 65 | @semantic-release/npm 66 | env: 67 | GIT_AUTHOR_EMAIL: ${{ secrets.SOCIALGROOVYBOT_EMAIL }} 68 | GIT_AUTHOR_NAME: ${{ secrets.SOCIALGROOVYBOT_NAME }} 69 | GIT_COMMITTER_EMAIL: ${{ secrets.SOCIALGROOVYBOT_EMAIL }} 70 | GIT_COMMITTER_NAME: ${{ secrets.SOCIALGROOVYBOT_NAME }} 71 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 72 | NPM_TOKEN: ${{ secrets.SOCIALGROOVYBOT_NPM_TOKEN }} 73 | -------------------------------------------------------------------------------- /src/__tests__/__snapshots__/index.test.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`index getSealedSecret should getSealedSecret for scope=cluster 1`] = ` 4 | Object { 5 | "apiVersion": "bitnami.com/v1alpha1", 6 | "kind": "SealedSecret", 7 | "metadata": Object { 8 | "annotations": Object { 9 | "sealedsecrets.bitnami.com/cluster-wide": "true", 10 | }, 11 | "name": "some-name", 12 | "namespace": "some-namespace", 13 | }, 14 | "spec": Object { 15 | "encryptedData": Object { 16 | "value1": Any, 17 | "value2": Any, 18 | }, 19 | "template": Object { 20 | "metadata": Object { 21 | "annotations": Object { 22 | "sealedsecrets.bitnami.com/cluster-wide": "true", 23 | }, 24 | "name": "some-name", 25 | }, 26 | "type": "Opaque", 27 | }, 28 | }, 29 | } 30 | `; 31 | 32 | exports[`index getSealedSecret should getSealedSecret for scope=namespace 1`] = ` 33 | Object { 34 | "apiVersion": "bitnami.com/v1alpha1", 35 | "kind": "SealedSecret", 36 | "metadata": Object { 37 | "annotations": Object { 38 | "sealedsecrets.bitnami.com/namespace-wide": "true", 39 | }, 40 | "name": "some-name", 41 | "namespace": "some-namespace", 42 | }, 43 | "spec": Object { 44 | "encryptedData": Object { 45 | "value1": Any, 46 | "value2": Any, 47 | }, 48 | "template": Object { 49 | "metadata": Object { 50 | "annotations": Object { 51 | "sealedsecrets.bitnami.com/namespace-wide": "true", 52 | }, 53 | "name": "some-name", 54 | }, 55 | "type": "Opaque", 56 | }, 57 | }, 58 | } 59 | `; 60 | 61 | exports[`index getSealedSecret should getSealedSecret for scope=strict 1`] = ` 62 | Object { 63 | "apiVersion": "bitnami.com/v1alpha1", 64 | "kind": "SealedSecret", 65 | "metadata": Object { 66 | "annotations": Object {}, 67 | "name": "some-name", 68 | "namespace": "some-namespace", 69 | }, 70 | "spec": Object { 71 | "encryptedData": Object { 72 | "value1": Any, 73 | "value2": Any, 74 | }, 75 | "template": Object { 76 | "metadata": Object { 77 | "annotations": Object {}, 78 | "name": "some-name", 79 | }, 80 | "type": "Opaque", 81 | }, 82 | }, 83 | } 84 | `; 85 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AES-GCM + RSA OAEP encryption Npm version 2 | 3 | 4 | AES-GCM + RSA-OAEP encryption/decryption using WebCrypto API in NodeJS or in the browser 5 | 6 | Tests uses `@peculiar/webcrypto` for polyfilling browser crypto api. 7 | 8 | This can be used to replace [kubeseal](https://github.com/bitnami-labs/sealed-secrets) encryption in JavaScript environments. 9 | 10 | See demo : http://socialgouv.github.io/webseal 11 | 12 | ## Usage 13 | 14 | ### High level 15 | 16 | ```js 17 | import { encryptValue, encryptValues, getSealedSecret } from "@socialgouv/aes-gcm-rsa-oaep" 18 | 19 | // encrypt single value 20 | const encryptedValue = encryptValue({ 21 | pemKey: "somekey", 22 | scope: "cluster", 23 | namespace: "dev", 24 | name: "my-secret", 25 | value: "plain-value"; 26 | }); 27 | 28 | // encrypt multiple values 29 | const encryptedValue = encryptValues({ 30 | pemKey: "somekey", 31 | scope: "cluster", 32 | namespace: "dev", 33 | name: "my-secret", 34 | values: { 35 | value1: "plain1", 36 | value2: "plain2" 37 | } 38 | }); 39 | 40 | // get sealed-secret 41 | const sealedSecret = getSealedSecret({ 42 | pemKey: "somekey", 43 | scope: "cluster", 44 | namespace: "dev", 45 | name: "my-secret", 46 | values: { 47 | value1: "plain1", 48 | value2: "plain2" 49 | } 50 | }); 51 | ``` 52 | 53 | ## Low level 54 | 55 | ```js 56 | import { pki } from 'node-forge'; 57 | import { HybridEncrypt, pemPublicKeyToCryptoKey } from '@socialgouv/aes-gcm-rsa-oaep'; 58 | 59 | const publicKeyPem = pki.publicKeyToPem(cert.publicKey); 60 | const publicKey = await pemPublicKeyToCryptoKey(publicKeyPem); 61 | 62 | const plainText = 'Bonjour le monde'; 63 | const label = Buffer.from(''); 64 | 65 | const result = await HybridEncrypt(publicKey, plainText, label); 66 | 67 | const sealedText = Buffer.from(result).toString('base64'); 68 | ``` 69 | 70 | ## Encryption Algorithm 71 | 72 | To encrypt content, we go through the following steps : 73 | 74 | - Generate a 128 bits AES key 75 | 76 | - Encrypt the payload using the AES-GCM algorithm (with the previously generated key). We use a 12 bits of 0 as IV, because the key is only used once. 77 | 78 | - Encrypt the AES key using the RSA-OAEP algorithm (using the provided public key). 79 | 80 | - Generate the output payload this way : 81 | (RSA payload length as 2 bytes integer) || (RSA encrypted aes key) || (AES encrypted payload) 82 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | const NO_COVERAGE = process.env.NO_COVERAGE === '1'; 2 | const CLEAR_CONSOLE = process.env.CLEAR_CONSOLE === '1'; 3 | 4 | const notice = () => console.log('Using Jest config from `jest.config.js`'); 5 | 6 | if (CLEAR_CONSOLE) { 7 | require('clear')(); 8 | console.log(); 9 | notice(); 10 | console.log('Clearing console due to CLEAR_CONSOLE=1'); 11 | } else { 12 | notice(); 13 | } 14 | 15 | if (NO_COVERAGE) { 16 | console.log('Coverage not collected due to NO_COVERAGE=1'); 17 | } 18 | 19 | console.log('Type checking is disabled during Jest for performance reasons, use `jest typecheck` when necessary.'); 20 | 21 | module.exports = { 22 | rootDir: __dirname, 23 | roots: [''], 24 | cache: true, 25 | verbose: true, 26 | cacheDirectory: '/tmp/jest', 27 | moduleFileExtensions: ['ts', 'tsx', 'js', 'json'], 28 | // preset configs 29 | // preset: 'ts-jest/presets/js-with-ts', 30 | // which files to test and which to ignore 31 | testMatch: ['**/__tests__/*.test.(ts|tsx)'], 32 | testPathIgnorePatterns: ['/node_modules/', '/tmp/', '/coverage/', '/stories/', '/\\.storybook/'], 33 | // don't watch for file changes in node_modules 34 | watchPathIgnorePatterns: ['/node_modules/'], 35 | // jest automock settings 36 | automock: false, 37 | unmockedModulePathPatterns: ['/node_modules/'], 38 | // test environment setup 39 | setupFiles: [`${__dirname}/setup/setup.js`], 40 | testEnvironment: `${__dirname}/setup/environment.js`, 41 | // setupFilesAfterEnv: [`${__dirname}/setup/setupAfterEnv.ts`], 42 | // coverage settings 43 | collectCoverage: NO_COVERAGE === false, 44 | collectCoverageFrom: NO_COVERAGE ? [] : ['**/*.{ts,tsx}', '!**/*.d.ts', '!**/node_modules/**'], 45 | coveragePathIgnorePatterns: ['/node_modules/', '\\.json$', '/__tests__/', '/stories/', '/\\.storybook/'], 46 | 47 | globals: { 48 | 'ts-jest': { 49 | tsConfig: `${__dirname}/tsconfig.json`, 50 | 51 | // https://huafu.github.io/ts-jest/user/config/diagnostics 52 | diagnostics: false, 53 | 54 | // Makes jest test run much faster, BUT, without type checking. 55 | // Type checking in CI is done with `tsc --noEmit` or `yarn typecheck` command. 56 | // https://huafu.github.io/ts-jest/user/config/isolatedModules 57 | isolatedModules: true, 58 | }, 59 | }, 60 | 61 | transformIgnorePatterns: ['/node_modules/(?!(lodash-es|antd|[^/]+/es|rc-animate|rc-util)/).*'], 62 | transform: { 63 | '\\.(ts|tsx)$': 'ts-jest', 64 | '/node_modules/((lodash-es|[^/]+/es)|rc-animate|rc-util)/.*': 'ts-jest', 65 | }, 66 | }; 67 | -------------------------------------------------------------------------------- /examples/node-yaml.ts: -------------------------------------------------------------------------------- 1 | import { pki } from 'node-forge'; 2 | import { HybridEncrypt, pemPublicKeyToCryptoKey } from '../src'; 3 | 4 | /* 5 | generate a sealed-secret from a public key in NodeJS 6 | NodeJS require `node-forge` to create the cryptoKey from the raw public key 7 | */ 8 | 9 | // some cluster public certificate 10 | const cert = pki.certificateFromPem( 11 | `-----BEGIN CERTIFICATE----- 12 | MIIErjCCApagAwIBAgIRAOqAV9ZpCl1cwMunTHirqXwwDQYJKoZIhvcNAQELBQAw 13 | ADAeFw0yMDA1MjYwODQxMTBaFw0zMDA1MjQwODQxMTBaMAAwggIiMA0GCSqGSIb3 14 | DQEBAQUAA4ICDwAwggIKAoICAQDc5wz/el5+ghmNAsQzpFK3jtLRIDcoYgyeGHaG 15 | LH/FCuatiE3qhFeJt2uYQ+GFmEg3e9N/6xDY8NgUMvLdUlAfl5TqV5H96EXTSsGE 16 | aRL2K4EIIdmFflas+r7dh5IYtmV8NTXW2ESLkyOe3PufK/OhdD/xcxglYLM+JnR2 17 | pbKqFvFcsk+LHK3FTWt0xSeqgd+6G9PIxUAbHtoKYI9R41rWnCXY7n0zp+zWD/6j 18 | EDsbCgFJ657Zwwn0XXGkQqItdZsN+ETIcBVIdqemzcksd7RRUZQiJF8imM5S/wj/ 19 | 1o55JwAPLqg5Odj5T/pQq4gSG+UG1P0eJxBQpUCgmtW1SAjS2Gv9kkuw/PAMmLAa 20 | N0uHnM8TY9OPLLMENjmn9IhvYrhj5lKff4NXNC4nHFSDUAVfu5kNHrYn9WnCCw/f 21 | ZE81p5Do7IcsbfmgEq3Ttg7p1eqXpH9NE9TWSZTz3HW/0lmEuIsNxjX/692DBeP/ 22 | CCUiYlAef704Woo84jwIz4jEwfjonsFVaHbbtIGyqjvTlWcYfQnCLvnB+Plqutc/ 23 | /p8/ViXXfuAwj/K3lYvhGWt2TNLo963XG/pqBVbZxYVI4m7IzrZ0vhbhck1oGYN8 24 | yX8sYPBUn5/5X/bJ8Xz1KsAeCfSXAvCjAZ3MfQDwxbsEURVQAeiDcF61G75/Lq/Z 25 | Bm74hwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 26 | BgkqhkiG9w0BAQsFAAOCAgEAKLvQtDV5QVNUEcg3ywL03IwtNWmU8EqqASgckVAc 27 | dmeGnFwV7+7VnrIHI34xCZDhAVEnlMAb0oELgTDSstTx4p25rjqOLQfPcb0TsPko 28 | cpJHS00trLsLX4DYoZ1dLprgySGrC9jQ/FMYMK1oZ0M04gA/U9alNVNu2DWKosHu 29 | JGRYBbXVnse5rZi3hl4GV5Vq2ZR/3GHL9xgcjMcSLqHhXoSULLm5qEBUA5flV0BJ 30 | bY2FEfpm1NHSh5vOaA5t7lrW/XqiAuo8lJM2Ztg/dsX6Zxq7Memq8nqMRpoMFbdj 31 | i0jTxlPL1ssVHvvmWcLsdx9fHX7XaNGZ4ulA6BIL4DZQMPxvtFk9alqbc9WnjsqL 32 | /8QA3STXkTSzWrSsTUcabxlp5+MLHRf31iE1dlGVv7FVLjPy29T145eSzoNPM9sN 33 | 1+aBLnXDCj5HUwto8+iKJn7VTURty5KceUFSeURXM2IKYYTec8MElLX0PeXi+SvO 34 | W37ZCn0RMnljus5aTUPbRHNvJ3Ut/1WDLQu8X0wIm509F1A80Udg5FTxHasBizM2 35 | L+rpr2923MG1JSitDfvNj1rxx1FLdynP84SOEJDCtbB8YRcDUC4bHKrAcG4FBAFX 36 | UDzbbsoR58onjcZW3QH4mvV9K3+llwHSo8zfYyKRhF8pUUJHp7A/8DgMYQyYAv2z 37 | bag= 38 | -----END CERTIFICATE-----` 39 | ); 40 | 41 | const generateSealedSecret = async () => { 42 | const publicKeyPem = pki.publicKeyToPem(cert.publicKey); 43 | const publicKey = await pemPublicKeyToCryptoKey(publicKeyPem); 44 | 45 | const encryptedText = 'Bonjour le monde'; 46 | 47 | // https://github.com/bitnami-labs/sealed-secrets/blob/717b7c1cae24af1ead57992b78196ff6dc70025e/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go#L77 48 | const label = Buffer.from(''); 49 | 50 | const result = await HybridEncrypt(publicKey, encryptedText, label); 51 | 52 | const sealedText = Buffer.from(result).toString('base64'); 53 | 54 | console.log(` 55 | apiVersion: bitnami.com/v1alpha1 56 | kind: SealedSecret 57 | metadata: 58 | annotations: 59 | &a1 60 | sealedsecrets.bitnami.com/cluster-wide: "true" 61 | name: app-sealed-secret-js 62 | namespace: sample-next-app 63 | spec: 64 | encryptedData: 65 | SECRET: ${sealedText} 66 | template: 67 | metadata: 68 | annotations: *a1 69 | name: app-sealed-secret-js 70 | type: Opaque 71 | 72 | `); 73 | }; 74 | 75 | generateSealedSecret(); 76 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { pki } from 'node-forge'; 2 | 3 | // Utility functions come from here : https://developers.google.com/web/updates/2012/06/How-to-convert-ArrayBuffer-to-and-from-String 4 | import { str2ab } from './utils'; 5 | 6 | type Scope = 'cluster' | 'namespace' | 'strict'; 7 | 8 | type GetLabelParams = { 9 | scope: Scope; 10 | namespace: string; 11 | name: string; 12 | }; 13 | 14 | type EncryptValueParams = { 15 | pemKey: string; 16 | scope: Scope; 17 | namespace: string; 18 | name: string; 19 | value: string; 20 | }; 21 | 22 | type EncryptParams = { 23 | publicKey: CryptoKey; 24 | value: string; 25 | label: Buffer; 26 | }; 27 | 28 | type EncryptValuesParams = { 29 | pemKey: string; 30 | scope: Scope; 31 | namespace: string; 32 | name: string; 33 | values: Record; 34 | }; 35 | 36 | type GetSealedSecretParams = { 37 | pemKey: string; 38 | scope: Scope; 39 | namespace: string; 40 | name: string; 41 | values: Record; 42 | }; 43 | 44 | const crypto = typeof window !== 'undefined' && window.crypto ? window.crypto : require('crypto').webcrypto; 45 | 46 | // Recommended nonce size for AES GCM is 96 bits 47 | const AES_GCM_NONCE_SIZE = 12; 48 | 49 | /** 50 | * Converts an ArrayBuffer to a string 51 | * @param buf 52 | */ 53 | function ab2str(buf: ArrayBuffer): string { 54 | return new TextDecoder().decode(buf); 55 | } 56 | 57 | function numberToLEBuffer(num: number): ArrayBuffer { 58 | const firstNumber = num / 256; 59 | const secondNumber = num % 256; 60 | return new Uint8Array([firstNumber, secondNumber]); 61 | } 62 | 63 | function numberFromLEBuffer(buffer: ArrayBuffer): number { 64 | const intArray = new Uint8Array(buffer.slice(0, 2)); 65 | return intArray[0] * 256 + intArray[1]; 66 | } 67 | 68 | /** 69 | * Encrypts a string using AES-GCM algorithm with a 0 nonce 70 | * @param key 71 | * @param str 72 | */ 73 | const aesGcmEncrypt = async (key: CryptoKey, str: string): Promise => { 74 | const nonce = new Uint8Array(AES_GCM_NONCE_SIZE); 75 | nonce.fill(0); 76 | return crypto.subtle.encrypt( 77 | { 78 | name: 'AES-GCM', 79 | iv: nonce, 80 | }, 81 | key, 82 | str2ab(str) 83 | ); 84 | }; 85 | 86 | /** 87 | * Decrypts an array buffer using AES-GCM algorithm with a 0 nonce 88 | * @param key 89 | * @param enc 90 | */ 91 | const aesGcmDecrypt = async (key: CryptoKey, enc: ArrayBuffer): Promise => { 92 | const nonce = new Uint8Array(AES_GCM_NONCE_SIZE); 93 | nonce.fill(0); 94 | const result = await crypto.subtle.decrypt( 95 | { 96 | name: 'AES-GCM', 97 | iv: nonce, 98 | }, 99 | key, 100 | enc 101 | ); 102 | 103 | return ab2str(result); 104 | }; 105 | 106 | const sessionKeyLength = 256; 107 | 108 | /** 109 | * Encrypts a string with AES-GCM/RSA-OAEP 110 | * @param pubKey - The RSA public key 111 | * @param text - The text to encrypt 112 | * @param label - The OAEP label 113 | * @constructor 114 | */ 115 | export async function HybridEncrypt(pubKey: CryptoKey, text: string, label: ArrayBuffer): Promise { 116 | const sessionKey = await crypto.subtle.generateKey({ name: 'AES-GCM', length: sessionKeyLength }, true, ['encrypt']); 117 | 118 | const rawSessionKey = await crypto.subtle.exportKey('raw', sessionKey); 119 | 120 | const rsaCipherText = await crypto.subtle.encrypt( 121 | { 122 | name: 'RSA-OAEP', 123 | label, 124 | }, 125 | pubKey, 126 | rawSessionKey 127 | ); 128 | 129 | const rsaCipherLength = numberToLEBuffer(rsaCipherText.byteLength); 130 | const result = await aesGcmEncrypt(sessionKey, text); 131 | 132 | const resultBuffer = new Uint8Array(rsaCipherLength.byteLength + rsaCipherText.byteLength + result.byteLength); 133 | 134 | resultBuffer.set(new Uint8Array(rsaCipherLength), 0); 135 | resultBuffer.set(new Uint8Array(rsaCipherText), rsaCipherLength.byteLength); 136 | resultBuffer.set(new Uint8Array(result), rsaCipherLength.byteLength + rsaCipherText.byteLength); 137 | return resultBuffer.buffer; 138 | } 139 | 140 | /** 141 | * Decrypts a string with AES-GCM/RSA-OAEP 142 | * @param privKey - The RSA private key 143 | * @param cipherText - The encrypted text 144 | * @param label - The OAEP label 145 | * @constructor 146 | */ 147 | export async function HybridDecrypt(privKey: CryptoKey, cipherText: ArrayBuffer, label: ArrayBuffer): Promise { 148 | const rsaLength = numberFromLEBuffer(cipherText.slice(0, 2)); 149 | const rsaCipherText = new Uint8Array(cipherText.slice(2, 2 + rsaLength)); 150 | const aesCipherText = new Uint8Array(cipherText.slice(2 + rsaLength)); 151 | const sessionKey = await crypto.subtle.decrypt( 152 | { 153 | name: 'RSA-OAEP', 154 | label, 155 | }, 156 | privKey, 157 | rsaCipherText 158 | ); 159 | 160 | const key = await crypto.subtle.importKey('raw', sessionKey, { name: 'AES-GCM', length: sessionKeyLength }, false, [ 161 | 'decrypt', 162 | ]); 163 | 164 | return aesGcmDecrypt(key, aesCipherText); 165 | } 166 | 167 | function uint8Str2Ab(str: string): ArrayBuffer { 168 | const buf = new ArrayBuffer(str.length); 169 | const bufView = new Uint8Array(buf); 170 | for (let i = 0, strLen = str.length; i < strLen; i += 1) { 171 | bufView[i] = str.charCodeAt(i); 172 | } 173 | return buf; 174 | } 175 | 176 | const atob = 177 | typeof window !== 'undefined' 178 | ? window.atob 179 | : function (str: string) { 180 | return Buffer.from(str, 'base64').toString('binary'); 181 | }; 182 | 183 | export async function pemPublicKeyToCryptoKey(pemContent: string): Promise { 184 | const data = pemContent 185 | .replace(/-*BEGIN PUBLIC KEY-*/, '') 186 | .replace(/-*END PUBLIC KEY-*/, '') 187 | .replace(/\n/g, ''); 188 | 189 | const keyBuffer = uint8Str2Ab(atob(data)); 190 | 191 | return crypto.subtle.importKey('spki', keyBuffer, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, ['encrypt']); 192 | } 193 | 194 | // from https://github.com/bitnami-labs/sealed-secrets/blob/4d699355c7f7f3fb9dbd4247cef9f11e4f14f26b/pkg/apis/sealed-secrets/v1alpha1/sealedsecret_expansion.go#L77 195 | export const getLabel = ({ scope, namespace, name }: GetLabelParams): string => { 196 | if (scope === 'cluster') { 197 | return ''; 198 | } 199 | if (scope === 'namespace') { 200 | return namespace; 201 | } 202 | return `${namespace}/${name}`; 203 | }; 204 | 205 | /** 206 | * Return public `CryptoKey` from pemKey 207 | * @constructor 208 | */ 209 | const getPublicKey = async (pemKey: string): Promise => { 210 | const cert = pki.certificateFromPem(pemKey); 211 | const publicKeyPem = pki.publicKeyToPem(cert.publicKey); 212 | const publicKey = await pemPublicKeyToCryptoKey(publicKeyPem); 213 | return publicKey; 214 | }; 215 | 216 | export const encryptFromPublicKey = async (args: EncryptParams) => 217 | Buffer.from(await HybridEncrypt(args.publicKey, args.value, args.label)).toString('base64'); 218 | 219 | /** 220 | * Encrypt a single value 221 | * @constructor 222 | */ 223 | export const encryptValue = async (args: EncryptValueParams): Promise => { 224 | const publicKey = await getPublicKey(args.pemKey); 225 | const label = Buffer.from(getLabel({ scope: args.scope, namespace: args.namespace, name: args.name })); 226 | const result = await encryptFromPublicKey({ publicKey, label, value: args.value }); 227 | return result; 228 | }; 229 | 230 | /** 231 | * Encrypt a bunch of values 232 | * @constructor 233 | */ 234 | export const encryptValues = async (args: EncryptValuesParams) => { 235 | const publicKey = await getPublicKey(args.pemKey); 236 | const label = Buffer.from(getLabel({ scope: args.scope, namespace: args.namespace, name: args.name })); 237 | const encryptedValues = ( 238 | await Promise.all( 239 | Object.keys(args.values).map(async (key) => ({ 240 | key, 241 | value: await encryptFromPublicKey({ publicKey, label, value: args.values[key] }), 242 | })) 243 | ) 244 | ).reduce((a, c) => ({ ...a, [c.key]: c.value }), {}); 245 | return encryptedValues; 246 | }; 247 | 248 | /** 249 | * Create a sealed-secret manifest and encrypt values 250 | * @constructor 251 | */ 252 | export const getSealedSecret = async (args: GetSealedSecretParams) => { 253 | const encryptedData = await encryptValues(args); 254 | 255 | const annotations = {} as Record; 256 | if (args.scope === 'cluster') { 257 | annotations['sealedsecrets.bitnami.com/cluster-wide'] = 'true'; 258 | } else if (args.scope === 'namespace') { 259 | annotations['sealedsecrets.bitnami.com/namespace-wide'] = 'true'; 260 | } 261 | 262 | const manifest = { 263 | apiVersion: 'bitnami.com/v1alpha1', 264 | kind: 'SealedSecret', 265 | metadata: { 266 | annotations, 267 | name: args.name, 268 | namespace: args.namespace, 269 | }, 270 | spec: { 271 | encryptedData, 272 | template: { 273 | metadata: { 274 | annotations, 275 | name: args.name, 276 | }, 277 | type: 'Opaque', 278 | }, 279 | }, 280 | }; 281 | 282 | return manifest; 283 | }; 284 | -------------------------------------------------------------------------------- /src/__tests__/index.test.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line import/no-unresolved 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import { 5 | HybridDecrypt, 6 | HybridEncrypt, 7 | pemPublicKeyToCryptoKey, 8 | encryptValue, 9 | encryptValues, 10 | getSealedSecret, 11 | getLabel, 12 | } from '../index'; 13 | 14 | describe('index', () => { 15 | describe('encrypt and decrypt', () => { 16 | it('works', async () => { 17 | const publicKey = fs.readFileSync(path.join(__dirname, '../../public.pem'), 'utf8'); 18 | const privateKey = fs.readFileSync(path.join(__dirname, '../../pk8.pem')); 19 | 20 | const pubKey = await pemPublicKeyToCryptoKey(publicKey); 21 | 22 | const privKey = await crypto.subtle.importKey('pkcs8', privateKey, { name: 'RSA-OAEP', hash: 'SHA-256' }, true, [ 23 | 'encrypt', 24 | 'decrypt', 25 | ]); 26 | 27 | const encryptedText = 'Bonjour le monde'; 28 | const label = Buffer.from('label'); 29 | 30 | const result = await HybridEncrypt(pubKey, encryptedText, label); 31 | 32 | const str = await HybridDecrypt(privKey, result, label); 33 | 34 | expect(str).toEqual(encryptedText); 35 | }); 36 | }); 37 | 38 | describe('import pem key', () => { 39 | it('works', async () => { 40 | const pemContent = `-----BEGIN PUBLIC KEY----- 41 | MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA3OcM/3pefoIZjQLEM6RS 42 | t47S0SA3KGIMnhh2hix/xQrmrYhN6oRXibdrmEPhhZhIN3vTf+sQ2PDYFDLy3VJQ 43 | H5eU6leR/ehF00rBhGkS9iuBCCHZhX5WrPq+3YeSGLZlfDU11thEi5Mjntz7nyvz 44 | oXQ/8XMYJWCzPiZ0dqWyqhbxXLJPixytxU1rdMUnqoHfuhvTyMVAGx7aCmCPUeNa 45 | 1pwl2O59M6fs1g/+oxA7GwoBSeue2cMJ9F1xpEKiLXWbDfhEyHAVSHanps3JLHe0 46 | UVGUIiRfIpjOUv8I/9aOeScADy6oOTnY+U/6UKuIEhvlBtT9HicQUKVAoJrVtUgI 47 | 0thr/ZJLsPzwDJiwGjdLh5zPE2PTjyyzBDY5p/SIb2K4Y+ZSn3+DVzQuJxxUg1AF 48 | X7uZDR62J/VpwgsP32RPNaeQ6OyHLG35oBKt07YO6dXql6R/TRPU1kmU89x1v9JZ 49 | hLiLDcY1/+vdgwXj/wglImJQHn+9OFqKPOI8CM+IxMH46J7BVWh227SBsqo705Vn 50 | GH0Jwi75wfj5arrXP/6fP1Yl137gMI/yt5WL4RlrdkzS6Pet1xv6agVW2cWFSOJu 51 | yM62dL4W4XJNaBmDfMl/LGDwVJ+f+V/2yfF89SrAHgn0lwLwowGdzH0A8MW7BFEV 52 | UAHog3BetRu+fy6v2QZu+IcCAwEAAQ== 53 | -----END PUBLIC KEY----- 54 | `; 55 | 56 | const key = await pemPublicKeyToCryptoKey(pemContent); 57 | expect(key).toBeDefined(); 58 | const encrypted = await HybridEncrypt(key, 'hello', Buffer.from('')); 59 | expect(encrypted).toBeDefined(); 60 | }); 61 | }); 62 | 63 | describe('encryptValue', () => { 64 | it('should encryptValue', async () => { 65 | const pemContent = `-----BEGIN CERTIFICATE----- 66 | MIIErjCCApagAwIBAgIRAOqAV9ZpCl1cwMunTHirqXwwDQYJKoZIhvcNAQELBQAw 67 | ADAeFw0yMDA1MjYwODQxMTBaFw0zMDA1MjQwODQxMTBaMAAwggIiMA0GCSqGSIb3 68 | DQEBAQUAA4ICDwAwggIKAoICAQDc5wz/el5+ghmNAsQzpFK3jtLRIDcoYgyeGHaG 69 | LH/FCuatiE3qhFeJt2uYQ+GFmEg3e9N/6xDY8NgUMvLdUlAfl5TqV5H96EXTSsGE 70 | aRL2K4EIIdmFflas+r7dh5IYtmV8NTXW2ESLkyOe3PufK/OhdD/xcxglYLM+JnR2 71 | pbKqFvFcsk+LHK3FTWt0xSeqgd+6G9PIxUAbHtoKYI9R41rWnCXY7n0zp+zWD/6j 72 | EDsbCgFJ657Zwwn0XXGkQqItdZsN+ETIcBVIdqemzcksd7RRUZQiJF8imM5S/wj/ 73 | 1o55JwAPLqg5Odj5T/pQq4gSG+UG1P0eJxBQpUCgmtW1SAjS2Gv9kkuw/PAMmLAa 74 | N0uHnM8TY9OPLLMENjmn9IhvYrhj5lKff4NXNC4nHFSDUAVfu5kNHrYn9WnCCw/f 75 | ZE81p5Do7IcsbfmgEq3Ttg7p1eqXpH9NE9TWSZTz3HW/0lmEuIsNxjX/692DBeP/ 76 | CCUiYlAef704Woo84jwIz4jEwfjonsFVaHbbtIGyqjvTlWcYfQnCLvnB+Plqutc/ 77 | /p8/ViXXfuAwj/K3lYvhGWt2TNLo963XG/pqBVbZxYVI4m7IzrZ0vhbhck1oGYN8 78 | yX8sYPBUn5/5X/bJ8Xz1KsAeCfSXAvCjAZ3MfQDwxbsEURVQAeiDcF61G75/Lq/Z 79 | Bm74hwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 80 | BgkqhkiG9w0BAQsFAAOCAgEAKLvQtDV5QVNUEcg3ywL03IwtNWmU8EqqASgckVAc 81 | dmeGnFwV7+7VnrIHI34xCZDhAVEnlMAb0oELgTDSstTx4p25rjqOLQfPcb0TsPko 82 | cpJHS00trLsLX4DYoZ1dLprgySGrC9jQ/FMYMK1oZ0M04gA/U9alNVNu2DWKosHu 83 | JGRYBbXVnse5rZi3hl4GV5Vq2ZR/3GHL9xgcjMcSLqHhXoSULLm5qEBUA5flV0BJ 84 | bY2FEfpm1NHSh5vOaA5t7lrW/XqiAuo8lJM2Ztg/dsX6Zxq7Memq8nqMRpoMFbdj 85 | i0jTxlPL1ssVHvvmWcLsdx9fHX7XaNGZ4ulA6BIL4DZQMPxvtFk9alqbc9WnjsqL 86 | /8QA3STXkTSzWrSsTUcabxlp5+MLHRf31iE1dlGVv7FVLjPy29T145eSzoNPM9sN 87 | 1+aBLnXDCj5HUwto8+iKJn7VTURty5KceUFSeURXM2IKYYTec8MElLX0PeXi+SvO 88 | W37ZCn0RMnljus5aTUPbRHNvJ3Ut/1WDLQu8X0wIm509F1A80Udg5FTxHasBizM2 89 | L+rpr2923MG1JSitDfvNj1rxx1FLdynP84SOEJDCtbB8YRcDUC4bHKrAcG4FBAFX 90 | UDzbbsoR58onjcZW3QH4mvV9K3+llwHSo8zfYyKRhF8pUUJHp7A/8DgMYQyYAv2z 91 | bag= 92 | -----END CERTIFICATE----- 93 | 94 | `; 95 | 96 | const encrypted = await encryptValue({ 97 | pemKey: pemContent, 98 | name: 'some-name', 99 | namespace: 'some-namespace', 100 | scope: 'strict', 101 | value: 'hello', 102 | }); 103 | 104 | expect(encrypted).toBeDefined(); 105 | }); 106 | }); 107 | 108 | describe('encryptValues', () => { 109 | it('should encryptValues', async () => { 110 | const pemContent = `-----BEGIN CERTIFICATE----- 111 | MIIErjCCApagAwIBAgIRAOqAV9ZpCl1cwMunTHirqXwwDQYJKoZIhvcNAQELBQAw 112 | ADAeFw0yMDA1MjYwODQxMTBaFw0zMDA1MjQwODQxMTBaMAAwggIiMA0GCSqGSIb3 113 | DQEBAQUAA4ICDwAwggIKAoICAQDc5wz/el5+ghmNAsQzpFK3jtLRIDcoYgyeGHaG 114 | LH/FCuatiE3qhFeJt2uYQ+GFmEg3e9N/6xDY8NgUMvLdUlAfl5TqV5H96EXTSsGE 115 | aRL2K4EIIdmFflas+r7dh5IYtmV8NTXW2ESLkyOe3PufK/OhdD/xcxglYLM+JnR2 116 | pbKqFvFcsk+LHK3FTWt0xSeqgd+6G9PIxUAbHtoKYI9R41rWnCXY7n0zp+zWD/6j 117 | EDsbCgFJ657Zwwn0XXGkQqItdZsN+ETIcBVIdqemzcksd7RRUZQiJF8imM5S/wj/ 118 | 1o55JwAPLqg5Odj5T/pQq4gSG+UG1P0eJxBQpUCgmtW1SAjS2Gv9kkuw/PAMmLAa 119 | N0uHnM8TY9OPLLMENjmn9IhvYrhj5lKff4NXNC4nHFSDUAVfu5kNHrYn9WnCCw/f 120 | ZE81p5Do7IcsbfmgEq3Ttg7p1eqXpH9NE9TWSZTz3HW/0lmEuIsNxjX/692DBeP/ 121 | CCUiYlAef704Woo84jwIz4jEwfjonsFVaHbbtIGyqjvTlWcYfQnCLvnB+Plqutc/ 122 | /p8/ViXXfuAwj/K3lYvhGWt2TNLo963XG/pqBVbZxYVI4m7IzrZ0vhbhck1oGYN8 123 | yX8sYPBUn5/5X/bJ8Xz1KsAeCfSXAvCjAZ3MfQDwxbsEURVQAeiDcF61G75/Lq/Z 124 | Bm74hwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 125 | BgkqhkiG9w0BAQsFAAOCAgEAKLvQtDV5QVNUEcg3ywL03IwtNWmU8EqqASgckVAc 126 | dmeGnFwV7+7VnrIHI34xCZDhAVEnlMAb0oELgTDSstTx4p25rjqOLQfPcb0TsPko 127 | cpJHS00trLsLX4DYoZ1dLprgySGrC9jQ/FMYMK1oZ0M04gA/U9alNVNu2DWKosHu 128 | JGRYBbXVnse5rZi3hl4GV5Vq2ZR/3GHL9xgcjMcSLqHhXoSULLm5qEBUA5flV0BJ 129 | bY2FEfpm1NHSh5vOaA5t7lrW/XqiAuo8lJM2Ztg/dsX6Zxq7Memq8nqMRpoMFbdj 130 | i0jTxlPL1ssVHvvmWcLsdx9fHX7XaNGZ4ulA6BIL4DZQMPxvtFk9alqbc9WnjsqL 131 | /8QA3STXkTSzWrSsTUcabxlp5+MLHRf31iE1dlGVv7FVLjPy29T145eSzoNPM9sN 132 | 1+aBLnXDCj5HUwto8+iKJn7VTURty5KceUFSeURXM2IKYYTec8MElLX0PeXi+SvO 133 | W37ZCn0RMnljus5aTUPbRHNvJ3Ut/1WDLQu8X0wIm509F1A80Udg5FTxHasBizM2 134 | L+rpr2923MG1JSitDfvNj1rxx1FLdynP84SOEJDCtbB8YRcDUC4bHKrAcG4FBAFX 135 | UDzbbsoR58onjcZW3QH4mvV9K3+llwHSo8zfYyKRhF8pUUJHp7A/8DgMYQyYAv2z 136 | bag= 137 | -----END CERTIFICATE----- 138 | 139 | `; 140 | 141 | const encrypted = await encryptValues({ 142 | pemKey: pemContent, 143 | name: 'some-name', 144 | namespace: 'some-namespace', 145 | scope: 'strict', 146 | values: { 147 | value1: 'hello', 148 | value2: 'world', 149 | }, 150 | }); 151 | 152 | // @ts-expect-error 153 | expect(encrypted.value1).toBeDefined(); 154 | // @ts-expect-error 155 | expect(encrypted.value2).toBeDefined(); 156 | }); 157 | }); 158 | 159 | describe('getLabel', () => { 160 | it("should set label to '' for scope=cluster", () => { 161 | expect(getLabel({ scope: 'cluster', namespace: 'namespace', name: 'name' })).toEqual(''); 162 | }); 163 | it("should set label to 'namespace' for scope=namespace", () => { 164 | expect(getLabel({ scope: 'namespace', namespace: 'namespace', name: 'name' })).toEqual('namespace'); 165 | }); 166 | it("should set label to 'namespace/name' for scope=strict", () => { 167 | expect(getLabel({ scope: 'strict', namespace: 'namespace', name: 'name' })).toEqual('namespace/name'); 168 | }); 169 | }); 170 | 171 | describe('getSealedSecret', () => { 172 | it('should getSealedSecret for scope=strict', async () => { 173 | const pemContent = `-----BEGIN CERTIFICATE----- 174 | MIIErjCCApagAwIBAgIRAOqAV9ZpCl1cwMunTHirqXwwDQYJKoZIhvcNAQELBQAw 175 | ADAeFw0yMDA1MjYwODQxMTBaFw0zMDA1MjQwODQxMTBaMAAwggIiMA0GCSqGSIb3 176 | DQEBAQUAA4ICDwAwggIKAoICAQDc5wz/el5+ghmNAsQzpFK3jtLRIDcoYgyeGHaG 177 | LH/FCuatiE3qhFeJt2uYQ+GFmEg3e9N/6xDY8NgUMvLdUlAfl5TqV5H96EXTSsGE 178 | aRL2K4EIIdmFflas+r7dh5IYtmV8NTXW2ESLkyOe3PufK/OhdD/xcxglYLM+JnR2 179 | pbKqFvFcsk+LHK3FTWt0xSeqgd+6G9PIxUAbHtoKYI9R41rWnCXY7n0zp+zWD/6j 180 | EDsbCgFJ657Zwwn0XXGkQqItdZsN+ETIcBVIdqemzcksd7RRUZQiJF8imM5S/wj/ 181 | 1o55JwAPLqg5Odj5T/pQq4gSG+UG1P0eJxBQpUCgmtW1SAjS2Gv9kkuw/PAMmLAa 182 | N0uHnM8TY9OPLLMENjmn9IhvYrhj5lKff4NXNC4nHFSDUAVfu5kNHrYn9WnCCw/f 183 | ZE81p5Do7IcsbfmgEq3Ttg7p1eqXpH9NE9TWSZTz3HW/0lmEuIsNxjX/692DBeP/ 184 | CCUiYlAef704Woo84jwIz4jEwfjonsFVaHbbtIGyqjvTlWcYfQnCLvnB+Plqutc/ 185 | /p8/ViXXfuAwj/K3lYvhGWt2TNLo963XG/pqBVbZxYVI4m7IzrZ0vhbhck1oGYN8 186 | yX8sYPBUn5/5X/bJ8Xz1KsAeCfSXAvCjAZ3MfQDwxbsEURVQAeiDcF61G75/Lq/Z 187 | Bm74hwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 188 | BgkqhkiG9w0BAQsFAAOCAgEAKLvQtDV5QVNUEcg3ywL03IwtNWmU8EqqASgckVAc 189 | dmeGnFwV7+7VnrIHI34xCZDhAVEnlMAb0oELgTDSstTx4p25rjqOLQfPcb0TsPko 190 | cpJHS00trLsLX4DYoZ1dLprgySGrC9jQ/FMYMK1oZ0M04gA/U9alNVNu2DWKosHu 191 | JGRYBbXVnse5rZi3hl4GV5Vq2ZR/3GHL9xgcjMcSLqHhXoSULLm5qEBUA5flV0BJ 192 | bY2FEfpm1NHSh5vOaA5t7lrW/XqiAuo8lJM2Ztg/dsX6Zxq7Memq8nqMRpoMFbdj 193 | i0jTxlPL1ssVHvvmWcLsdx9fHX7XaNGZ4ulA6BIL4DZQMPxvtFk9alqbc9WnjsqL 194 | /8QA3STXkTSzWrSsTUcabxlp5+MLHRf31iE1dlGVv7FVLjPy29T145eSzoNPM9sN 195 | 1+aBLnXDCj5HUwto8+iKJn7VTURty5KceUFSeURXM2IKYYTec8MElLX0PeXi+SvO 196 | W37ZCn0RMnljus5aTUPbRHNvJ3Ut/1WDLQu8X0wIm509F1A80Udg5FTxHasBizM2 197 | L+rpr2923MG1JSitDfvNj1rxx1FLdynP84SOEJDCtbB8YRcDUC4bHKrAcG4FBAFX 198 | UDzbbsoR58onjcZW3QH4mvV9K3+llwHSo8zfYyKRhF8pUUJHp7A/8DgMYQyYAv2z 199 | bag= 200 | -----END CERTIFICATE----- 201 | 202 | `; 203 | 204 | const secret = await getSealedSecret({ 205 | pemKey: pemContent, 206 | name: 'some-name', 207 | namespace: 'some-namespace', 208 | scope: 'strict', 209 | values: { 210 | value1: 'hello', 211 | value2: 'world', 212 | }, 213 | }); 214 | 215 | expect(secret).toMatchSnapshot({ 216 | spec: { 217 | encryptedData: { 218 | value1: expect.any(String), 219 | value2: expect.any(String), 220 | }, 221 | }, 222 | }); 223 | }); 224 | 225 | it('should getSealedSecret for scope=namespace', async () => { 226 | const pemContent = `-----BEGIN CERTIFICATE----- 227 | MIIErjCCApagAwIBAgIRAOqAV9ZpCl1cwMunTHirqXwwDQYJKoZIhvcNAQELBQAw 228 | ADAeFw0yMDA1MjYwODQxMTBaFw0zMDA1MjQwODQxMTBaMAAwggIiMA0GCSqGSIb3 229 | DQEBAQUAA4ICDwAwggIKAoICAQDc5wz/el5+ghmNAsQzpFK3jtLRIDcoYgyeGHaG 230 | LH/FCuatiE3qhFeJt2uYQ+GFmEg3e9N/6xDY8NgUMvLdUlAfl5TqV5H96EXTSsGE 231 | aRL2K4EIIdmFflas+r7dh5IYtmV8NTXW2ESLkyOe3PufK/OhdD/xcxglYLM+JnR2 232 | pbKqFvFcsk+LHK3FTWt0xSeqgd+6G9PIxUAbHtoKYI9R41rWnCXY7n0zp+zWD/6j 233 | EDsbCgFJ657Zwwn0XXGkQqItdZsN+ETIcBVIdqemzcksd7RRUZQiJF8imM5S/wj/ 234 | 1o55JwAPLqg5Odj5T/pQq4gSG+UG1P0eJxBQpUCgmtW1SAjS2Gv9kkuw/PAMmLAa 235 | N0uHnM8TY9OPLLMENjmn9IhvYrhj5lKff4NXNC4nHFSDUAVfu5kNHrYn9WnCCw/f 236 | ZE81p5Do7IcsbfmgEq3Ttg7p1eqXpH9NE9TWSZTz3HW/0lmEuIsNxjX/692DBeP/ 237 | CCUiYlAef704Woo84jwIz4jEwfjonsFVaHbbtIGyqjvTlWcYfQnCLvnB+Plqutc/ 238 | /p8/ViXXfuAwj/K3lYvhGWt2TNLo963XG/pqBVbZxYVI4m7IzrZ0vhbhck1oGYN8 239 | yX8sYPBUn5/5X/bJ8Xz1KsAeCfSXAvCjAZ3MfQDwxbsEURVQAeiDcF61G75/Lq/Z 240 | Bm74hwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 241 | BgkqhkiG9w0BAQsFAAOCAgEAKLvQtDV5QVNUEcg3ywL03IwtNWmU8EqqASgckVAc 242 | dmeGnFwV7+7VnrIHI34xCZDhAVEnlMAb0oELgTDSstTx4p25rjqOLQfPcb0TsPko 243 | cpJHS00trLsLX4DYoZ1dLprgySGrC9jQ/FMYMK1oZ0M04gA/U9alNVNu2DWKosHu 244 | JGRYBbXVnse5rZi3hl4GV5Vq2ZR/3GHL9xgcjMcSLqHhXoSULLm5qEBUA5flV0BJ 245 | bY2FEfpm1NHSh5vOaA5t7lrW/XqiAuo8lJM2Ztg/dsX6Zxq7Memq8nqMRpoMFbdj 246 | i0jTxlPL1ssVHvvmWcLsdx9fHX7XaNGZ4ulA6BIL4DZQMPxvtFk9alqbc9WnjsqL 247 | /8QA3STXkTSzWrSsTUcabxlp5+MLHRf31iE1dlGVv7FVLjPy29T145eSzoNPM9sN 248 | 1+aBLnXDCj5HUwto8+iKJn7VTURty5KceUFSeURXM2IKYYTec8MElLX0PeXi+SvO 249 | W37ZCn0RMnljus5aTUPbRHNvJ3Ut/1WDLQu8X0wIm509F1A80Udg5FTxHasBizM2 250 | L+rpr2923MG1JSitDfvNj1rxx1FLdynP84SOEJDCtbB8YRcDUC4bHKrAcG4FBAFX 251 | UDzbbsoR58onjcZW3QH4mvV9K3+llwHSo8zfYyKRhF8pUUJHp7A/8DgMYQyYAv2z 252 | bag= 253 | -----END CERTIFICATE----- 254 | 255 | `; 256 | 257 | const secret = await getSealedSecret({ 258 | pemKey: pemContent, 259 | name: 'some-name', 260 | namespace: 'some-namespace', 261 | scope: 'namespace', 262 | values: { 263 | value1: 'hello', 264 | value2: 'world', 265 | }, 266 | }); 267 | 268 | expect(secret).toMatchSnapshot({ 269 | spec: { 270 | encryptedData: { 271 | value1: expect.any(String), 272 | value2: expect.any(String), 273 | }, 274 | }, 275 | }); 276 | }); 277 | 278 | it('should getSealedSecret for scope=cluster', async () => { 279 | const pemContent = `-----BEGIN CERTIFICATE----- 280 | MIIErjCCApagAwIBAgIRAOqAV9ZpCl1cwMunTHirqXwwDQYJKoZIhvcNAQELBQAw 281 | ADAeFw0yMDA1MjYwODQxMTBaFw0zMDA1MjQwODQxMTBaMAAwggIiMA0GCSqGSIb3 282 | DQEBAQUAA4ICDwAwggIKAoICAQDc5wz/el5+ghmNAsQzpFK3jtLRIDcoYgyeGHaG 283 | LH/FCuatiE3qhFeJt2uYQ+GFmEg3e9N/6xDY8NgUMvLdUlAfl5TqV5H96EXTSsGE 284 | aRL2K4EIIdmFflas+r7dh5IYtmV8NTXW2ESLkyOe3PufK/OhdD/xcxglYLM+JnR2 285 | pbKqFvFcsk+LHK3FTWt0xSeqgd+6G9PIxUAbHtoKYI9R41rWnCXY7n0zp+zWD/6j 286 | EDsbCgFJ657Zwwn0XXGkQqItdZsN+ETIcBVIdqemzcksd7RRUZQiJF8imM5S/wj/ 287 | 1o55JwAPLqg5Odj5T/pQq4gSG+UG1P0eJxBQpUCgmtW1SAjS2Gv9kkuw/PAMmLAa 288 | N0uHnM8TY9OPLLMENjmn9IhvYrhj5lKff4NXNC4nHFSDUAVfu5kNHrYn9WnCCw/f 289 | ZE81p5Do7IcsbfmgEq3Ttg7p1eqXpH9NE9TWSZTz3HW/0lmEuIsNxjX/692DBeP/ 290 | CCUiYlAef704Woo84jwIz4jEwfjonsFVaHbbtIGyqjvTlWcYfQnCLvnB+Plqutc/ 291 | /p8/ViXXfuAwj/K3lYvhGWt2TNLo963XG/pqBVbZxYVI4m7IzrZ0vhbhck1oGYN8 292 | yX8sYPBUn5/5X/bJ8Xz1KsAeCfSXAvCjAZ3MfQDwxbsEURVQAeiDcF61G75/Lq/Z 293 | Bm74hwIDAQABoyMwITAOBgNVHQ8BAf8EBAMCAAEwDwYDVR0TAQH/BAUwAwEB/zAN 294 | BgkqhkiG9w0BAQsFAAOCAgEAKLvQtDV5QVNUEcg3ywL03IwtNWmU8EqqASgckVAc 295 | dmeGnFwV7+7VnrIHI34xCZDhAVEnlMAb0oELgTDSstTx4p25rjqOLQfPcb0TsPko 296 | cpJHS00trLsLX4DYoZ1dLprgySGrC9jQ/FMYMK1oZ0M04gA/U9alNVNu2DWKosHu 297 | JGRYBbXVnse5rZi3hl4GV5Vq2ZR/3GHL9xgcjMcSLqHhXoSULLm5qEBUA5flV0BJ 298 | bY2FEfpm1NHSh5vOaA5t7lrW/XqiAuo8lJM2Ztg/dsX6Zxq7Memq8nqMRpoMFbdj 299 | i0jTxlPL1ssVHvvmWcLsdx9fHX7XaNGZ4ulA6BIL4DZQMPxvtFk9alqbc9WnjsqL 300 | /8QA3STXkTSzWrSsTUcabxlp5+MLHRf31iE1dlGVv7FVLjPy29T145eSzoNPM9sN 301 | 1+aBLnXDCj5HUwto8+iKJn7VTURty5KceUFSeURXM2IKYYTec8MElLX0PeXi+SvO 302 | W37ZCn0RMnljus5aTUPbRHNvJ3Ut/1WDLQu8X0wIm509F1A80Udg5FTxHasBizM2 303 | L+rpr2923MG1JSitDfvNj1rxx1FLdynP84SOEJDCtbB8YRcDUC4bHKrAcG4FBAFX 304 | UDzbbsoR58onjcZW3QH4mvV9K3+llwHSo8zfYyKRhF8pUUJHp7A/8DgMYQyYAv2z 305 | bag= 306 | -----END CERTIFICATE----- 307 | 308 | `; 309 | 310 | const secret = await getSealedSecret({ 311 | pemKey: pemContent, 312 | name: 'some-name', 313 | namespace: 'some-namespace', 314 | scope: 'cluster', 315 | values: { 316 | value1: 'hello', 317 | value2: 'world', 318 | }, 319 | }); 320 | 321 | expect(secret).toMatchSnapshot({ 322 | spec: { 323 | encryptedData: { 324 | value1: expect.any(String), 325 | value2: expect.any(String), 326 | }, 327 | }, 328 | }); 329 | }); 330 | }); 331 | }); 332 | --------------------------------------------------------------------------------