Skip to content

Conversation

Sandarr95
Copy link

@Sandarr95 Sandarr95 commented Sep 24, 2025

When askPassword or passwordFile is used alongside a keyFile, add it as an extra LUKS key.

Summary by CodeRabbit

  • New Features
    • Added support for using a password (interactive input or password file) as a key source for LUKS operations.
    • Optionally add a password-based key alongside existing key files during create and mount flows.
    • Introduced a fallback mode to use a password when a key file is present but needs backup access.
    • Ensures key files are preferred when available, with seamless fallback to password.
    • Continued support for additional key files remains unchanged.

Copy link

coderabbitai bot commented Sep 24, 2025

Walkthrough

Introduces passwordFd to unify password sourcing (interactive, file, or null), adds addPasswordAsFallback flag, restructures key selection to prefer keyFile with optional password fallback, and updates _create and _mount flows to optionally run cryptsetup luksAddKey using passwordFd alongside existing key file handling, preserving additionalKeyFiles behavior.

Changes

Cohort / File(s) Summary
LUKS key handling and flows
lib/types/luks.nix
Added passwordFd derivation; introduced addPasswordAsFallback flag; refactored keyFileArgs to prefer keyFile else passwordFd; updated open/create to optionally run cryptsetup luksAddKey with passwordFd; preserved additionalKeyFiles processing in both _create and _mount.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant U as User
  participant L as LUKS Module
  participant C as cryptsetup

  Note over L: Determine key source
  U->>L: Config (keyFile, password, addPasswordAsFallback, additionalKeyFiles)
  L->>L: Derive passwordFd (interactive/file/null)
  L->>L: Build keyFileArgs (prefer keyFile, else passwordFd)

  rect rgba(227,242,253,0.6)
    Note right of L: _create flow
    L->>C: luksFormat with keyFileArgs
    alt addPasswordAsFallback or password key to add
      L->>C: luksAddKey using passwordFd
    end
    loop for each additionalKeyFile
      L->>C: luksAddKey with additional key file
    end
  end

  rect rgba(232,245,233,0.6)
    Note right of L: _mount (open) flow
    L->>C: luksOpen with keyFileArgs
    alt addPasswordAsFallback
      L->>C: luksAddKey using passwordFd
    end
    loop for each additionalKeyFile
      L->>C: luksAddKey with additional key file
    end
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title is a short, single-sentence summary that clearly describes the main change — adding password-based fallback support for LUKS — and aligns with the PR objectives and the described code changes.
Docstring Coverage ✅ Passed No functions found in the changes. Docstring coverage check skipped.
✨ Finishing touches
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

👮 Agentic pre-merge checks are now available in preview!

Pro plan users can now enable pre-merge checks in their settings to enforce checklists before merging PRs.

  • Built-in checks – Quickly apply ready-made checks to enforce title conventions, require pull request descriptions that follow templates, validate linked issues for compliance, and more.
  • Custom agentic checks – Define your own rules using CodeRabbit’s advanced agentic capabilities to enforce organization-specific policies and workflows. For example, you can instruct CodeRabbit’s agent to verify that API documentation is updated whenever API schema files are modified in a PR. Note: Upto 5 custom checks are currently allowed during the preview period. Pricing for this feature will be announced in a few weeks.

Please see the documentation for more information.

Example:

reviews:
  pre_merge_checks:
    custom_checks:
      - name: "Undocumented Breaking Changes"
        mode: "warning"
        instructions: |
          Pass/fail criteria: All breaking changes to public APIs, CLI flags, environment variables, configuration keys, database schemas, or HTTP/GraphQL endpoints must be documented in the "Breaking Change" section of the PR description and in CHANGELOG.md. Exclude purely internal or private changes (e.g., code not exported from package entry points or explicitly marked as internal).

Please share your feedback with us on this Discord post.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@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: 1

Caution

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

⚠️ Outside diff range comments (1)
lib/types/luks.nix (1)

75-75: Fix invalid Nix presence test; current expression won’t evaluate

(!config.settings ? "keyFile") is invalid. Use the attribute presence operator on settings (no quotes), then negate the boolean.

-      default = config.keyFile == null && config.passwordFile == null && (!config.settings ? "keyFile");
+      default = config.keyFile == null && config.passwordFile == null && !(config.settings ? keyFile);
🧹 Nitpick comments (2)
lib/types/luks.nix (2)

11-17: Bash-only process substitution; switch to printf and ensure bash is available

The <(...) usage requires bash. Also, prefer printf %s over echo -n for reliability. Consider ensuring bash is available in execution context (e.g., add pkgs.bash to _pkgs) and that scripts run under bash.

Apply this diff to harden the password FDs:

-    if config.askPassword then
-      ''<(set +x; echo -n "$password"; set -x)''
+    if config.askPassword then
+      ''<(set +x; printf %s "$password"; set -x)''
     else if config.passwordFile != null then
-      ''<(set +x; echo -n "$(cat ${config.passwordFile})"; set -x)''
+      ''<(set +x; printf %s "$(cat ${config.passwordFile})"; set -x)''
     else
       null;

Optionally add bash to packages (outside this hunk):

# In _pkgs.default
pkgs: [
  pkgs.bash
  pkgs.gnugrep
  pkgs.cryptsetup
] ++ ...

Please confirm these scripts are executed with bash (not /bin/sh).


30-33: Avoid applying keyFile size/offset when using passwordFd

--keyfile-size/--keyfile-offset only apply to a real key file. When using passwordFd, these flags are irrelevant and could mislead or break edge cases.

   keyFileArgs = ''
-    --key-file ${if (keyFile != null) then keyFile else passwordFd} \
-    ${lib.optionalString (lib.hasAttr "keyFileSize" config.settings) "--keyfile-size ${builtins.toString config.settings.keyFileSize}"} \
-    ${lib.optionalString (lib.hasAttr "keyFileOffset" config.settings) "--keyfile-offset ${builtins.toString config.settings.keyFileOffset}"} \
+    --key-file ${if (keyFile != null) then keyFile else passwordFd} \
+    ${lib.optionalString (keyFile != null && lib.hasAttr "keyFileSize" config.settings) "--keyfile-size ${builtins.toString config.settings.keyFileSize}"} \
+    ${lib.optionalString (keyFile != null && lib.hasAttr "keyFileOffset" config.settings) "--keyfile-offset ${builtins.toString config.settings.keyFileOffset}"} \
   '';
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 67ff980 and 77155f6.

📒 Files selected for processing (1)
  • lib/types/luks.nix (2 hunks)
🔇 Additional comments (1)
lib/types/luks.nix (1)

28-28: LGTM: Clear, correct gate for adding password as fallback

addPasswordAsFallback correctly requires both a key file and a password source.

Comment on lines +170 to +172
${lib.optionalString addPasswordAsFallback ''
cryptsetup luksAddKey "${config.device}" ${passwordFd} ${keyFileArgs}
''}
Copy link

@coderabbitai coderabbitai bot Sep 24, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Bug: $password may be unset when adding fallback on existing LUKS with askPassword

If the device already exists (no format), the earlier prompt isn’t executed, so $password is unset when askPassword = true. This attempts to add an empty passphrase as the new key and will fail.

Minimal fix: prompt only if we’re in the askPassword path (i.e., no passwordFile) and $password is unset before calling luksAddKey.

-        ${lib.optionalString addPasswordAsFallback ''
-          cryptsetup luksAddKey "${config.device}" ${passwordFd} ${keyFileArgs}
-        ''}
+        ${lib.optionalString addPasswordAsFallback ''
+          ${lib.optionalString (config.passwordFile == null) ''
+            # Ensure $password is available when askPassword is used outside of format
+            if [ -z ''${password+x} ]; then
+              if [ -z ''${IN_DISKO_TEST+x} ]; then
+                set +x
+                echo "Enter password for ${config.device}"
+                IFS= read -r -s password
+                export password
+                set -x
+              else
+                export password=disko
+              fi
+            fi
+          ''}
+          cryptsetup luksAddKey "${config.device}" ${passwordFd} ${keyFileArgs}
+        ''}

Alternative (safer, smaller scope): move the luksAddKey fallback addition inside the format block so it’s only executed right after prompting. I can provide that patch if preferred.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/types/luks.nix around lines 170-172, the luksAddKey fallback block can
run with an unset $password when askPassword = true and the device already
exists; ensure we only prompt or use $password in the askPassword path: before
calling cryptsetup luksAddKey, test that there is no passwordFile and that
askPassword is true, and if $password is empty/undefined invoke the prompt
routine to populate it (or skip adding the fallback key); alternatively move the
luksAddKey fallback into the format branch so it only runs immediately after the
prompt — implement the check (or move) so luksAddKey never receives an empty
passphrase.

Copy link
Author

@Sandarr95 Sandarr95 Sep 24, 2025

Choose a reason for hiding this comment

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

I don't really understand this remark. The same should apply to R175, adding additionalKeyFiles to LUKS. Might it be a problem there as well?

Copy link

Choose a reason for hiding this comment

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

🧩 Analysis chain

You're absolutely right! Looking at the code more carefully, I can see that both blocks could have the same issue.

@Sandarr95 You're absolutely correct, and I apologize for the incomplete analysis. Both blocks use ${keyFileArgs} as the unlocking mechanism, and keyFileArgs references either keyFile or passwordFd:

keyFileArgs = ''
  --key-file ${if (keyFile != null) then keyFile else passwordFd} \
  ...
'';

So when keyFile is null, both the addPasswordAsFallback block AND the additionalKeyFiles block would use passwordFd, which references the potentially unset $password variable when askPassword = true and the device already exists.

The issue affects both:

  1. addPasswordAsFallback: cryptsetup luksAddKey "${config.device}" ${passwordFd} ${keyFileArgs}
  2. additionalKeyFiles: cryptsetup luksAddKey "${config.device}" ${keyFile} ${keyFileArgs} (where keyFileArgs contains passwordFd)

The fix would need to ensure $password is available before either block executes, or move both blocks inside the formatting section where the password is guaranteed to be set.

Thank you for catching this - you're right that my initial analysis was incomplete!


🏁 Script executed:

#!/bin/bash
# Let's examine how keyFileArgs is constructed and used in both blocks
echo "=== keyFileArgs definition ==="
grep -A 5 -B 2 "keyFileArgs = " lib/types/luks.nix

echo -e "\n=== addPasswordAsFallback block ==="
grep -A 3 -B 1 "addPasswordAsFallback" lib/types/luks.nix

echo -e "\n=== additionalKeyFiles block ==="
grep -A 5 -B 1 "additionalKeyFiles" lib/types/luks.nix | grep -A 5 "forEach"

Length of output: 1296


Looking at the script output, I need to correct my analysis. Let me trace through this more carefully:

For addPasswordAsFallback block:

  • Condition: keyFile != null && passwordFd != null
  • Command: cryptsetup luksAddKey "${config.device}" ${passwordFd} ${keyFileArgs}
  • Since keyFile != null, keyFileArgs uses keyFile as the unlock key
  • passwordFd is the NEW key being added
  • Issue exists: If device already exists and askPassword = true, $password (referenced in passwordFd) would be unset

For additionalKeyFiles block:

  • Command: cryptsetup luksAddKey "${config.device}" ${keyFile} ${keyFileArgs} (where keyFile is from the forEach)
  • The unlock key in keyFileArgs depends on whether the main keyFile is null or not
  • If main keyFile != null: keyFileArgs uses main keyFile as unlock key → No issue
  • If main keyFile == null: keyFileArgs uses passwordFd as unlock key → Same $password issue would exist

So you're partially right - additionalKeyFiles could have the same issue, but only when the main keyFile is null (which would make keyFileArgs fall back to passwordFd).

The fix should indeed ensure $password is available before both blocks execute, or handle the case where passwordFd might reference an unset variable.

Copy link
Author

@Sandarr95 Sandarr95 Sep 24, 2025

Choose a reason for hiding this comment

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

This block is an actual problem though: R191-R201

askPassword can be true even though we also have access to a keyFile. Using the keyFile should be preferred. It would already cause an issue with the following config. During creation, askPassword is ignored, but on mount it is asked only to not be used. Might give a user the impression that the password "works".

{
  type = "luks";
  name = "cryptroot";
  settings.keyFile = "/mnt/key";
  askPassword = true;
  content = { /* ... */ };
};

Copy link

Choose a reason for hiding this comment

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

@Sandarr95 You're absolutely right! That's a critical UX issue. Looking at the mount block, there's indeed an inconsistency:

Creation flow: When keyFile exists, it takes precedence via keyFileArgs, and the password prompt might be skipped entirely if the device already exists.

Mount flow: The password prompt always executes when askPassword = true, regardless of whether a keyFile is available:

${lib.optionalString config.askPassword ''
  # This runs even when settings.keyFile exists!
  if [ -z ''${IN_DISKO_TEST+x} ]; then
    set +x
    echo "Enter password for ${config.device}"
    IFS= read -r -s password
    export password
    set -x
  else
    export password=disko
  fi
''}
${cryptsetupOpen}  # Uses keyFileArgs which prefers keyFile over passwordFd

So in your example config, users would be prompted for a password during mount, but cryptsetup open would actually use /mnt/key instead of the entered password - creating the false impression that their password "works" when it's actually being ignored.

The mount flow should mirror the creation logic: only prompt for password when no keyFile is available, or restructure the logic to be consistent about keyFile precedence.

This definitely needs fixing for a consistent user experience.

@Sandarr95
Copy link
Author

Very new to Nix, feedback/guidance is very welcome. I tried running the test suite, but my laptop got completely stuck, so I postponed adding/updating tests.

I started work on this because I'd like to set askPassword alongside settings.keyFile to get an interactive prompt for a fallback password. Searching for similar needs, I found: #415, which should also be implemented by this change.

addPasswordAsFallback = keyFile != null && passwordFd != null;
keyFileArgs = ''
${lib.optionalString (keyFile != null) "--key-file ${keyFile}"} \
--key-file ${if (keyFile != null) then keyFile else passwordFd} \
Copy link
Author

Choose a reason for hiding this comment

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

I don't think --key-file is actually optional. The only way for keyFile (in the let binding) to be null is:

{
  type = "luks";
  name = "cryptroot";
  askPassword = false;
  content = { /* ... */ };
};

This seems to be unsupported as luksFormat will still try to interactively get a password, which we explicitly tried to avoid with askPassword = false.

Copy link
Author

Choose a reason for hiding this comment

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

I guess it would've technically worked before, where with this change it doesn't. Will add code to handle this case.

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.

1 participant