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",