Skip to content

Conversation

@jyn514
Copy link

@jyn514 jyn514 commented Jun 1, 2025

This fixes the bug in two places:

  1. Don't retry passwords if stdin isn't a tty.
  2. Notice and error out when no input is available when reading a passphrase. Technically this isn't necessary now that we're not retrying, but it avoids needlessly spending time trying to unlock the protector.

This still retries empty passwords when interactive.

Fixes #429

crypto/key.go Outdated
// We got EOF in the middle of a line, before seeing a newline.
// If there is at least some input, that's ok, treat it the same as a newline.
// But if we have read 0 bytes (not even a newline) and hit EOF, something is wrong, so return an error.
if totalBytesRead == 0 {
Copy link
Collaborator

Choose a reason for hiding this comment

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

Shouldn't this just go into the io.EOF case? It shouldn't make a difference whether there is a trailing newline or not, and I don't think empty passphrases are meant to be supported.

If doing it that way, then there is no need for the new io.ErrUnexpectedEOF case (which is also misleadingly named because it is sometimes expected).

Copy link
Author

Choose a reason for hiding this comment

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

oh, nice! if empty passwords aren't supported that makes this much easier, yes.

func unwrapProtectorKey(info ProtectorInfo, keyFn KeyFunc) (*crypto.Key, error) {
retry := false
for {
for i := 0; i < 3; i++ {
Copy link
Collaborator

@ebiggers ebiggers Jun 1, 2025

Choose a reason for hiding this comment

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

It is already supposed to not retry for non-interactive sessions. Either the program is running interactively, in which case there is no need for a limit, or it's running non-interactively in which case it should only try once. So there should be no need for an arbitrary limit such as 3. Perhaps you found a case in which it allows retries when it shouldn't?

Copy link
Author

@jyn514 jyn514 Jun 2, 2025

Choose a reason for hiding this comment

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

I think by "non-interactive" you mean --quiet:

fscrypt/cmd/fscrypt/keys.go

Lines 150 to 151 in 15f711c

// Don't retry for non-interactive sessions
if quietFlag.Value {
. But that is not actually the same as being non-interactive, because a script could run fscrypt without passing quiet. And so in that case we end up with this infinite loop.

If you like I can change quietFlag to default to true if stdin is not a tty? but that might be confusing for existing users; I don't know enough about how fscrypt is used.

Copy link
Collaborator

Choose a reason for hiding this comment

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

Programs typically check whether stdin is a tty or not to decide whether they're running interactively or not. That should be the case here too. It sounds like the issue is that the retry behavior is currently determined by --quiet instead. It should instead be based on whether stdin is a tty.

Copy link
Author

Choose a reason for hiding this comment

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

ok, done :) i also fixed a bug where an empty password wouldn't retry when interactive, but i don't have a way to automatically test it because of the infinite retry behavior. i did test manually that the new code works.

@jyn514 jyn514 force-pushed the no-tty branch 2 times, most recently from 45f7ee2 to fcd80e4 Compare June 5, 2025 03:02
@ebiggers
Copy link
Collaborator

Sorry, I've been busy. I'm taking a look at this again.

If I understand correctly, removing support for empty passwords is actually unnecessary to fix the issue. Would you mind splitting that change into a separate commit, or even a separate pull request?

Also, I tested fscrypt encrypt --quiet, and it does actually accept empty passwords currently, so removing support for them would technically be a breaking change. The use case for an empty password would be people setting a default password. For example, a system administrator could set up a new encrypted directory on behalf of a user using a default password initially, and the user could optionally set a real password later.

Empty passwords aren't actually necessary to support that use case, as people can just choose any other default password, like "default" or whatever. Empty passwords should not have been allowed.

But since they were allowed, I'm not sure we can rule out that someone is using them.

@jyn514
Copy link
Author

jyn514 commented Jun 22, 2025

If I understand correctly, removing support for empty passwords is actually unnecessary to fix the issue. Would you mind splitting that change into a separate commit, or even a separate pull request?

i have removed all the stuff about empty passwords. i don't feel strongly about disallowing empty passwords so i'm not going to open a separate PR.

i do want to note that this is now more prone to the original bug than my original PR—if stdin is a tty, but no input is available (maybe because we're running under expect ), we'll still loop forever trying to read input. i can fix that without disallowing empty passwords, but you didn't like my fix (#430), so i'm not sure what to do there.

@jyn514
Copy link
Author

jyn514 commented Nov 29, 2025

👋 i'd appreciate a review when you have a chance :)

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.

fscrypt unlock </dev/null hangs indefinitely

2 participants