Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
82 changes: 82 additions & 0 deletions auth_requestor_plymouth.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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
}
258 changes: 258 additions & 0 deletions auth_requestor_plymouth_test.go
Original file line number Diff line number Diff line change
@@ -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 <http://www.gnu.org/licenses/>.
*
*/

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)
}
6 changes: 3 additions & 3 deletions auth_requestor_systemd.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Loading
Loading