Skip to content

feat(api): support encrypted array start inputs#1944

Merged
elibosley merged 4 commits intomainfrom
codex/issue-1943-array-decryption-password
Mar 21, 2026
Merged

feat(api): support encrypted array start inputs#1944
elibosley merged 4 commits intomainfrom
codex/issue-1943-array-decryption-password

Conversation

@elibosley
Copy link
Member

@elibosley elibosley commented Mar 20, 2026

Summary

  • add GraphQL support for starting encrypted arrays with either a passphrase or keyfile payload
  • mirror the existing webgui behavior for passphrase encoding and keyfile handling
  • redact encrypted array unlock inputs from emcmd debug logging and add regression coverage

Testing

  • pnpm --filter ./api test src/unraid-api/graph/resolvers/array/array.service.spec.ts
  • pnpm --filter ./api type-check

Closes #1943

Summary by CodeRabbit

  • New Features

    • Array startup now supports providing decryption credentials (password or keyfile) to unlock encrypted disks.
  • Improvements

    • Broadened automatic redaction of encryption-related fields in logs.
    • Command execution logging switched to structured metadata.
    • Stronger input validation to reject invalid passwords/keyfiles and submitting both methods together.
  • Tests

    • Added tests for decryption flows, keyfile handling, validation failures, and log redaction.

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 20, 2026

Walkthrough

Adds support to start encrypted arrays with either a decryption password or a keyfile, including validation, keyfile decoding/writing, emcmd integration, expanded logging redaction, and tests for success and failure scenarios.

Changes

Cohort / File(s) Summary
Logging
api/src/core/log.ts, api/src/__test__/core/log.test.ts
Exported LOG_REDACT_PATHS and expanded redaction globs to include *.luksKey, *.luksKeyfile, *.decryptionPassword, *.decryptionKeyfile; added test ensuring those fields are redacted from pino output.
Client Logging
api/src/core/utils/clients/emcmd.ts
Switched emcmd invocation logging from stringified inline text to structured logging ({ commands }) with a fixed message.
GraphQL Model
api/src/unraid-api/graph/resolvers/array/array.model.ts
Added nullable decryptionPassword and decryptionKeyfile fields to ArrayStateInput with validation and descriptions.
Service Implementation & Tests
api/src/unraid-api/graph/resolvers/array/array.service.ts, api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
Extended updateArrayState to accept decryption inputs; added ASCII/base64 validation, data-URL decoding, keyfile write flow (mkdir + writeFile with 0o600), rejection of both inputs simultaneously, and emcmd population with either luksKey or luksKeyfile; tests mock fs, extend emhttp state, and cover success and multiple failure cases.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant GraphQL as "GraphQL API"
    participant Service as "ArrayService"
    participant FS as "FileSystem"
    participant Emcmd as "emcmd"

    Client->>GraphQL: updateArrayState(START, decryptionPassword|decryptionKeyfile)
    GraphQL->>Service: updateArrayState(desiredState, decryption inputs)

    alt decryptionPassword provided
        Service->>Service: validate printable ASCII
        Service->>Service: base64-encode password
        Service->>Emcmd: execute(command with luksKey=<encoded>)
    else decryptionKeyfile provided
        Service->>Service: decode data: URL or base64 payload
        Service->>FS: mkdir(keyfile directory)
        Service->>FS: writeFile(path, decoded, mode=0o600)
        Service->>Emcmd: execute(command with luksKeyfile=<path>)
    end

    Emcmd-->>Service: execution result
    Service-->>GraphQL: updated UnraidArray
    GraphQL-->>Client: response
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰
I nibble bytes and hide the keys away,
Base64 or data-URL, I handle your tray,
I write with care (0o600 neat),
Call emcmd and never leak a beat,
Hop — the array wakes, secure and gay.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'feat(api): support encrypted array start inputs' accurately describes the main change: adding support for encrypted array start inputs (decryption password/keyfile).
Linked Issues check ✅ Passed The PR fully addresses issue #1943 by extending ArrayStateInput with decryptionPassword and decryptionKeyfile fields, enabling array start with decryption credentials as requested.
Out of Scope Changes check ✅ Passed All changes are directly related to the linked issue objective. Log redaction updates and structured logging changes support the security requirement of not exposing decryption inputs in debug logs.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch codex/issue-1943-array-decryption-password

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 64208ae29a

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

@codecov
Copy link

codecov bot commented Mar 20, 2026

Codecov Report

❌ Patch coverage is 92.10526% with 9 lines in your changes missing coverage. Please review.
✅ Project coverage is 51.60%. Comparing base (f0241a8) to head (e1726ea).
⚠️ Report is 3 commits behind head on main.

Files with missing lines Patch % Lines
.../unraid-api/graph/resolvers/array/array.service.ts 90.12% 8 Missing ⚠️
api/src/core/utils/clients/emcmd.ts 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1944      +/-   ##
==========================================
+ Coverage   51.51%   51.60%   +0.09%     
==========================================
  Files        1025     1025              
  Lines       70460    70597     +137     
  Branches     7793     7836      +43     
==========================================
+ Hits        36297    36433     +136     
- Misses      34040    34041       +1     
  Partials      123      123              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
api/src/unraid-api/graph/resolvers/array/array.service.ts (1)

153-176: ⚠️ Potential issue | 🔴 Critical

Don’t acquire pendingState before the decryption preflight can throw.

pendingState is set before the mutual-exclusion check, password encoding, and keyfile write, but the finally that clears it only starts around emcmd(). If any of those earlier steps fail, this singleton service stays stuck in “Array state is still being updated” for every later updateArrayState() call.

🔧 Suggested fix
-        // Set lock then start/stop array
-        this.pendingState = newPendingState;
         const command: Record<string, string> = {
             [`cmd${capitalCase(desiredState)}`]: capitalCase(desiredState),
             startState: constantCase(startState),
         };
 
         if (decryptionPassword && decryptionKeyfile) {
             throw new BadRequestException(
                 new AppError('Provide either a decryption password or a decryption keyfile, not both.')
             );
         }
 
-        if (desiredState === ArrayStateInputState.START && decryptionPassword) {
-            command.luksKey = this.encodeDecryptionPassword(decryptionPassword);
-        }
-
-        if (desiredState === ArrayStateInputState.START && decryptionKeyfile) {
-            await this.writeDecryptionKeyfile(decryptionKeyfile);
-        }
-
         try {
+            this.pendingState = newPendingState;
+
+            if (desiredState === ArrayStateInputState.START && decryptionPassword) {
+                command.luksKey = this.encodeDecryptionPassword(decryptionPassword);
+            }
+
+            if (desiredState === ArrayStateInputState.START && decryptionKeyfile) {
+                await this.writeDecryptionKeyfile(decryptionKeyfile);
+            }
+
             await emcmd(command);
         } finally {
             this.pendingState = null;
         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/array/array.service.ts` around lines 153 -
176, pendingState is set too early (this.pendingState = newPendingState) before
the mutual-exclusion check and decryption preflight (the if that throws,
encodeDecryptionPassword, and writeDecryptionKeyfile), so an exception there
leaves the singleton stuck; move the assignment of pendingState
(newPendingState) to immediately before calling emcmd (i.e., after the
mutual-exclusion check, after calling encodeDecryptionPassword and
writeDecryptionKeyfile), keep the try { await emcmd(command); } finally {
this.pendingState = null; } around emcmd so cleanup still occurs, and ensure no
early return/throw happens before pendingState is set.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@api/src/unraid-api/graph/resolvers/array/array.model.ts`:
- Around line 201-218: The GraphQL input type ArrayStateInput was updated to
include decryptionPassword and decryptionKeyfile but the generated client types
are stale; regenerate the GraphQL artifacts so the generated types include these
new fields. Run your GraphQL codegen (the project’s generation script / codegen
config) to re-generate the client files (the generated graphql.ts artifacts used
by the API and web clients), then verify ArrayStateInput in the generated types
includes decryptionPassword and decryptionKeyfile and adjust any call sites
using that input if needed.

In `@api/src/unraid-api/graph/resolvers/array/array.service.ts`:
- Around line 47-59: The data URL branch using dataUrlMatch/meta/payload
currently calls Buffer.from(payload, 'base64') without validating the base64
payload; reuse the same base64 validation used for raw payloads (the
/^[A-Za-z0-9+/]+={0,2}$/ style check used where raw payloads are decoded) to
verify payload before decoding, and if it fails throw the existing
BadRequestException(new AppError(...)); apply this validation path in the
;base64 branch prior to calling Buffer.from(payload, 'base64') so malformed
data:...;base64,... inputs are rejected instead of producing silent corruption.

---

Outside diff comments:
In `@api/src/unraid-api/graph/resolvers/array/array.service.ts`:
- Around line 153-176: pendingState is set too early (this.pendingState =
newPendingState) before the mutual-exclusion check and decryption preflight (the
if that throws, encodeDecryptionPassword, and writeDecryptionKeyfile), so an
exception there leaves the singleton stuck; move the assignment of pendingState
(newPendingState) to immediately before calling emcmd (i.e., after the
mutual-exclusion check, after calling encodeDecryptionPassword and
writeDecryptionKeyfile), keep the try { await emcmd(command); } finally {
this.pendingState = null; } around emcmd so cleanup still occurs, and ensure no
early return/throw happens before pendingState is set.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: d1618bda-56d1-4d9d-9261-cb5b94d74de2

📥 Commits

Reviewing files that changed from the base of the PR and between f0241a8 and 5c20920.

📒 Files selected for processing (5)
  • api/src/core/log.ts
  • api/src/core/utils/clients/emcmd.ts
  • api/src/unraid-api/graph/resolvers/array/array.model.ts
  • api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
  • api/src/unraid-api/graph/resolvers/array/array.service.ts

@github-actions
Copy link
Contributor

This plugin has been deployed to Cloudflare R2 and is available for testing.
Download it at this URL:

https://preview.dl.unraid.net/unraid-api/tag/PR1944/dynamix.unraid.net.plg

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
api/src/unraid-api/graph/resolvers/array/array.service.ts (1)

166-172: Consider validating decryption inputs aren't provided with STOP.

If a caller mistakenly provides decryptionPassword or decryptionKeyfile when desiredState is STOP, the inputs are silently ignored. Throwing an explicit error would provide clearer feedback:

🔧 Suggested validation
         command.luksKey = this.encodeDecryptionPassword(decryptionPassword);
     }

     if (desiredState === ArrayStateInputState.START && decryptionKeyfile) {
         await this.writeDecryptionKeyfile(decryptionKeyfile);
     }

+    if (desiredState === ArrayStateInputState.STOP && (decryptionPassword || decryptionKeyfile)) {
+        throw new BadRequestException(
+            new AppError('Decryption credentials are only applicable when starting the array.')
+        );
+    }
+
     this.pendingState = newPendingState;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@api/src/unraid-api/graph/resolvers/array/array.service.ts` around lines 166 -
172, When desiredState === ArrayStateInputState.STOP and either
decryptionPassword or decryptionKeyfile is provided, add explicit validation to
throw an error instead of silently ignoring them: check (desiredState ===
ArrayStateInputState.STOP && (decryptionPassword || decryptionKeyfile)) and
throw a descriptive Error (or a domain-specific error) indicating decryption
inputs are only valid for START; keep the existing calls to
encodeDecryptionPassword and writeDecryptionKeyfile for the START branch
(methods referenced: encodeDecryptionPassword, writeDecryptionKeyfile, and the
desiredState/ArrayStateInputState checks) so this validation runs before those
START-specific blocks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@api/src/unraid-api/graph/resolvers/array/array.service.ts`:
- Around line 166-172: When desiredState === ArrayStateInputState.STOP and
either decryptionPassword or decryptionKeyfile is provided, add explicit
validation to throw an error instead of silently ignoring them: check
(desiredState === ArrayStateInputState.STOP && (decryptionPassword ||
decryptionKeyfile)) and throw a descriptive Error (or a domain-specific error)
indicating decryption inputs are only valid for START; keep the existing calls
to encodeDecryptionPassword and writeDecryptionKeyfile for the START branch
(methods referenced: encodeDecryptionPassword, writeDecryptionKeyfile, and the
desiredState/ArrayStateInputState checks) so this validation runs before those
START-specific blocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 8f9589a3-71c5-43d5-a1ef-f29b61f1e0a0

📥 Commits

Reviewing files that changed from the base of the PR and between af33c1b and e1726ea.

📒 Files selected for processing (2)
  • api/src/unraid-api/graph/resolvers/array/array.service.spec.ts
  • api/src/unraid-api/graph/resolvers/array/array.service.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/src/unraid-api/graph/resolvers/array/array.service.spec.ts

@elibosley elibosley merged commit 018a8d5 into main Mar 21, 2026
13 checks passed
@elibosley elibosley deleted the codex/issue-1943-array-decryption-password branch March 21, 2026 02:46
elibosley pushed a commit that referenced this pull request Mar 23, 2026
🤖 I have created a release *beep* *boop*
---


## [4.31.0](v4.30.1...v4.31.0)
(2026-03-23)


### Features

* **api:** support encrypted array start inputs
([#1944](#1944))
([018a8d5](018a8d5))
* **onboarding:** add shared loading states
([#1945](#1945))
([776c8cc](776c8cc))
* Serverside state for onboarding display
([#1936](#1936))
([682d51c](682d51c))


### Bug Fixes

* **api:** reconcile emhttp state without spinning disks
([#1946](#1946))
([d3e0b95](d3e0b95))
* **onboarding:** auto-open incomplete onboarding on 7.3+
([#1940](#1940))
([f0241a8](f0241a8))
* **onboarding:** replace internal boot native selects
([#1942](#1942))
([d6ea032](d6ea032))
* preserve onboarding resume state on reload
([#1941](#1941))
([91f7fe9](91f7fe9))
* recover VM availability after reconnect
([#1947](#1947))
([e064de7](e064de7))
* Unify callback server payloads
([#1938](#1938))
([f58fcc0](f58fcc0))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

New API: Start array with decryption password

1 participant