├── .dockerignore ├── .eslintignore ├── .eslintrc ├── .gitattributes ├── .github ├── CODEOWNERS ├── renovate.json └── workflows │ ├── code-review.yml │ ├── codeql-analysis.yml │ └── nodejs.yml ├── .gitignore ├── .nvmrc ├── .nycrc ├── .pre-commit-config.yaml ├── .prettierrc ├── .releaserc ├── .snyk ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── SECURITY.md ├── docker-compose.yml ├── package-lock.json ├── package.json ├── scripts └── version.sh ├── sonar-project.properties ├── src ├── config │ ├── config.json │ └── postman.environment.json ├── controllers │ └── secretController.js ├── healthcheck.js ├── index.js ├── lib │ ├── crypto.js │ ├── db.js │ └── logger.js ├── models │ └── secret.js ├── routes │ └── index.js └── swagger │ ├── options.js │ ├── specification.js │ └── specification.yaml └── test ├── config.spec.js ├── functional ├── healthcheck.spec.js ├── index.spec.js ├── secret.spec.js └── swagger.spec.js ├── lib.crypto.spec.js ├── lib.db.spec.js ├── lib.logger.spec.js ├── routes.index.spec.js └── testData.json /.dockerignore: -------------------------------------------------------------------------------- 1 | # https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Don't send the following files to the docker daemon as the build context. 3 | 4 | .git 5 | .gitattributes 6 | .travis.yml 7 | README.md 8 | docker-compose.yml 9 | **/node_modules 10 | **/npm-debug.log 11 | 12 | # Ignore .DS_Store Mac file 13 | .DS_Store 14 | # SonarQube Lint 15 | .sonarlint 16 | # MongoDB volume folder 17 | data 18 | # Test coverage 19 | coverage 20 | .nyc_output 21 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "airbnb", 3 | "env": { 4 | "mocha": true 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # https://help.github.com/articles/dealing-with-line-endings/ 2 | 3 | # Set the default behavior, in case people don't have core.autocrlf set. 4 | * text=auto 5 | 6 | Dockerfile text 7 | *.yml text 8 | *.md text 9 | .dockerignore text 10 | 11 | # Shell scripts should always use LF line endings, otherwise the Docker base image might cause problems 12 | *.sh text eol=lf 13 | *.js text eol=lf 14 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | @timoa 2 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": ["github>renovatebot/.github"], 4 | "platform": "github", 5 | "platformAutomerge": true, 6 | "branchPrefix": "fix/deps/", 7 | "addLabels": ["dependencies", "security"], 8 | "assignees": ["timoa"], 9 | "packageRules": [ 10 | { 11 | "description": "Automerge renovate minor and patch updates", 12 | "matchPackageNames": ["renovate/renovate"], 13 | "matchUpdateTypes": ["minor", "patch"], 14 | "automerge": true, 15 | "branchTopic": "{{{depNameSanitized}}}-{{{currentValue}}}" 16 | }, 17 | { 18 | "description": "Allow updates after 15 days (exclude renovate)", 19 | "excludePackageNames": ["renovate/renovate"], 20 | "separateMinorPatch": true, 21 | "minimumReleaseAge": "15 days" 22 | } 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.github/workflows/code-review.yml: -------------------------------------------------------------------------------- 1 | name: Code Review 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | 7 | # -- ESLINT ----------------------------------------------------------------- 8 | eslint: 9 | name: ESLint 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - name: Harden GitHub Actions Runner 14 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 15 | with: 16 | egress-policy: block 17 | allowed-endpoints: > 18 | api.github.com:443 19 | github.com:443 20 | objects.githubusercontent.com:443 21 | raw.githubusercontent.com:443 22 | registry.npmjs.org:443 23 | snyk.io:443 24 | 25 | - name: Checkout 26 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 27 | 28 | - name: Run ESLint 29 | uses: reviewdog/action-eslint@f2ee6727e05e6f0e46ea1d06a16f6685d3d7fb37 # v1.19.2 30 | env: 31 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | # -- DOCKER ----------------------------------------------------------------- 34 | hadolint: 35 | name: Hadolint 36 | runs-on: ubuntu-latest 37 | 38 | steps: 39 | - name: Harden GitHub Actions Runner 40 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 41 | with: 42 | egress-policy: block 43 | allowed-endpoints: > 44 | api.github.com:443 45 | github.com:443 46 | objects.githubusercontent.com:443 47 | raw.githubusercontent.com:443 48 | 49 | - name: Checkout 50 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 51 | 52 | - name: Run hadolint 53 | uses: reviewdog/action-hadolint@7bd0800b7ce35c6d644cde762174e69f18896973 # v1.35.0 54 | env: 55 | REVIEWDOG_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} 56 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL analysis" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | schedule: 9 | # ┌───────────── minute (0 - 59) 10 | # │ ┌───────────── hour (0 - 23) 11 | # │ │ ┌───────────── day of the month (1 - 31) 12 | # │ │ │ ┌───────────── month (1 - 12 or JAN-DEC) 13 | # │ │ │ │ ┌───────────── day of the week (0 - 6 or SUN-SAT) 14 | # │ │ │ │ │ 15 | # │ │ │ │ │ 16 | # │ │ │ │ │ 17 | # * * * * * 18 | - cron: '30 1 * * 0' 19 | 20 | jobs: 21 | CodeQL-Build: 22 | # CodeQL runs on ubuntu-latest, windows-latest, and macos-latest 23 | runs-on: ubuntu-latest 24 | 25 | permissions: 26 | # required for all workflows 27 | security-events: write 28 | 29 | # only required for workflows in private repositories 30 | actions: read 31 | contents: read 32 | 33 | steps: 34 | - name: Harden GitHub Actions Runner 35 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 36 | with: 37 | egress-policy: block 38 | allowed-endpoints: > 39 | api.github.com:443 40 | github.com:443 41 | objects.githubusercontent.com:443 42 | 43 | - name: Checkout repository 44 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 49 | # Override language selection by uncommenting this and choosing your languages 50 | # with: 51 | # languages: go, javascript, csharp, python, cpp, java 52 | 53 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 54 | # If this step fails, then you should remove it and run the build manually (see below). 55 | - name: Autobuild 56 | uses: github/codeql-action/autobuild@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 57 | 58 | # ℹ️ Command-line programs to run using the OS shell. 59 | # 📚 https://git.io/JvXDl 60 | 61 | # ✏️ If the Autobuild fails above, remove it and uncomment the following 62 | # three lines and modify them (or add more) to build your code if your 63 | # project uses a compiled language 64 | 65 | #- run: | 66 | # make bootstrap 67 | # make release 68 | 69 | - name: Perform CodeQL Analysis 70 | uses: github/codeql-action/analyze@ddccb873888234080b77e9bc2d4764d5ccaaccf9 # v2.21.9 71 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push] 4 | 5 | jobs: 6 | 7 | # -- TESTS ------------------------------------------------------------------ 8 | tests: 9 | name: Tests 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | node: ['18'] 15 | mongodb: ['5.0'] 16 | 17 | steps: 18 | - name: Harden GitHub Actions Runner 19 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 20 | with: 21 | egress-policy: block 22 | allowed-endpoints: > 23 | api.github.com:443 24 | auth.docker.io:443 25 | github.com:443 26 | objects.githubusercontent.com:443 27 | pipelines.actions.githubusercontent.com:443 28 | production.cloudflare.docker.com:443 29 | registry-1.docker.io:443 30 | registry.npmjs.org:443 31 | snyk.io:443 32 | docker.io:443 33 | auth.docker.io:443 34 | production.cloudflare.docker.com:443 35 | 36 | - name: Checkout 37 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 38 | 39 | - name: Setup Node.js ${{ matrix.node }} 40 | uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 41 | with: 42 | node-version: ${{ matrix.node }} 43 | check-latest: true 44 | 45 | - name: Install dependencies 46 | run: npm install 47 | 48 | - name: Start MongoDB 49 | uses: supercharge/mongodb-github-action@e815fd8a9dfede09fd6e6c144f2c9f4875e933df # tag=1.7.0 50 | with: 51 | mongodb-version: ${{ matrix.mongodb }} 52 | mongodb-db: encryptionAPI 53 | 54 | - name: Run Unit-Tests + Code Coverage 55 | run: npm run test:coverage 56 | 57 | - name: Save Code Coverage 58 | uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 59 | with: 60 | name: code-coverage 61 | path: coverage 62 | 63 | # -- SONARCLOUD ------------------------------------------------------------- 64 | code-quality: 65 | name: Code Quality 66 | runs-on: ubuntu-latest 67 | needs: tests 68 | 69 | steps: 70 | - name: Harden GitHub Actions Runner 71 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 72 | with: 73 | egress-policy: block 74 | allowed-endpoints: > 75 | api.github.com:443 76 | github.com:443 77 | pipelines.actions.githubusercontent.com:443 78 | sonarcloud.io:443 79 | scanner.sonarcloud.io:443 80 | 81 | - name: Checkout 82 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 83 | 84 | - name: Download Code Coverage 85 | uses: actions/download-artifact@9bc31d5ccc31df68ecc42ccf4149144866c47d8a # v3.0.2 86 | with: 87 | name: code-coverage 88 | path: coverage 89 | 90 | - name: Get App Version 91 | run: ./scripts/version.sh 92 | 93 | - name: SonarCloud Scan 94 | uses: sonarsource/sonarcloud-github-action@master 95 | env: 96 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 97 | SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} 98 | 99 | # -- SAST SCAN -------------------------------------------------------------- 100 | code-security: 101 | name: Code Security 102 | runs-on: ubuntu-latest 103 | needs: tests 104 | # Skip any PR created by dependabot to avoid permission issues 105 | if: (github.actor != 'dependabot[bot]') 106 | 107 | steps: 108 | - name: Harden GitHub Actions Runner 109 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 110 | with: 111 | egress-policy: block 112 | allowed-endpoints: > 113 | github.com:443 114 | api.github.com:443 115 | pipelines.actions.githubusercontent.com:443 116 | registry.npmjs.org:443 117 | registry-1.docker.io:443 118 | osv-vulnerabilities.storage.googleapis.com:443 119 | nvd.nist.gov:443 120 | pypi.org:443 121 | location.services.mozilla.com:443 122 | docker.io:443 123 | auth.docker.io:443 124 | production.cloudflare.docker.com:443 125 | 126 | - name: Checkout 127 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 128 | 129 | - name: Perform Scan 130 | uses: ShiftLeftSecurity/scan-action@master 131 | env: 132 | WORKSPACE: https://github.com/${{ github.repository }}/blob/${{ github.sha }} 133 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 134 | SCAN_ANNOTATE_PR: true 135 | 136 | - name: Save the SCAN reports 137 | uses: actions/upload-artifact@a8a3f3ad30e3422c9c7b888a15615d19a852ae32 # v3.1.3 138 | with: 139 | name: sast-reports 140 | path: reports 141 | 142 | # -- API TESTS -------------------------------------------------------------- 143 | api-tests: 144 | name: API Tests 145 | runs-on: ubuntu-latest 146 | needs: tests 147 | 148 | strategy: 149 | matrix: 150 | node: ['18'] 151 | mongodb: ['5.0'] 152 | 153 | steps: 154 | - name: Harden GitHub Actions Runner 155 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 156 | with: 157 | egress-policy: audit 158 | 159 | - name: Checkout 160 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 161 | 162 | - name: Setup Node.js ${{ matrix.node }} 163 | uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 164 | with: 165 | node-version: ${{ matrix.node }} 166 | check-latest: true 167 | 168 | - name: Install dependencies 169 | run: npm install 170 | 171 | - name: Install Postman Newman 172 | run: npm install newman 173 | 174 | - name: Start MongoDB 175 | uses: supercharge/mongodb-github-action@e815fd8a9dfede09fd6e6c144f2c9f4875e933df # tag=1.7.0 176 | with: 177 | mongodb-version: ${{ matrix.mongodb }} 178 | mongodb-db: encryptionAPI 179 | 180 | - name: Start the app 181 | run: npm start > /dev/null & 182 | 183 | - name: Run Postman Newman 184 | run: node_modules/.bin/newman run https://api.getpostman.com/collections/${{ secrets.POSTMAN_COLLECTION }}?apikey=${{ secrets.POSTMAN_API_TOKEN }} -e https://api.getpostman.com/environments/${{ secrets.POSTMAN_ENVIRONMENT }}?apikey=${{ secrets.POSTMAN_API_TOKEN }} 185 | 186 | # -- ZAP Scan --------------------------------------------------------------- 187 | api-security: 188 | name: API Security 189 | runs-on: ubuntu-latest 190 | needs: tests 191 | # Skip any PR created by dependabot to avoid permission issues 192 | if: (github.actor != 'dependabot[bot]') 193 | 194 | strategy: 195 | matrix: 196 | node: ['18'] 197 | mongodb: ['5.0'] 198 | 199 | steps: 200 | - name: Harden GitHub Actions Runner 201 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 202 | with: 203 | egress-policy: block 204 | allowed-endpoints: > 205 | api.github.com:443 206 | auth.docker.io:443 207 | bit.ly:443 208 | cfu.zaproxy.org:443 209 | content-signature-2.cdn.mozilla.net:443 210 | docker.io:443 211 | firefox.settings.services.mozilla.com:443 212 | github.com:443 213 | location.services.mozilla.com:443 214 | news.zaproxy.org:443 215 | objects.githubusercontent.com:443 216 | pipelines.actions.githubusercontent.com:443 217 | production.cloudflare.docker.com:443 218 | raw.githubusercontent.com:443 219 | registry-1.docker.io:443 220 | registry.npmjs.org:443 221 | shavar.services.mozilla.com:443 222 | snyk.io:443 223 | tel.zaproxy.org:443 224 | tracking-protection.cdn.mozilla.net:443 225 | 226 | - name: Checkout 227 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 228 | 229 | - name: Setup Node.js ${{ matrix.node }} 230 | uses: actions/setup-node@5e21ff4d9bc1a8cf6de233a3057d20ec6b3fb69d # v3.8.1 231 | with: 232 | node-version: ${{ matrix.node }} 233 | check-latest: true 234 | 235 | - name: Install dependencies 236 | run: npm install 237 | 238 | - name: Start MongoDB 239 | uses: supercharge/mongodb-github-action@e815fd8a9dfede09fd6e6c144f2c9f4875e933df # tag=1.7.0 240 | with: 241 | mongodb-version: ${{ matrix.mongodb }} 242 | mongodb-db: encryptionAPI 243 | 244 | - name: Start the app 245 | run: npm start > /dev/null & 246 | 247 | - name: Run ZAP API Scan 248 | uses: zaproxy/action-api-scan@d7eab41e224d7427459ca0a3b7523ba7b98f3ccc # v0.3.1 249 | with: 250 | target: http://localhost:3000/swagger/json 251 | format: openapi 252 | 253 | # -- PRE-RELEASE ------------------------------------------------------------ 254 | pre-release: 255 | name: Prepare Release 256 | runs-on: ubuntu-latest 257 | needs: 258 | - code-quality 259 | - code-security 260 | - api-security 261 | - api-tests 262 | if: github.ref == 'refs/heads/master' 263 | 264 | steps: 265 | - name: Harden GitHub Actions Runner 266 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 267 | with: 268 | egress-policy: audit 269 | 270 | - name: Checkout 271 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 272 | 273 | - name: Semantic Release 274 | uses: cycjimmy/semantic-release-action@8e58d20d0f6c8773181f43eb74d6a05e3099571d # v3.4.2 275 | env: 276 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 277 | 278 | # -- BUILD ------------------------------------------------------------------ 279 | build: 280 | name: Build & Release 281 | runs-on: ubuntu-latest 282 | needs: pre-release 283 | if: github.ref == 'refs/heads/master' 284 | 285 | steps: 286 | - name: Harden GitHub Actions Runner 287 | uses: step-security/harden-runner@2579b52abd08b0a4c3ad0e4476ddb0f2036e3b67 288 | with: 289 | egress-policy: audit 290 | 291 | - name: Checkout 292 | uses: actions/checkout@c85c95e3d7251135ab7dc9ce3241c5835cc595a9 # v3.5.3 293 | 294 | - name: Docker meta 295 | id: meta 296 | uses: docker/metadata-action@818d4b7b91585d195f67373fd9cb0332e31a7175 # v4.6.0 297 | with: 298 | images: ${{ github.repository }} 299 | tags: | 300 | type=schedule 301 | type=ref,event=branch 302 | type=ref,event=pr 303 | type=semver,pattern={{version}} 304 | type=semver,pattern={{major}}.{{minor}} 305 | type=semver,pattern={{major}} 306 | type=sha 307 | type=raw,value=latest 308 | 309 | - name: Set up QEMU 310 | uses: docker/setup-qemu-action@2b82ce82d56a2a04d2637cd93a637ae1b359c0a7 # v2.2.0 311 | 312 | - name: Set up Docker Buildx 313 | uses: docker/setup-buildx-action@4c0219f9ac95b02789c1075625400b2acbff50b1 # v2.9.1 314 | 315 | - name: Login to DockerHub 316 | uses: docker/login-action@465a07811f14bebb1938fbed4728c6a1ff8901fc # v2.2.0 317 | with: 318 | username: ${{ secrets.DOCKER_USERNAME }} 319 | password: ${{ secrets.DOCKER_PASSWORD }} 320 | 321 | - name: Build and push 322 | uses: docker/build-push-action@2eb1c1961a95fc15694676618e422e8ba1d63825 # v4.1.1 323 | with: 324 | context: . 325 | push: true 326 | tags: ${{ steps.meta.outputs.tags }} 327 | labels: ${{ steps.meta.outputs.labels }} 328 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (https://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # TypeScript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | # next.js build output 61 | .next 62 | 63 | # Mongo DB data volume 64 | data 65 | 66 | # SonarQube 67 | .sonarlint 68 | .scannerwork 69 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 2 | -------------------------------------------------------------------------------- /.nycrc: -------------------------------------------------------------------------------- 1 | { 2 | "check-coverage": true, 3 | "lines": 0, 4 | "statements": 0, 5 | "functions": 0, 6 | "branches": 0, 7 | "reporter": [ 8 | "lcov", 9 | "text-summary" 10 | ], 11 | "include": [ 12 | "src/*.js", 13 | "src/**/*.js" 14 | ], 15 | "exclude": [ 16 | "test/*.spec.js", 17 | "coverage/**", 18 | "src/swagger/*.js", 19 | "data/**/*" 20 | ], 21 | "all": true 22 | } 23 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | 3 | # Default 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v4.1.0 6 | hooks: 7 | - id: check-yaml ### Control YAML format 8 | - id: check-json ### Control JSON format 9 | - id: end-of-file-fixer ### Fix end of file with one line 10 | - id: trailing-whitespace ### Remove end of line spaces 11 | - id: check-added-large-files ### Check files size to add only 500ko max 12 | - id: check-merge-conflict ### Check if there is already merge conflict(s) 13 | - id: detect-private-key ### Detect private keys 14 | 15 | # ESLint 16 | - repo: https://github.com/pre-commit/mirrors-eslint 17 | rev: v8.10.0 18 | hooks: 19 | - id: eslint 20 | additional_dependencies: 21 | - eslint-config-airbnb 22 | - eslint-config-prettier 23 | 24 | # Conventional Commit 25 | - repo: https://github.com/compilerla/conventional-pre-commit 26 | rev: v1.2.0 27 | hooks: 28 | - id: conventional-pre-commit 29 | stages: [commit-msg] 30 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "all", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": true, 10 | "fluid": false, 11 | "arrowParens": "always" 12 | } 13 | -------------------------------------------------------------------------------- /.releaserc: -------------------------------------------------------------------------------- 1 | { 2 | "repositoryUrl": "https://github.com/timoa/nodejs-encryption-api-example.git", 3 | "branches": [ 4 | "master", 5 | "develop" 6 | ], 7 | "tagFormat": "v${version}", 8 | "plugins": [ 9 | [ 10 | "@semantic-release/commit-analyzer", 11 | { 12 | "preset": "angular", 13 | "releaseRules": [ 14 | { 15 | "type": "docs", 16 | "release": "patch" 17 | }, 18 | { 19 | "type": "refactor", 20 | "release": "patch" 21 | }, 22 | { 23 | "type": "test", 24 | "release": "patch" 25 | }, 26 | { 27 | "type": "style", 28 | "release": "patch" 29 | } 30 | ], 31 | "parserOpts": { 32 | "noteKeywords": [ 33 | "BREAKING CHANGE", 34 | "BREAKING CHANGES", 35 | "BREAKING" 36 | ] 37 | } 38 | } 39 | ], 40 | [ 41 | "@semantic-release/release-notes-generator", 42 | { 43 | "preset": "angular", 44 | "parserOpts": { 45 | "noteKeywords": [ 46 | "BREAKING CHANGE", 47 | "BREAKING CHANGES", 48 | "BREAKING" 49 | ] 50 | } 51 | } 52 | ], 53 | [ 54 | "@semantic-release/changelog", 55 | { 56 | "changelogFile": "CHANGELOG.md" 57 | } 58 | ], 59 | [ 60 | "@semantic-release/npm" 61 | ], 62 | [ 63 | "@semantic-release/git", 64 | { 65 | "assets": [ 66 | "CHANGELOG.md", 67 | "README.md", 68 | "package.json", 69 | "package-lock.json" 70 | ] 71 | } 72 | ], 73 | [ 74 | "@semantic-release/github", { 75 | "assignees": "timoa" 76 | } 77 | ] 78 | ] 79 | } 80 | -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.16.0 3 | ignore: {} 4 | # patches apply the minimum changes required to fix a vulnerability 5 | patch: 6 | SNYK-JS-LODASH-567746: 7 | - lodash: 8 | patched: '2020-05-01T07:54:09.075Z' 9 | - winston > async > lodash: 10 | patched: '2020-05-01T07:54:09.075Z' 11 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## [1.2.1](https://github.com/timoa/nodejs-encryption-api-example/compare/v1.2.0...v1.2.1) (2022-06-19) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * **cicd:** add different names for the SAST and ZAP reports ([49be62f](https://github.com/timoa/nodejs-encryption-api-example/commit/49be62fc3e86167439eb1c0f6bbabd24456ae1c7)) 7 | * **cicd:** add wait for the app to start ([d1613bb](https://github.com/timoa/nodejs-encryption-api-example/commit/d1613bb1c4430c6af3e1c9675bd0667349af0aaf)) 8 | * **cicd:** run the app in the background ([345aa34](https://github.com/timoa/nodejs-encryption-api-example/commit/345aa34f95bfdfe78d0c53730a6dfbe517c6aa3f)) 9 | * **cicd:** unblocked domains from Harden GitHub Actions ([8152e3d](https://github.com/timoa/nodejs-encryption-api-example/commit/8152e3d44194591ecd26e83e33e7544e92bb688e)) 10 | * **deps:** update dependency @fastify/helmet to v8.0.1 ([3a397cd](https://github.com/timoa/nodejs-encryption-api-example/commit/3a397cd7cbe87e64c331086c40cd38855c5db9ef)) 11 | * **deps:** update dependency @fastify/helmet to v8.1.0 ([074bcb7](https://github.com/timoa/nodejs-encryption-api-example/commit/074bcb7609548c653d2fcdcc6057eba4ded0c7d0)) 12 | * **deps:** update dependency @snyk/protect to v1.915.0 ([013fc04](https://github.com/timoa/nodejs-encryption-api-example/commit/013fc04ab7c6ffd34fc9467159fb40a172910b19)) 13 | * **deps:** update dependency @snyk/protect to v1.917.0 ([556d9db](https://github.com/timoa/nodejs-encryption-api-example/commit/556d9db3c869a1e025eaefc8d15bccf4b06a0b81)) 14 | * **deps:** update dependency @snyk/protect to v1.918.0 ([653ad31](https://github.com/timoa/nodejs-encryption-api-example/commit/653ad311a9312a40cda110cf4b5bead7761eaf27)) 15 | * **deps:** update dependency @snyk/protect to v1.919.0 ([429764a](https://github.com/timoa/nodejs-encryption-api-example/commit/429764a849994e7fce4336b7284c7984fb14f7da)) 16 | * **deps:** update dependency @snyk/protect to v1.921.0 ([b98e794](https://github.com/timoa/nodejs-encryption-api-example/commit/b98e7942c82e08a4f46d566009a3c1e50aac5c6f)) 17 | * **deps:** update dependency @snyk/protect to v1.922.0 ([10d64fc](https://github.com/timoa/nodejs-encryption-api-example/commit/10d64fc3e0cd82264d4071d5fc5cc02fd7d47f04)) 18 | * **deps:** update dependency @snyk/protect to v1.924.0 ([343b91a](https://github.com/timoa/nodejs-encryption-api-example/commit/343b91a154396911074e8610abff1666d74f6a20)) 19 | * **deps:** update dependency @snyk/protect to v1.925.0 ([43a35c3](https://github.com/timoa/nodejs-encryption-api-example/commit/43a35c364583d530b95a68c3a842b45ecef0e30e)) 20 | * **deps:** update dependency @snyk/protect to v1.927.0 ([6c7c9af](https://github.com/timoa/nodejs-encryption-api-example/commit/6c7c9af3dad957b93849cf526d24f1b2737b0f03)) 21 | * **deps:** update dependency @snyk/protect to v1.928.0 ([eed31d8](https://github.com/timoa/nodejs-encryption-api-example/commit/eed31d8136277b024f79d144430096cdfc0a2147)) 22 | * **deps:** update dependency @snyk/protect to v1.929.0 ([4776543](https://github.com/timoa/nodejs-encryption-api-example/commit/4776543adcf17a42a3328072794e831cfa9d3448)) 23 | * **deps:** update dependency @snyk/protect to v1.931.0 ([8a7e37d](https://github.com/timoa/nodejs-encryption-api-example/commit/8a7e37d1f13fd3262fda4caccee4dfc61d608778)) 24 | * **deps:** update dependency @snyk/protect to v1.932.0 ([7879982](https://github.com/timoa/nodejs-encryption-api-example/commit/7879982e52c6ea81e20cde55783edb6a3a1c46cb)) 25 | * **deps:** update dependency @snyk/protect to v1.933.0 ([3c4b86d](https://github.com/timoa/nodejs-encryption-api-example/commit/3c4b86d63e04ee040b266d2c1cfd185fe759c36a)) 26 | * **deps:** update dependency @snyk/protect to v1.934.0 ([a4e81d0](https://github.com/timoa/nodejs-encryption-api-example/commit/a4e81d0a1dd674fc051311ba6de3c1581a7e1b35)) 27 | * **deps:** update dependency @snyk/protect to v1.935.0 ([1de671a](https://github.com/timoa/nodejs-encryption-api-example/commit/1de671ae29b22de9768513a0fbf25d8ddc62971f)) 28 | * **deps:** update dependency @snyk/protect to v1.936.0 ([4eb66c1](https://github.com/timoa/nodejs-encryption-api-example/commit/4eb66c1c40e3fd63a29a039d383f66d43add242c)) 29 | * **deps:** update dependency @snyk/protect to v1.939.0 ([caeeec9](https://github.com/timoa/nodejs-encryption-api-example/commit/caeeec9b3a10b99cf749443818551c976e02cfe4)) 30 | * **deps:** update dependency @snyk/protect to v1.940.0 ([9eb9994](https://github.com/timoa/nodejs-encryption-api-example/commit/9eb9994df02f6d8abf685d2daecf52a186d30058)) 31 | * **deps:** update dependency @snyk/protect to v1.942.0 ([4f756e2](https://github.com/timoa/nodejs-encryption-api-example/commit/4f756e2c538e4f00b8f7caf2ddc28018fec50b9a)) 32 | * **deps:** update dependency @snyk/protect to v1.945.0 ([fa99afd](https://github.com/timoa/nodejs-encryption-api-example/commit/fa99afda165c145f124c762f5d1b850c280f0faa)) 33 | * **deps:** update dependency @snyk/protect to v1.946.0 ([4374b6a](https://github.com/timoa/nodejs-encryption-api-example/commit/4374b6a0000871ba5518fcd4ac103aad068b8f5c)) 34 | * **swagger:** fix package name for Fastify Swagger ([626aeaa](https://github.com/timoa/nodejs-encryption-api-example/commit/626aeaa94872e378a8faf2ef9f3ea2905868171e)) 35 | 36 | # [1.2.0](https://github.com/timoa/nodejs-encryption-api-example/compare/v1.1.0...v1.2.0) (2022-05-14) 37 | 38 | 39 | ### Bug Fixes 40 | 41 | * **cicd:** fix nodejs pipeline ([2f35f57](https://github.com/timoa/nodejs-encryption-api-example/commit/2f35f571b2031eb98ed8ef881aff2a931112f921)) 42 | * **deps:** update dependency @snyk/protect to v1.902.0 ([6512d31](https://github.com/timoa/nodejs-encryption-api-example/commit/6512d3110dac34b19378771332bbd3f65a76ad21)) 43 | * **deps:** update dependency @snyk/protect to v1.903.0 ([f733361](https://github.com/timoa/nodejs-encryption-api-example/commit/f733361e61a68bb3927ba227491f7a3fae456961)) 44 | * **deps:** update dependency @snyk/protect to v1.904.0 ([9c7bca8](https://github.com/timoa/nodejs-encryption-api-example/commit/9c7bca8b1f728f63374d69b2419ae9b6e2dab01c)) 45 | * **deps:** update dependency @snyk/protect to v1.905.0 ([bbe125c](https://github.com/timoa/nodejs-encryption-api-example/commit/bbe125c7fe07ed42525789462ad637a364eb83a3)) 46 | * **deps:** update dependency @snyk/protect to v1.906.0 ([eca8c04](https://github.com/timoa/nodejs-encryption-api-example/commit/eca8c04ecb1cf30ddead94e5fa5999118820a44e)) 47 | * **deps:** update dependency @snyk/protect to v1.907.0 ([7681384](https://github.com/timoa/nodejs-encryption-api-example/commit/7681384bd4708a9966d974ede5aa5b8d2f589b2e)) 48 | * **deps:** update dependency @snyk/protect to v1.908.0 ([1c48afa](https://github.com/timoa/nodejs-encryption-api-example/commit/1c48afa2a587fbefa4075a0f9932eaf974d3dc81)) 49 | * **deps:** update dependency @snyk/protect to v1.910.0 ([d3f91c3](https://github.com/timoa/nodejs-encryption-api-example/commit/d3f91c392416d4fe5ef2ba673181b102bbac1615)) 50 | * **deps:** update dependency @snyk/protect to v1.912.0 ([5c796e1](https://github.com/timoa/nodejs-encryption-api-example/commit/5c796e177038bd5682269642435d98e1b21ca1b2)) 51 | * **deps:** update dependency @snyk/protect to v1.913.0 ([884c1a5](https://github.com/timoa/nodejs-encryption-api-example/commit/884c1a599ce473f381076ab98fb8c9e78f2ca39a)) 52 | * **deps:** update dependency @snyk/protect to v1.914.0 ([fed6f25](https://github.com/timoa/nodejs-encryption-api-example/commit/fed6f25defa85af2d9ae6a3271d610c40a12d9e6)) 53 | * **deps:** update dependency fastify to v3.29.0 ([f2ad318](https://github.com/timoa/nodejs-encryption-api-example/commit/f2ad318a20248abdb0592e096f3e516ead3d6a0d)) 54 | * **deps:** update dependency fastify-swagger to v5.2.0 ([9ea4b34](https://github.com/timoa/nodejs-encryption-api-example/commit/9ea4b34c0fe5ff05d850c3d4ab9868484d796156)) 55 | * **docs:** fix duplicated lines badge link ([4df3cf5](https://github.com/timoa/nodejs-encryption-api-example/commit/4df3cf5ee7413d6e088e257a2237c4635b613aa3)) 56 | 57 | 58 | ### Features 59 | 60 | * **helmet:** add Helmet support to secure HTTP headers ([89a7c41](https://github.com/timoa/nodejs-encryption-api-example/commit/89a7c411c63599b353990b9c09b2afb5086357f6)) 61 | 62 | # [1.1.0](https://github.com/timoa/nodejs-encryption-api-example/compare/v1.0.0...v1.1.0) (2022-04-15) 63 | 64 | 65 | ### Bug Fixes 66 | 67 | * **deps:** update dependencies to latest version ([dcce943](https://github.com/timoa/nodejs-encryption-api-example/commit/dcce94305abb8ab732bb5f44f81b1e9557d7d53a)) 68 | * **deps:** update dependency fastify to v2.15.3 ([18314fd](https://github.com/timoa/nodejs-encryption-api-example/commit/18314fd4cbc5cf9162a3f1c27e012f3c7427afb6)) 69 | * **deps:** update dependency fastify to v3 ([0306fb4](https://github.com/timoa/nodejs-encryption-api-example/commit/0306fb4049cffbc012bb90113275010101d0de19)) 70 | * **deps:** update dependency fastify-swagger to v4.0.1 ([78f6eb7](https://github.com/timoa/nodejs-encryption-api-example/commit/78f6eb7e46ee228bb7abfacf1bed8334cdb4fb98)) 71 | * **deps:** update dependency fastify-swagger to v4.17.1 ([d0d48ff](https://github.com/timoa/nodejs-encryption-api-example/commit/d0d48fffe2cb810e3bee25ea24ccb744bd21d31c)) 72 | * **deps:** update dependency fastify-swagger to v4.3.3 ([b0485ec](https://github.com/timoa/nodejs-encryption-api-example/commit/b0485ec59e8349285c01a2eff967fb92eabf511c)) 73 | * **deps:** update dependency fastify-swagger to v5 ([b82ffc0](https://github.com/timoa/nodejs-encryption-api-example/commit/b82ffc047c380ab30b5895712641e8b438e6851b)) 74 | * **deps:** update dependency mongoose to v5.13.14 ([8929d49](https://github.com/timoa/nodejs-encryption-api-example/commit/8929d49c7590ac8cd351e36e09dc02276886f54a)) 75 | * **deps:** update dependency snyk to v1.890.0 ([d55cc91](https://github.com/timoa/nodejs-encryption-api-example/commit/d55cc916a636065fc16d185f48953c5c729187ba)) 76 | * **deps:** update dependency snyk to v1.891.0 ([79d33dd](https://github.com/timoa/nodejs-encryption-api-example/commit/79d33ddd0a593ebbed5aaba2bdedfad972d4ea7b)) 77 | * **deps:** update dependency snyk to v1.893.0 ([1dc7afd](https://github.com/timoa/nodejs-encryption-api-example/commit/1dc7afdc4da6c26f9e7159a35fa019ea1ed5ebe2)) 78 | * **deps:** update dependency uuid to v8 ([e586eeb](https://github.com/timoa/nodejs-encryption-api-example/commit/e586eeb65dd7537e8b1abffcd031eacdafcf51a1)) 79 | * **deps:** update dependency winston to v3.3.4 ([63fb0a0](https://github.com/timoa/nodejs-encryption-api-example/commit/63fb0a091918f329aba3806dcc7899e13554a0e3)) 80 | * **deps:** update dependency winston to v3.6.0 ([d76a6cb](https://github.com/timoa/nodejs-encryption-api-example/commit/d76a6cbb0348c9d30e4eaed601b62a214cbb91d1)) 81 | * **lint:** fix ESLint minor issue ([61303dd](https://github.com/timoa/nodejs-encryption-api-example/commit/61303dd1ff00b4b9119526a605b0a8860b16e22b)) 82 | * **lint:** fix formating ([426baf8](https://github.com/timoa/nodejs-encryption-api-example/commit/426baf835534df5b9d8cff7d8aa6c44a6ed9f92d)) 83 | * package.json & package-lock.json to reduce vulnerabilities ([f87da2f](https://github.com/timoa/nodejs-encryption-api-example/commit/f87da2f39f5c49c8970281772a8fcb521eb503bd)) 84 | * package.json & package-lock.json to reduce vulnerabilities ([faf8866](https://github.com/timoa/nodejs-encryption-api-example/commit/faf8866a3366fc78496b0bddf06d5322aa405127)) 85 | * package.json & package-lock.json to reduce vulnerabilities ([ff0bd33](https://github.com/timoa/nodejs-encryption-api-example/commit/ff0bd336d8417887e9babef801de1a9bc46fcb14)) 86 | * package.json & package-lock.json to reduce vulnerabilities ([60d4a4d](https://github.com/timoa/nodejs-encryption-api-example/commit/60d4a4d8e57215bb9b1c7307d09b4a14bb2cd01a)) 87 | 88 | 89 | ### Features 90 | 91 | * **project:** update Node to 16.x ([8db9cf0](https://github.com/timoa/nodejs-encryption-api-example/commit/8db9cf0b01c545c0d16547be539f321682e930f2)) 92 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:18.17.1-alpine3.17@sha256:7186663f5c94e0f5e389304c748a2d5b5e5f8c941d305c3bae4f68a70b264261 2 | ARG appPort=3000 3 | 4 | LABEL maintainer="Damien Laureaux " \ 5 | org.label-schema.vendor="Timoa" \ 6 | org.label-schema.name="Node.js encryption API example" \ 7 | org.label-schema.description="Node.js encryption API example" \ 8 | org.label-schema.url="https://timoa.com" \ 9 | org.label-schema.vcs-url="https://github.com/timoa/nodejs-encryption-api-example" \ 10 | org.label-schema.version=latest \ 11 | org.label-schema.schema-version="1.0" 12 | 13 | RUN \ 14 | apk --no-cache update && \ 15 | apk --no-cache upgrade && \ 16 | apk add --no-cache ca-certificates && update-ca-certificates && \ 17 | rm -rf /var/cache/apk/* && \ 18 | npm install -g npm@latest && \ 19 | mkdir -p /opt/app && \ 20 | adduser -S app-user 21 | 22 | WORKDIR /opt/app/ 23 | COPY ./package.json ./ 24 | COPY ./src ./src 25 | 26 | HEALTHCHECK --interval=15s --timeout=5s --start-period=30s \ 27 | CMD npm run docker:status 28 | 29 | RUN \ 30 | npm install --production --unsafe-perm && \ 31 | npm cache clean --force 32 | 33 | ADD https://github.com/ufoscout/docker-compose-wait/releases/download/2.5.0/wait /wait 34 | 35 | RUN chmod +x /wait && chown -R app-user /opt/app 36 | USER app-user 37 | 38 | EXPOSE ${appPort} 39 | CMD /wait && npm start 40 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Damien Laureaux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Encryption API endpoints with Node.js 2 | 3 | [![Latest Release][release-badge]][release-url] 4 | [![Build Status][github-badge]][github-url] 5 | [![Docker Pulls][docker-badge]][docker-url] 6 | 7 | [![Quality Gate Status][sonarcloud-status-badge]][sonarcloud-url] 8 | [![Security Rating][sonarcloud-security-badge]][sonarcloud-url] 9 | [![Maintainability Rating][sonarcloud-maintainability-badge]][sonarcloud-url] 10 | 11 | [![Bugs][sonarcloud-bugs-badge]][sonarcloud-url] 12 | [![Code Smells][sonarcloud-codesmells-badge]][sonarcloud-url] 13 | [![Coverage][sonarcloud-coverage-badge]][sonarcloud-url] 14 | [![Duplicated Lines (%)][sonarcloud-duplicated-badge]][sonarcloud-url] 15 | 16 | ## Introduction 17 | 18 | Example of encrypting/decrypting data thru an API using node.js. 19 | 20 | The idea with this example is to test how to store encrypted data under a datastore (ex. MongoDB) and keep control of your data by providing the encryption key for each call. 21 | 22 | > This project doesn't cover encryption in transit (SSL) and not meant to be used in production. 23 | 24 | ## Features 25 | 26 | - API storing endpoint that encrypts data with the provided key and stores it into a MongoDB collection (AES-256-GCM encryption) 27 | - API retrieval endpoint that decrypts data with the provided key and returns the data 28 | - AES-256-GCM encryption that uses a random Initialization Vector (IV) and Auth TAG 29 | - IV and Auth TAG stored with the encrypted data (separated by a `:` character) 30 | - Logs with correlation ID 31 | - Hardening of the HTTP Headers with Helmet 32 | - MongoDB as a data store (using Mongoose) 33 | - Swagger support for API specifications/documentation (WIP) 34 | - Health check endpoint to check if the app is still alive 35 | - Dockerfile to generate the Docker image 36 | - Docker Compose file to launch the API and MongoDB official Docker images 37 | - Build, test and deploy to Docker Hub with GitHub Actions 38 | - SonarQube code quality check (SonarCloud) 39 | - Unit tests and functional tests 40 | - Postman collection and environment 41 | 42 | ## Run locally 43 | 44 | ### Install 45 | 46 | ``` bash 47 | node install 48 | ``` 49 | 50 | ### Run 51 | 52 | ``` bash 53 | npm start 54 | ``` 55 | 56 | ### Tests 57 | 58 | ``` bash 59 | npm test 60 | ``` 61 | 62 | ### Tests coverage 63 | 64 | ``` bash 65 | npm run test:coverage 66 | ``` 67 | 68 | ### Functional tests 69 | 70 | ``` bash 71 | npm run test:functional 72 | ``` 73 | 74 | ### Run all the tests 75 | 76 | ``` bash 77 | npm run test:all 78 | ``` 79 | 80 | ## Docker 81 | 82 | ### Docker Compose 83 | 84 | > Be sure that you are not running MongoDB + another node.js app that uses the `3000` port 85 | 86 | ```bash 87 | docker-compose up 88 | ``` 89 | 90 | ## Test with Postman 91 | 92 | First, you need to import the Postman environment. 93 | 94 | There is a default encryption key and ID to have a quick look to the API. 95 | 96 | ### Download and Import the Postman environment 97 | 98 | Download the [Postman Environment][postman-environment] 99 | 100 | ### Run the Postman collection 101 | 102 | [![Run in Postman][postman-run-button]][postman-run-url] 103 | 104 | ## Documentation / Specifications 105 | 106 | You can access the documentation (Swagger) here: 107 | 108 | [http://localhost:3000/swagger][swagger] 109 | 110 | ## How it works 111 | 112 | ### Generate an Encryption key (256 bit / 32 chars length) 113 | 114 | You need to generate an encryption key that you will use to encrypt the data saved in MongoDB. 115 | 116 | You can use this online website to create your key (256 bit): 117 | 118 | [https://www.allkeysgenerator.com][allkeysgenerator] 119 | 120 | ### Add secret 121 | 122 | Fill the following curl command with your key and value is the JSON data you want to encrypt. 123 | 124 | ``` bash 125 | curl -X POST \ 126 | http://127.0.0.1:3000/api/secrets/add \ 127 | -H 'Accept: application/json' \ 128 | -H 'Content-Type: application/json' \ 129 | -d '{ 130 | "id": "test-01", 131 | "encryption_key": "p2s5v8y/B?E(H+MbQeShVmYq3t6w9z$C", 132 | "value": { 133 | "first_name": "firstname", 134 | "last_name": "lastname", 135 | "email": "email@email.com", 136 | "password": "app123", 137 | "password_confirmation": "app123" 138 | } 139 | }' 140 | ``` 141 | 142 | Payload with the encryption key used in the example (will be different for you since we use a random IV): 143 | 144 | ``` bash 145 | { 146 | "_id": "5c61979c82126860464dd0e8", 147 | "id": "test-01", 148 | "value": "42d0f6eb0810caaaaf5bad7477ebfc44:3572036ad7b4d77959cbc85feb364bf2c3442f7290ab210e88b00aae5a8122509df282db39ffcd092a927c4f302b93ba87f70563af8a51b29577196cc010d5514d29351ee74b64538d9004f581c911ea059be8769520075659e497a6b716ab95af692b56326a682b443d05150e90d8b75c43eabe15a27c01f240eae9edecf345436bb294b28c41087629754b01ada42c", 149 | "__v": 0 150 | } 151 | ``` 152 | 153 | Note that the IV is in the first part fo the encrypted data (`42d0f6eb0810caaaaf5bad7477ebfc44`) 154 | 155 | ### Get secret(s) 156 | 157 | #### Get a specific ID 158 | 159 | You can search by ID (`test-01` in this example): 160 | 161 | ``` bash 162 | curl -X POST \ 163 | http://127.0.0.1:3000/api/secrets \ 164 | -H 'Accept: application/json' \ 165 | -H 'Content-Type: application/json' \ 166 | -d '{ 167 | "id": "test-01", 168 | "encryption_key": "p2s5v8y/B?E(H+MbQeShVmYq3t6w9z$C" 169 | }' 170 | ``` 171 | 172 | This will return an array with a unique result: 173 | 174 | ``` json 175 | [ 176 | { 177 | "id": "test-01", 178 | "value": { 179 | "first_name": "firstname", 180 | "last_name": "lastname", 181 | "email": "email@email.com", 182 | "password": "app123", 183 | "password_confirmation": "app123" 184 | } 185 | } 186 | ] 187 | ``` 188 | 189 | #### Get ID with a wildcard `*` 190 | 191 | You can also search by using a wildcard `*` at the end of your ID (`test-01-*` in this example): 192 | 193 | ``` bash 194 | curl -X POST \ 195 | http://127.0.0.1:3000/api/secrets \ 196 | -H 'Accept: application/json' \ 197 | -H 'Content-Type: application/json' \ 198 | -d '{ 199 | "id": "test-01-*", 200 | "encryption_key": "p2s5v8y/B?E(H+MbQeShVmYq3t6w9z$C" 201 | }' 202 | ``` 203 | 204 | This will return an array of results: 205 | 206 | ``` json 207 | [ 208 | { 209 | "id": "test-01-01", 210 | "value": { 211 | "first_name": "firstname", 212 | "last_name": "lastname", 213 | "email": "email@email.com", 214 | "password": "app123", 215 | "password_confirmation": "app123" 216 | } 217 | }, 218 | { 219 | "id": "test-01-02", 220 | "value": { 221 | "first_name": "firstname", 222 | "last_name": "lastname", 223 | "email": "email@email.com", 224 | "password": "app123", 225 | "password_confirmation": "app123" 226 | } 227 | }, 228 | { 229 | "id": "test-01-03", 230 | "value": { 231 | "first_name": "firstname", 232 | "last_name": "lastname", 233 | "email": "email@email.com", 234 | "password": "app123", 235 | "password_confirmation": "app123" 236 | } 237 | } 238 | ] 239 | ``` 240 | 241 | ## TODO 242 | 243 | - Return an empty array if wrong encryption key instead of error 244 | - Swagger detailed schema 245 | - PM2 support under the Docker container (to restart the app in case of crash) 246 | 247 | [swagger]: http://localhost:3000/swagger 248 | [allkeysgenerator]: https://www.allkeysgenerator.com/Random/Security-Encryption-Key-Generator.aspx 249 | [postman-environment]: https://raw.githubusercontent.com/timoa/nodejs-encryption-api-example/master/src/config/postman.environment.json 250 | [postman-run-button]: https://run.pstmn.io/button.svg 251 | [postman-run-url]: https://app.getpostman.com/run-collection/e34aee6688c0937c6643 252 | [sonarcloud]: https://sonarcloud.io/about 253 | [release-badge]: https://img.shields.io/github/v/release/timoa/nodejs-encryption-api-example?logoColor=orange 254 | [release-url]: https://github.com/timoa/nodejs-encryption-api-example/releases 255 | [github-badge]: https://github.com/timoa/nodejs-encryption-api-example/workflows/Build/badge.svg 256 | [github-url]: https://github.com/timoa/nodejs-encryption-api-example/actions?query=workflow%3ABuild 257 | [docker-badge]: https://img.shields.io/docker/pulls/timoa/nodejs-encryption-api-example.svg 258 | [docker-url]: https://hub.docker.com/r/timoa/nodejs-encryption-api-example 259 | [sonarcloud-url]: https://sonarcloud.io/dashboard?id=timoa_nodejs-encryption-api-example 260 | [sonarcloud-status-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=alert_status 261 | [sonarcloud-security-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=security_rating 262 | [sonarcloud-maintainability-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=sqale_rating 263 | [sonarcloud-bugs-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=bugs 264 | [sonarcloud-codesmells-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=code_smells 265 | [sonarcloud-coverage-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=coverage 266 | [sonarcloud-duplicated-badge]: https://sonarcloud.io/api/project_badges/measure?project=timoa_nodejs-encryption-api-example&metric=duplicated_lines_density 267 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Supported Versions 4 | 5 | We release patches for security vulnerabilities. Which versions are eligible 6 | receiving such patches depend on the CVSS v3.0 Rating: 7 | 8 | | Version | Supported | 9 | | ------- | ------------------ | 10 | | > 1.0.0 | :white_check_mark: | 11 | | < 1.0.0 | :x: | 12 | 13 | ## Reporting a Vulnerability 14 | 15 | Please report (suspected) security vulnerabilities to **[issue board](https://github.com/timoa/nodejs-encryption-api-example/issues)** 16 | with the label **vulnerability**. If the issue is confirmed, we will release a patch as soon as possible depending on complexity, 17 | but historically within a few days. 18 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.3' 2 | 3 | services: 4 | api: 5 | image: timoa/nodejs-encryption-api-example:latest@sha256:33cac806d192b0c025adb464d0dae158785b13ade8826a34ca4d08a8f6a19b61 6 | environment: 7 | - NODE_ENV=production 8 | - NODE_HOST=0.0.0.0 9 | - MONGO_HOST=mongo 10 | - WAIT_HOSTS=mongo:27017 11 | ports: 12 | - 3000:3000 13 | restart: always 14 | links: 15 | - mongo 16 | depends_on: 17 | - mongo 18 | mongo: 19 | container_name: mongo 20 | image: mongo@sha256:d341a86584b96eb665345a8f5b35fba8695ee1d0618fd012ec4696223a3d6c62 21 | volumes: 22 | - ./data:/data/db 23 | ports: 24 | - 27017:27017 25 | healthcheck: 26 | test: ["CMD", "curl", "-f", "http://localhost:27017"] 27 | interval: 30s 28 | timeout: 10s 29 | retries: 5 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nodejs-encryption-api-example", 3 | "version": "1.2.1", 4 | "description": "Example of encrypting/decrypting data thru an API using node.js", 5 | "main": "src/index.js", 6 | "private": true, 7 | "snyk": true, 8 | "scripts": { 9 | "start": "node src/index", 10 | "pretest": "eslint ./src", 11 | "test": "./node_modules/.bin/mocha --reporter spec", 12 | "test:coverage": "./node_modules/.bin/nyc npm test", 13 | "test:functional": "./node_modules/.bin/mocha --reporter spec ./test/functional", 14 | "test:all": "./node_modules/.bin/run-s test:functional test:coverage", 15 | "docker:status": "node src/healthcheck" 16 | }, 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/timoa/nodejs-encryption-api-example.git" 20 | }, 21 | "keywords": [ 22 | "encryption", 23 | "api", 24 | "endpoint", 25 | "datastore" 26 | ], 27 | "author": { 28 | "name": "Damien Laureaux", 29 | "email": "d.laureaux@timoa.com", 30 | "url": "https://timoa.com" 31 | }, 32 | "license": "MIT", 33 | "bugs": { 34 | "url": "https://github.com/timoa/nodejs-encryption-api-example/issues" 35 | }, 36 | "homepage": "https://github.com/timoa/nodejs-encryption-api-example#readme", 37 | "dependencies": { 38 | "@fastify/helmet": "10.1.1", 39 | "@fastify/swagger": "8.11.0", 40 | "@hapi/boom": "10.0.1", 41 | "fastify": "4.24.1", 42 | "fastify-healthcheck": "4.4.0", 43 | "lodash": "4.17.21", 44 | "mongoose": "5.13.20", 45 | "uuid": "8.3.2", 46 | "winston": "3.10.0" 47 | }, 48 | "engines": { 49 | "node": ">=18.0", 50 | "npm": ">=8.6.0" 51 | }, 52 | "os": [ 53 | "linux", 54 | "win32", 55 | "darwin" 56 | ], 57 | "devDependencies": { 58 | "acorn": "8.10.0", 59 | "acorn-jsx": "5.3.2", 60 | "chai": "4.3.10", 61 | "chai-as-promised": "7.1.1", 62 | "chai-http": "4.4.0", 63 | "eslint": "8.51.0", 64 | "eslint-config-airbnb": "19.0.4", 65 | "eslint-plugin-import": "2.28.1", 66 | "eslint-plugin-jsx-a11y": "6.7.1", 67 | "eslint-plugin-react": "7.33.2", 68 | "espree": "9.6.1", 69 | "mocha": "10.2.0", 70 | "npm-run-all": "4.1.5", 71 | "nyc": "15.1.0", 72 | "request": "2.88.2" 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /scripts/version.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | function readJson { 4 | UNAMESTR=`uname` 5 | if [[ "$UNAMESTR" == 'Linux' ]]; then 6 | SED_EXTENDED='-r' 7 | elif [[ "$UNAMESTR" == 'Darwin' ]]; then 8 | SED_EXTENDED='-E' 9 | fi; 10 | 11 | VALUE=`grep -m 1 "\"${2}\"" ${1} | sed ${SED_EXTENDED} 's/^ *//;s/.*: *"//;s/",?//'` 12 | 13 | if [ ! "$VALUE" ]; then 14 | echo "Error: Cannot find \"${2}\" in ${1}" >&2; 15 | exit 1; 16 | else 17 | echo $VALUE ; 18 | fi; 19 | } 20 | 21 | VERSION=`readJson package.json version` || exit 1; 22 | 23 | echo $VERSION 24 | -------------------------------------------------------------------------------- /sonar-project.properties: -------------------------------------------------------------------------------- 1 | sonar.projectKey=timoa_nodejs-encryption-api-example 2 | sonar.projectName=nodejs-encryption-api-example 3 | 4 | # ===================================================== 5 | # Meta-data for the project 6 | # ===================================================== 7 | 8 | sonar.links.homepage=https://github.com/timoa/nodejs-encryption-api-example 9 | sonar.links.ci=https://travis-ci.com/timoa/nodejs-encryption-api-example 10 | sonar.links.scm=https://github.com/timoa/nodejs-encryption-api-example 11 | sonar.links.issue=https://github.com/timoa/nodejs-encryption-api-example/issues 12 | 13 | 14 | # ===================================================== 15 | # Properties that will be shared amongst all modules 16 | # ===================================================== 17 | 18 | sonar.host.url=https://sonarcloud.io 19 | sonar.organization=timoa-github 20 | sonar.scm.provider=git 21 | sonar.pullrequest.provider=github 22 | sonar.pullrequest.github.repository=timoa/nodejs-encryption-api-example 23 | sonar.sources=src 24 | sonar.exclusions=node_modules/**/* 25 | 26 | # ===================================================== 27 | # Java config 28 | # ===================================================== 29 | sonar.java.source=1.8 30 | sonar.java.binaries=. 31 | 32 | # ===================================================== 33 | # Test coverage 34 | # ===================================================== 35 | sonar.javascript.lcov.reportPaths=coverage/lcov.info 36 | -------------------------------------------------------------------------------- /src/config/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "app": { 3 | "name": "encryptionAPI", 4 | "port": 3000 5 | }, 6 | "healthCheck": { 7 | "path": "_health" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/config/postman.environment.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "ea8a2f5e-c53e-4be1-ac05-97bf15d4fe88", 3 | "name": "Encryption API", 4 | "values": [ 5 | { 6 | "key": "server", 7 | "value": "http:///localhost", 8 | "description": "", 9 | "enabled": true 10 | }, 11 | { 12 | "key": "port", 13 | "value": "3000", 14 | "description": "", 15 | "enabled": true 16 | }, 17 | { 18 | "key": "id", 19 | "value": "test-01", 20 | "type": "text", 21 | "description": "", 22 | "enabled": true 23 | }, 24 | { 25 | "key": "encryptionKey", 26 | "value": "p2s5v8y/B?E(H+MbQeShVmYq3t6w9z$C", 27 | "description": "", 28 | "enabled": true 29 | } 30 | ], 31 | "_postman_variable_scope": "environment", 32 | "_postman_exported_at": "2019-02-15T16:53:17.865Z", 33 | "_postman_exported_using": "Postman/6.7.3" 34 | } 35 | -------------------------------------------------------------------------------- /src/controllers/secretController.js: -------------------------------------------------------------------------------- 1 | const boom = require('@hapi/boom'); 2 | const _ = require('lodash'); 3 | const Secret = require('../models/secret'); 4 | const crypto = require('../lib/crypto'); 5 | 6 | // Get secrets by ID 7 | exports.getSecrets = async (req) => { 8 | try { 9 | const query = req.body.id.search(/\*/) !== -1 ? { $regex: `${req.body.id}` } : req.body.id; 10 | const encryptedSecrets = await Secret.find({ id: query }); 11 | const secrets = []; 12 | 13 | _.forEach(encryptedSecrets, (secret) => { 14 | secrets.push({ 15 | id: secret.id, 16 | value: JSON.parse(crypto.decrypt(secret.value, req.body.encryption_key)), 17 | }); 18 | }); 19 | 20 | return secrets; 21 | } catch (err) { 22 | throw boom.boomify(err); 23 | } 24 | }; 25 | 26 | // Add a new secret 27 | exports.addSecret = async (req) => { 28 | try { 29 | // Cipher data must be a string or a buffer 30 | const unencryptedSecret = JSON.stringify(req.body.value); 31 | 32 | // Encrypt secret with the encryption key passed by POST 33 | const encryptedSecret = crypto.encrypt(unencryptedSecret, req.body.encryption_key); 34 | 35 | // Save the encrypted Secret under MongoDB 36 | const data = { 37 | id: req.body.id, 38 | value: encryptedSecret, 39 | }; 40 | 41 | // Save data 42 | return Secret.findOneAndUpdate( 43 | { 44 | id: req.body.id, 45 | }, 46 | data, 47 | { 48 | upsert: true, 49 | new: true, 50 | runValidators: true, 51 | }, 52 | ); 53 | } catch (err) { 54 | throw boom.boomify(err); 55 | } 56 | }; 57 | -------------------------------------------------------------------------------- /src/healthcheck.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2018 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | // standalone script, to get content from the running web application 18 | // for example it can be called by container health checks 19 | // return code will be 0 for success, or the HTTP error code 20 | 21 | // use Node.js 'http' integrated module, 22 | // even to avoid dependencies clash 23 | const http = require('http'); 24 | const logger = require('./lib/logger'); 25 | const config = require('./config/config.json'); 26 | 27 | const options = { 28 | timeout: 5000, // 5 sec 29 | log: true, // if enabled, write log to console 30 | }; 31 | const url = process.argv[2] || `http://${config.app.host}:${config.app.port}/${config.healthCheck.path}`; 32 | if (options.log === true) { 33 | logger.info(`GET call for healthcheck at: ${url} ...`); 34 | } 35 | 36 | const request = http.get(url, (res) => { 37 | if (options.log === true) { 38 | logger.info(`statusCode: ${res.statusCode}`); 39 | if (res.statusMessage) { 40 | logger.info(`statusMessage: '${res.statusMessage}'`); 41 | } 42 | logger.info('----------------'); 43 | } 44 | if (res.statusCode === 200) { 45 | process.exit(0); 46 | } else { 47 | process.exit(res.statusCode || 1); 48 | } 49 | }); 50 | request.setTimeout(options.timeout); 51 | 52 | request.on('error', (err) => { 53 | if (options.log === true) { 54 | logger.info(`error: ${err.message}`); 55 | } 56 | process.exit(err.statusCode || 1); 57 | }); 58 | 59 | request.end(); 60 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const fastify = require('fastify')(); 2 | const fastifyHelmet = require('@fastify/helmet'); 3 | const fastifyHealthcheck = require('fastify-healthcheck'); 4 | const fastifySwagger = require('@fastify/swagger'); 5 | 6 | const logger = require('./lib/logger'); 7 | const config = require('./config/config.json'); 8 | const routes = require('./routes'); 9 | const db = require('./lib/db'); 10 | 11 | const host = process.env.NODE_HOST || 'localhost'; 12 | const port = process.env.NODE_PORT || config.app.port; 13 | 14 | // Connect to MongoDB 15 | db.connect(); 16 | 17 | // Register Helmet 18 | fastify.register(fastifyHelmet, { 19 | global: true, 20 | }); 21 | 22 | // Register the Health plugin 23 | fastify.register(fastifyHealthcheck, { 24 | healthcheckUrl: `/${config.healthCheck.path}`, 25 | }); 26 | 27 | // Import Swagger Options 28 | const swagger = require('./swagger/options'); 29 | 30 | // Register the Swagger plugin 31 | fastify.register(fastifySwagger, swagger.options); 32 | 33 | // Load the routes 34 | routes.forEach((route) => { 35 | fastify.route(route); 36 | }); 37 | 38 | // Start the Fastify HTTP server 39 | const start = async () => { 40 | try { 41 | await fastify.listen({ port, host }) 42 | .then((address) => { 43 | fastify.swagger(); 44 | logger.info(`Server listening on ${address}`); 45 | }); 46 | } catch (err) { 47 | logger.error(err); 48 | process.exit(1); 49 | } 50 | }; 51 | 52 | start(); 53 | -------------------------------------------------------------------------------- /src/lib/crypto.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | 3 | const algorithm = 'AES-256-GCM'; 4 | 5 | /** 6 | * Generate an Initialization Vector 7 | * @returns {Buffer} 8 | */ 9 | function generateIv() { 10 | return crypto.randomBytes(32); 11 | } 12 | 13 | /** 14 | * Encrypt a secret 15 | * @param {String} data Data to encrypt 16 | * @param {String} encryptionKey Encryption key to uses to encrypt the secret 17 | * 18 | * @returns {String} Encrypted data 19 | */ 20 | function encrypt(data, encryptionKey) { 21 | // Generate an Initialization Vector for each encryption 22 | const iv = generateIv(); 23 | 24 | // Cipher 25 | const cipher = crypto.createCipheriv(algorithm, encryptionKey, iv); 26 | 27 | // Encrypt the data 28 | const encryptedData = Buffer.concat([ 29 | cipher.update(Buffer.from(data, 'utf-8')), 30 | cipher.final(), 31 | ]); 32 | 33 | const authTag = cipher.getAuthTag(); 34 | 35 | // Embedded IV with the encrypted secret 36 | return `${iv.toString('hex')}:${authTag.toString('hex')}:${encryptedData.toString('hex')}`; 37 | } 38 | 39 | /** 40 | * Decrypt a secret 41 | * @param {String} data Encrypted data 42 | * @param {String} encryptionKey Encryption key used to encrypt the data 43 | * 44 | * @returns {String} 45 | */ 46 | function decrypt(data, encryptionKey) { 47 | // Retrieve the IV from the encrypted data 48 | const encryptedData = data.split(':'); 49 | const iv = Buffer.from(encryptedData.shift(), 'hex'); 50 | const authTag = Buffer.from(encryptedData.shift(), 'hex'); 51 | 52 | // Retrieve the secret 53 | const encryptedSecret = Buffer.from(encryptedData.join(':'), 'hex'); 54 | 55 | // Decipher 56 | const decipher = crypto.createDecipheriv(algorithm, Buffer.from(encryptionKey), iv); 57 | decipher.setAuthTag(authTag); 58 | 59 | // Decrypt the data 60 | const decrypted = Buffer.concat([ 61 | decipher.update(encryptedSecret), 62 | decipher.final(), 63 | ]); 64 | 65 | return decrypted.toString('utf-8'); 66 | } 67 | 68 | module.exports = { encrypt, decrypt }; 69 | -------------------------------------------------------------------------------- /src/lib/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | const logger = require('./logger'); 3 | const config = require('../config/config.json'); 4 | 5 | const host = process.env.MONGO_HOST || 'localhost'; 6 | const port = process.env.MONGO_PORT || 27017; 7 | 8 | /** 9 | * MongoDB connection 10 | */ 11 | function connect() { 12 | mongoose.connect(`mongodb://${host}:${port}/${config.app.name}`, { 13 | useNewUrlParser: true, 14 | useCreateIndex: true, 15 | useFindAndModify: false, 16 | useUnifiedTopology: true, 17 | }) 18 | .then(() => logger.info('MongoDB connected')) 19 | .catch((err) => logger.error(err)); 20 | } 21 | 22 | function close(callback) { 23 | mongoose.connection.close(true, callback); 24 | } 25 | 26 | module.exports = { connect, close }; 27 | -------------------------------------------------------------------------------- /src/lib/logger.js: -------------------------------------------------------------------------------- 1 | const { createLogger, format, transports } = require('winston'); 2 | const uuid = require('uuid'); 3 | 4 | const env = process.env.NODE_ENV || 'development'; 5 | const correlationId = uuid.v1(); 6 | 7 | const logger = createLogger({ 8 | level: env === 'development' ? 'debug' : 'info', 9 | format: format.combine( 10 | format.timestamp({ 11 | format: 'YYYY-MM-DD HH:mm:ss', 12 | }), 13 | format.json(), 14 | ), 15 | transports: [ 16 | new transports.Console({ 17 | level: 'info', 18 | format: format.combine( 19 | format.colorize(), 20 | format.printf( 21 | (info) => `${correlationId} | ${info.timestamp} | ${info.level}: ${info.message}`, 22 | ), 23 | ), 24 | }), 25 | ], 26 | }); 27 | 28 | module.exports = logger; 29 | -------------------------------------------------------------------------------- /src/models/secret.js: -------------------------------------------------------------------------------- 1 | // External Dependancies 2 | const mongoose = require('mongoose'); 3 | 4 | const secretSchema = new mongoose.Schema({ 5 | id: { 6 | type: String, 7 | required: true, 8 | index: true, 9 | }, 10 | value: { // Encrypted JSON 11 | type: String, 12 | required: true, 13 | }, 14 | }); 15 | 16 | module.exports = mongoose.model('Secret', secretSchema); 17 | -------------------------------------------------------------------------------- /src/routes/index.js: -------------------------------------------------------------------------------- 1 | const secretController = require('../controllers/secretController'); 2 | const specification = require('../swagger/specification'); 3 | 4 | const routes = [ 5 | { 6 | method: 'POST', 7 | url: '/api/secrets', 8 | schema: specification.schema.getSecrets, 9 | handler: secretController.getSecrets, 10 | }, 11 | { 12 | method: 'POST', 13 | url: '/api/secrets/add', 14 | schema: specification.schema.addSecret, 15 | handler: secretController.addSecret, 16 | }, 17 | ]; 18 | 19 | module.exports = routes; 20 | -------------------------------------------------------------------------------- /src/swagger/options.js: -------------------------------------------------------------------------------- 1 | const config = require('../config/config.json'); 2 | 3 | exports.options = { 4 | routePrefix: '/swagger', 5 | exposeRoute: true, 6 | staticCSP: true, 7 | transformStaticCSP: (header) => header, 8 | swagger: { 9 | info: { 10 | title: 'Encryption API with Node.js', 11 | description: 'REST API with encryption/decryption endpoints using Node.js, MongoDB, Fastify and Swagger', 12 | version: '1.0.0', 13 | }, 14 | externalDocs: { 15 | url: 'https://swagger.io', 16 | description: 'Find more info here', 17 | }, 18 | host: `localhost:${config.app.port}`, 19 | schemes: ['http'], 20 | consumes: ['application/json'], 21 | produces: ['application/json'], 22 | }, 23 | uiConfig: { 24 | docExpansion: 'full', 25 | deepLinking: false, 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/swagger/specification.js: -------------------------------------------------------------------------------- 1 | exports.schema = { 2 | getSecrets: { 3 | description: 'Get one or more secrets stored on MongoDB and decrypt them, using a provided encryption key', 4 | tags: ['Secrets'], 5 | body: { 6 | type: 'object', 7 | properties: { 8 | id: { 9 | type: 'string', 10 | description: 'Secret ID. You can also provide a wildcard "*" at the end of the secret ID', 11 | }, 12 | encryptionKey: { 13 | type: 'string', 14 | description: 'Encryption Key used to encrypt the secret before saving it to MongoDB', 15 | }, 16 | }, 17 | }, 18 | }, 19 | addSecret: { 20 | description: 'Encrypt a secret (any JSON types) and save it to MongoDB using an Encryption Key', 21 | tags: ['Secrets'], 22 | body: { 23 | type: 'object', 24 | properties: { 25 | id: { 26 | type: 'string', 27 | description: 'Secret ID', 28 | }, 29 | encryptionKey: { 30 | type: 'string', 31 | description: 'Encryption Key used to encrypt the secret before saving it to MongoDB', 32 | }, 33 | value: { 34 | type: 'object', 35 | description: 'Object or any other JSON types to encrypt', 36 | }, 37 | }, 38 | }, 39 | }, 40 | healthCheck: { 41 | description: 'Health check endpoint', 42 | tags: ['Status'], 43 | body: { 44 | type: 'object', 45 | properties: { 46 | statusCode: { 47 | type: 'number', 48 | description: 'Status code', 49 | }, 50 | status: { 51 | type: 'string', 52 | description: 'Status', 53 | }, 54 | }, 55 | }, 56 | }, 57 | }; 58 | -------------------------------------------------------------------------------- /src/swagger/specification.yaml: -------------------------------------------------------------------------------- 1 | openapi: 3.0.0 2 | info: 3 | description: "Encryption API specification" 4 | version: "1.0.0" 5 | title: "Test Swagger specification" 6 | contact: 7 | email: "d.laureaux@timoa.com" 8 | servers: 9 | - url: http://localhost:3000/ 10 | description: Localhost (uses test data) 11 | paths: 12 | /_health: 13 | get: 14 | description: Heath check route, so we can check if server is alive 15 | tags: 16 | - Status 17 | responses: 18 | 200: 19 | description: 'Server is alive' 20 | content: 21 | application/json: 22 | schema: 23 | type: object 24 | properties: 25 | statusCode: 26 | type: number 27 | status: 28 | type: string 29 | example: 30 | statusCode: 200 31 | status: "ok" 32 | -------------------------------------------------------------------------------- /test/config.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const config = require('../src/config/config.json'); 3 | 4 | // Configuration 5 | describe('Config file', () => { 6 | it('expect "app.name" to be a string', (done) => { 7 | expect(config.app.name).to.be.a('string'); 8 | done(); 9 | }); 10 | it('expect "app.port" to be a number', (done) => { 11 | expect(config.app.port).to.be.an('number'); 12 | done(); 13 | }); 14 | it('expect "healthCheck.path" to be a string', (done) => { 15 | expect(config.healthCheck.path).to.be.a('string'); 16 | done(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/functional/healthcheck.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { expect } = require('chai'); 3 | const config = require('../../src/config/config.json'); 4 | 5 | // Server 6 | describe('Health endpoint response', () => { 7 | it('should return 200', (done) => { 8 | request.get(`http://localhost:${config.app.port}/${config.healthCheck.path}`, (err, res) => { 9 | if (err) throw err; 10 | expect(res.statusCode).to.equal(200); 11 | done(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/functional/index.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { expect } = require('chai'); 3 | const config = require('../../src/config/config.json'); 4 | 5 | // Server 6 | describe('Root endpoint response', () => { 7 | it('should return 404', (done) => { 8 | request.get(`http://localhost:${config.app.port}/`, (err, res) => { 9 | if (err) throw err; 10 | expect(res.statusCode).to.equal(404); 11 | done(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/functional/secret.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { expect } = require('chai'); 3 | const config = require('../../src/config/config.json'); 4 | const testData = require('../testData.json'); 5 | 6 | // Add Secret 7 | describe('Add Secret endpoint response', () => { 8 | it('should return 200', (done) => { 9 | const options = { 10 | method: 'POST', 11 | url: `http://localhost:${config.app.port}/api/secrets/add`, 12 | headers: { 13 | Accept: 'application/json', 14 | 'Content-Type': 'application/json', 15 | }, 16 | body: { 17 | id: testData.id, 18 | encryption_key: testData.encryptionKey, 19 | value: testData.secret, 20 | }, 21 | json: true, 22 | }; 23 | 24 | request(options, (err, res) => { 25 | if (err) throw err; 26 | expect(res.statusCode).to.equal(200); 27 | done(); 28 | }); 29 | }); 30 | }); 31 | 32 | // Get Secret 33 | describe('Get Secret(s) endpoint response', () => { 34 | it('should return 200', (done) => { 35 | const options = { 36 | method: 'POST', 37 | url: `http://localhost:${config.app.port}/api/secrets`, 38 | headers: { 39 | Accept: 'application/json', 40 | 'Content-Type': 'application/json', 41 | }, 42 | body: { 43 | id: testData.id, 44 | encryption_key: testData.encryptionKey, 45 | }, 46 | json: true, 47 | }; 48 | 49 | request(options, (err, res) => { 50 | if (err) throw err; 51 | expect(res.statusCode).to.equal(200); 52 | done(); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /test/functional/swagger.spec.js: -------------------------------------------------------------------------------- 1 | const request = require('request'); 2 | const { expect } = require('chai'); 3 | const config = require('../../src/config/config.json'); 4 | 5 | // Swagger 6 | describe('Swagger endpoint response', () => { 7 | it('should return 200', (done) => { 8 | request.get(`http://localhost:${config.app.port}/swagger/`, (err, res) => { 9 | if (err) throw err; 10 | expect(res.statusCode).to.equal(200); 11 | done(); 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /test/lib.crypto.spec.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable max-len */ 2 | const { expect } = require('chai'); 3 | const crypto = require('../src/lib/crypto'); 4 | const testData = require('./testData.json'); 5 | 6 | // Crypto 7 | describe('Crypto library', () => { 8 | it('expect the "encrypt" function to return a string', (done) => { 9 | expect(crypto.encrypt(JSON.stringify(testData.secret), testData.encryptionKey)).to.be.a('string'); 10 | done(); 11 | }); 12 | it('expect the "decrypt" function to return an object', (done) => { 13 | expect(JSON.parse(crypto.decrypt(testData.encryptedSecret, testData.encryptionKey))).to.be.an('object'); 14 | done(); 15 | }); 16 | it('expect the "decrypt" function to return the same object as original data', (done) => { 17 | expect(JSON.parse(crypto.decrypt(testData.encryptedSecret, testData.encryptionKey))).to.deep.equals(testData.secret); 18 | done(); 19 | }); 20 | }); 21 | -------------------------------------------------------------------------------- /test/lib.db.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const db = require('../src/lib/db'); 3 | 4 | // DB 5 | describe('DB Connect', () => { 6 | it('expect "connect" to be a function', (done) => { 7 | expect(typeof (db.connect)).to.be.equals('function'); 8 | done(); 9 | }); 10 | it('expect "connect" to not throw', (done) => { 11 | expect(db.connect).to.not.throw(); 12 | db.close(() => { 13 | done(); 14 | }); 15 | }); 16 | }); 17 | 18 | describe('DB Close', () => { 19 | it('expect "close" to be a function', (done) => { 20 | expect(typeof (db.close)).to.be.equals('function'); 21 | done(); 22 | }); 23 | it('expect "close" to not throw', (done) => { 24 | expect(() => { 25 | done(); 26 | }).to.not.throw(); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /test/lib.logger.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const logger = require('../src/lib/logger'); 3 | 4 | // Logger 5 | describe('Logger library', () => { 6 | it('expect the "logger.info" to exists and be a function', (done) => { 7 | expect(typeof (logger.info)).to.be.equals('function'); 8 | done(); 9 | }); 10 | it('expect the "logger.info" function to not throw without an argument', (done) => { 11 | expect(logger.info).to.not.throw(); 12 | done(); 13 | }); 14 | it('expect the "logger.warn" to exists and be a function', (done) => { 15 | expect(typeof (logger.warn)).to.be.equals('function'); 16 | done(); 17 | }); 18 | it('expect the "logger.warn" function to not throw without an argument', (done) => { 19 | expect(logger.warn).to.not.throw(); 20 | done(); 21 | }); 22 | it('expect the "logger.error" to exists and be a function', (done) => { 23 | expect(typeof (logger.error)).to.be.equals('function'); 24 | done(); 25 | }); 26 | it('expect the "logger.error" function to not throw without an argument', (done) => { 27 | expect(logger.error).to.not.throw(); 28 | done(); 29 | }); 30 | it('expect the "logger.log" to exists and be a function', (done) => { 31 | expect(typeof (logger.log)).to.be.equals('function'); 32 | done(); 33 | }); 34 | it('expect the "logger.log" function to throw without an argument', (done) => { 35 | expect(logger.log).to.throw(); 36 | done(); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /test/routes.index.spec.js: -------------------------------------------------------------------------------- 1 | const { expect } = require('chai'); 2 | const routes = require('../src/routes'); 3 | 4 | // Routes 5 | describe('Routes', () => { 6 | it('expect "routes" to be a array', (done) => { 7 | expect(routes).to.be.an('array'); 8 | done(); 9 | }); 10 | it('expect the first route URL to be "/api/secrets"', (done) => { 11 | expect(routes[0].url).to.be.equals('/api/secrets'); 12 | done(); 13 | }); 14 | it('expect the second route URL to be "/api/secrets/add"', (done) => { 15 | expect(routes[1].url).to.be.equals('/api/secrets/add'); 16 | done(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /test/testData.json: -------------------------------------------------------------------------------- 1 | { 2 | "id": "test-01", 3 | "encryptionKey": "5v8y/B?E(H+MbQeThWmZq4t6w9z$C&F)", 4 | "secret": { 5 | "first_name": "firstname", 6 | "last_name": "lastname", 7 | "email": "email@email.com", 8 | "password": "app123", 9 | "password_confirmation": "app123" 10 | }, 11 | "encryptedSecret": "758bdba944c8fd1bf0ec446420179f4ce66f272777537318ea07e5d1fb95ecff:923653370a204b5162cda7d145d6f691:5e4fd43516fe1c22ab50f2500ae94b216b3fd6a534a7f642d2d4fcd147da6f9125bd28a9100141508125c3e7adcecabeafba20739086e3dfcdcbf87a34d5ef1301b4d9e0189626ef0c5d0666cf3a780f191c712f519c9f500da2371629d5d58d2ca330e9ad5191ae3512eb27cc7adb3a4e936f89d06de0d954260161143b04b6" 12 | } 13 | --------------------------------------------------------------------------------