diff --git a/auth_requestor_plymouth.go b/auth_requestor_plymouth.go new file mode 100644 index 00000000..187f4dd8 --- /dev/null +++ b/auth_requestor_plymouth.go @@ -0,0 +1,82 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package secboot + +import ( + "bytes" + "context" + "errors" + "fmt" + "io" + "os" + "os/exec" +) + +// PlymouthAuthRequestorStringer is used by the Plymouth implementation +// of [AuthRequestor] to obtain translated strings. +type PlymouthAuthRequestorStringer interface { + // RequestUserCredentialFormatString returns a format string used by + // RequestUserCredential to construct a message that is used to request + // credentials with the supplied auth types. The returned format string + // is interpreted with the following parameters: + // - %[1]s: A human readable name for the storage container. + // - %[2]s: The path of the encrypted storage container. + RequestUserCredentialFormatString(authTypes UserAuthType) (string, error) +} + +type plymouthAuthRequestor struct { + stringer PlymouthAuthRequestorStringer +} + +func (r *plymouthAuthRequestor) RequestUserCredential(ctx context.Context, name, path string, authTypes UserAuthType) (string, error) { + fmtString, err := r.stringer.RequestUserCredentialFormatString(authTypes) + if err != nil { + return "", fmt.Errorf("cannot request format string for requested auth types: %w", err) + } + msg := fmt.Sprintf(fmtString, name, path) + + cmd := exec.CommandContext( + ctx, "plymouth", "ask-for-password", + "--prompt", msg) + out := new(bytes.Buffer) + cmd.Stdout = out + cmd.Stdin = os.Stdin + if err := cmd.Run(); err != nil { + return "", fmt.Errorf("cannot execute plymouth ask-for-password: %w", err) + } + result, err := io.ReadAll(out) + if err != nil { + // The only error returned from bytes.Buffer.Read should be io.EOF, + // which io.ReadAll filters out. + return "", fmt.Errorf("unexpected error: %w", err) + } + return string(result), nil +} + +// NewPlymouthAuthRequestor creates an implementation of AuthRequestor that +// communicates directly with Plymouth. +func NewPlymouthAuthRequestor(stringer PlymouthAuthRequestorStringer) (AuthRequestor, error) { + if stringer == nil { + return nil, errors.New("must supply an implementation of PlymouthAuthRequestorStringer") + } + return &plymouthAuthRequestor{ + stringer: stringer, + }, nil +} diff --git a/auth_requestor_plymouth_test.go b/auth_requestor_plymouth_test.go new file mode 100644 index 00000000..436fc927 --- /dev/null +++ b/auth_requestor_plymouth_test.go @@ -0,0 +1,258 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2026 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package secboot_test + +import ( + "context" + "errors" + "fmt" + "io/ioutil" + "path/filepath" + + "github.com/snapcore/secboot/internal/testutil" + snapd_testutil "github.com/snapcore/snapd/testutil" + + . "gopkg.in/check.v1" + + . "github.com/snapcore/secboot" +) + +type authRequestorPlymouthSuite struct { + snapd_testutil.BaseTest + + passwordFile string + mockPlymouth *snapd_testutil.MockCmd +} + +func (s *authRequestorPlymouthSuite) SetUpTest(c *C) { + dir := c.MkDir() + s.passwordFile = filepath.Join(dir, "password") // password to be returned by the mock plymouth + + plymouthBottom := `cat %[1]s` + s.mockPlymouth = snapd_testutil.MockCommand(c, "plymouth", fmt.Sprintf(plymouthBottom, s.passwordFile)) + s.AddCleanup(s.mockPlymouth.Restore) +} + +func (s *authRequestorPlymouthSuite) setPassphrase(c *C, passphrase string) { + c.Assert(ioutil.WriteFile(s.passwordFile, []byte(passphrase), 0600), IsNil) +} + +var _ = Suite(&authRequestorPlymouthSuite{}) + +type mockPlymouthAuthRequestorStringer struct { + rucErr error +} + +func (s *mockPlymouthAuthRequestorStringer) RequestUserCredentialFormatString(authType UserAuthType) (string, error) { + if s.rucErr != nil { + return "", s.rucErr + } + + switch authType { + case UserAuthTypePassphrase: + return "Enter passphrase for %[1]s (%[2]s):", nil + case UserAuthTypePIN: + return "Enter PIN for %[1]s (%[2]s):", nil + case UserAuthTypeRecoveryKey: + return "Enter recovery key for %[1]s (%[2]s):", nil + case UserAuthTypePassphrase | UserAuthTypePIN: + return "Enter passphrase or PIN for %[1]s (%[2]s):", nil + case UserAuthTypePassphrase | UserAuthTypeRecoveryKey: + return "Enter passphrase or recovery key for %[1]s (%[2]s):", nil + case UserAuthTypePIN | UserAuthTypeRecoveryKey: + return "Enter PIN or recovery key for %[1]s (%[2]s):", nil + case UserAuthTypePassphrase | UserAuthTypePIN | UserAuthTypeRecoveryKey: + return "Enter passphrase, PIN or recovery key for %[1]s (%[2]s):", nil + default: + return "", errors.New("unexpected UserAuthType") + } +} + +type testPlymouthRequestUserCredentialsParams struct { + passphrase string + + ctx context.Context + name string + path string + authTypes UserAuthType + + expectedMsg string +} + +func (s *authRequestorPlymouthSuite) testRequestUserCredential(c *C, params *testPlymouthRequestUserCredentialsParams) { + s.setPassphrase(c, params.passphrase) + + requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer)) + c.Assert(err, IsNil) + + passphrase, err := requestor.RequestUserCredential(params.ctx, params.name, params.path, params.authTypes) + c.Check(err, IsNil) + c.Check(passphrase, Equals, params.passphrase) + + c.Check(s.mockPlymouth.Calls(), HasLen, 1) + c.Check(s.mockPlymouth.Calls()[0], DeepEquals, []string{"plymouth", "ask-for-password", "--prompt", params.expectedMsg}) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphrase(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseDifferentPassphrase(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "1234", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialDifferentPath(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/nvme0n1p1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for data (/dev/nvme0n1p1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseDifferentName(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "foo", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase, + expectedMsg: "Enter passphrase for foo (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPIN(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "1234", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePIN, + expectedMsg: "Enter PIN for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypeRecoveryKey, + expectedMsg: "Enter recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseOrPIN(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "1234", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase | UserAuthTypePIN, + expectedMsg: "Enter passphrase or PIN for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseOrRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase | UserAuthTypeRecoveryKey, + expectedMsg: "Enter passphrase or recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPINOrRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePIN | UserAuthTypeRecoveryKey, + expectedMsg: "Enter PIN or recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialPassphraseOrPINOrRecoveryKey(c *C) { + s.testRequestUserCredential(c, &testPlymouthRequestUserCredentialsParams{ + passphrase: "password", + ctx: context.Background(), + name: "data", + path: "/dev/sda1", + authTypes: UserAuthTypePassphrase | UserAuthTypePIN | UserAuthTypeRecoveryKey, + expectedMsg: "Enter passphrase, PIN or recovery key for data (/dev/sda1):", + }) +} + +func (s *authRequestorPlymouthSuite) TestNewRequestorNoStringer(c *C) { + _, err := NewPlymouthAuthRequestor(nil) + c.Check(err, ErrorMatches, `must supply an implementation of PlymouthAuthRequestorStringer`) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialObtainFormatStringError(c *C) { + requestor, err := NewPlymouthAuthRequestor(&mockPlymouthAuthRequestorStringer{ + rucErr: errors.New("some error"), + }) + c.Assert(err, IsNil) + + _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase) + c.Check(err, ErrorMatches, `cannot request format string for requested auth types: some error`) +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialFailure(c *C) { + requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer)) + c.Assert(err, IsNil) + + _, err = requestor.RequestUserCredential(context.Background(), "data", "/dev/sda1", UserAuthTypePassphrase) + c.Check(err, ErrorMatches, "cannot execute plymouth ask-for-password: exit status 1") +} + +func (s *authRequestorPlymouthSuite) TestRequestUserCredentialCanceledContext(c *C) { + s.setPassphrase(c, "foo") + + requestor, err := NewPlymouthAuthRequestor(new(mockPlymouthAuthRequestorStringer)) + c.Assert(err, IsNil) + + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + _, err = requestor.RequestUserCredential(ctx, "data", "/dev/sda1", UserAuthTypePassphrase) + c.Check(err, ErrorMatches, "cannot execute plymouth ask-for-password: context canceled") + c.Check(errors.Is(err, context.Canceled), testutil.IsTrue) +} diff --git a/auth_requestor_systemd.go b/auth_requestor_systemd.go index 39cac1cf..8078262b 100644 --- a/auth_requestor_systemd.go +++ b/auth_requestor_systemd.go @@ -61,9 +61,9 @@ func (r *systemdAuthRequestor) RequestUserCredential(ctx context.Context, name, } // NewSystemdAuthRequestor creates an implementation of AuthRequestor that -// delegates to the systemd-ask-password binary. The caller supplies a map -// of user auth type combinations to format strings that are used to construct -// messages. The format strings are interpreted with the following parameters: +// delegates to the systemd-ask-password binary. The caller supplies a callback +// to map user auth type combinations to format strings that are used to +// messages.The format strings are interpreted with the following parameters: // - %[1]s: A human readable name for the storage container. // - %[2]s: The path of the encrypted storage container. func NewSystemdAuthRequestor(formatStringFn func(UserAuthType) (string, error)) (AuthRequestor, error) { diff --git a/auth_requestor_systemd_test.go b/auth_requestor_systemd_test.go index 5f4d9e17..7ab5149d 100644 --- a/auth_requestor_systemd_test.go +++ b/auth_requestor_systemd_test.go @@ -57,7 +57,7 @@ func (s *authRequestorSystemdSuite) setPassphrase(c *C, passphrase string) { var _ = Suite(&authRequestorSystemdSuite{}) -type testRequestUserCredentialParams struct { +type testSystemdRequestUserCredentialsParams struct { passphrase string ctx context.Context @@ -68,7 +68,7 @@ type testRequestUserCredentialParams struct { expectedMsg string } -func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *testRequestUserCredentialParams) { +func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *testSystemdRequestUserCredentialsParams) { s.setPassphrase(c, params.passphrase) requestor, err := NewSystemdAuthRequestor(func(authType UserAuthType) (string, error) { @@ -103,7 +103,7 @@ func (s *authRequestorSystemdSuite) testRequestUserCredential(c *C, params *test } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphrase(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data", @@ -114,7 +114,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphrase(c *C) { } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferentPassphrase(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "1234", ctx: context.Background(), name: "data", @@ -125,7 +125,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferent } func (s *authRequestorSystemdSuite) TestRequestUserCredentialDifferentPath(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data", @@ -136,7 +136,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialDifferentPath(c *C) } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferentName(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "foo", @@ -147,7 +147,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseDifferent } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPIN(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "1234", ctx: context.Background(), name: "data", @@ -158,7 +158,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPIN(c *C) { } func (s *authRequestorSystemdSuite) TestRequestUserCredentialRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", ctx: context.Background(), name: "data", @@ -169,7 +169,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialRecoveryKey(c *C) { } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrPIN(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "1234", ctx: context.Background(), name: "data", @@ -180,7 +180,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrPIN(c * } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data", @@ -191,7 +191,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrRecover } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPINOrRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "00000-11111-22222-33333-44444-55555-00000-11111", ctx: context.Background(), name: "data", @@ -202,7 +202,7 @@ func (s *authRequestorSystemdSuite) TestRequestUserCredentialPINOrRecoveryKey(c } func (s *authRequestorSystemdSuite) TestRequestUserCredentialPassphraseOrPINOrRecoveryKey(c *C) { - s.testRequestUserCredential(c, &testRequestUserCredentialParams{ + s.testRequestUserCredential(c, &testSystemdRequestUserCredentialsParams{ passphrase: "password", ctx: context.Background(), name: "data",