diff --git a/secboot/keys/export_sb_test.go b/secboot/keys/export_sb_test.go
new file mode 100644
index 00000000000..9c8bfdf84cf
--- /dev/null
+++ b/secboot/keys/export_sb_test.go
@@ -0,0 +1,35 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//go:build !nosecboot
+
+/*
+ * Copyright (C) 2024 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 keys
+
+import (
+ "io"
+
+ sb "github.com/snapcore/secboot"
+)
+
+func MockSbNewProtectedKey(f func(rand io.Reader, protectorKey []byte, primaryKey sb.PrimaryKey) (protectedKey *sb.KeyData, primaryKeyOut sb.PrimaryKey, unlockKey sb.DiskUnlockKey, err error)) (restore func()) {
+ oldSbNewProtectedKey := sbNewProtectedKey
+ sbNewProtectedKey = f
+ return func() {
+ sbNewProtectedKey = oldSbNewProtectedKey
+ }
+}
diff --git a/secboot/keys/plainkey.go b/secboot/keys/plainkey.go
new file mode 100644
index 00000000000..00082dc9904
--- /dev/null
+++ b/secboot/keys/plainkey.go
@@ -0,0 +1,85 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//go:build !nosecboot
+
+/*
+ * Copyright (C) 2024 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 keys
+
+import (
+ "crypto/rand"
+ "io"
+ "os"
+ "path/filepath"
+
+ sb "github.com/snapcore/secboot"
+ sb_plainkey "github.com/snapcore/secboot/plainkey"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+const (
+ protectorKeySize = 32
+)
+
+var (
+ sbNewProtectedKey = sb_plainkey.NewProtectedKey
+)
+
+// ProtectorKey is a key that can be used to protect "plainkey" keys.
+type ProtectorKey []byte
+
+// NewProtectorKey creates a new random ProtectorKey.
+func NewProtectorKey() (ProtectorKey, error) {
+ key := make(ProtectorKey, protectorKeySize)
+ _, err := rand.Read(key[:])
+ return key, err
+}
+
+// SaveToFile saves the ProtectorKey to a file at given path.
+func (key ProtectorKey) SaveToFile(path string) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+ return osutil.AtomicWriteFile(path, key[:], 0600, 0)
+}
+
+// PlainKey is a wrapper for a secboot KeyData representing a plainkey.
+type PlainKey struct {
+ keyData *sb.KeyData
+}
+
+// CreateProtectedKey creates a protected key for a given ProtectorKey
+// and primary key. It returns a the protected key wrapped as a PlainKey
+// as well the used primary key and the unlock key.
+// If primaryKey is nil, the primary key will be generated.
+func (key ProtectorKey) CreateProtectedKey(primaryKey []byte) (*PlainKey, []byte, []byte, error) {
+ protectedKey, generatedPK, unlockKey, err := sbNewProtectedKey(rand.Reader, key[:], primaryKey)
+ return &PlainKey{protectedKey}, generatedPK, unlockKey, err
+}
+
+// KeyDataWriter is a the same as KeyDataWriter from
+// github.com/canonical/secboot.
+type KeyDataWriter interface {
+ io.Writer
+ Commit() error
+}
+
+// Write writes a PlainKey to a KeyDataWriter.
+func (key *PlainKey) Write(writer KeyDataWriter) error {
+ return key.keyData.WriteAtomic(writer)
+}
diff --git a/secboot/keys/plainkey_nosb.go b/secboot/keys/plainkey_nosb.go
new file mode 100644
index 00000000000..a86071c9c75
--- /dev/null
+++ b/secboot/keys/plainkey_nosb.go
@@ -0,0 +1,65 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//go:build nosecboot
+
+/*
+ * Copyright (C) 2024 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 keys
+
+import (
+ "crypto/rand"
+ "io"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/osutil"
+)
+
+const (
+ protectorKeySize = 32
+)
+
+type ProtectorKey []byte
+
+func NewProtectorKey() (ProtectorKey, error) {
+ key := make(ProtectorKey, protectorKeySize)
+ _, err := rand.Read(key[:])
+ return key, err
+}
+
+type PlainKey struct {
+}
+
+func (key ProtectorKey) SaveToFile(path string) error {
+ if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
+ return err
+ }
+ return osutil.AtomicWriteFile(path, key[:], 0600, 0)
+}
+
+func (key ProtectorKey) CreateProtectedKey(primaryKey []byte) (*PlainKey, []byte, []byte, error) {
+ return &PlainKey{}, primaryKey, []byte{}, nil
+}
+
+type KeyDataWriter interface {
+ io.Writer
+ Commit() error
+}
+
+func (key *PlainKey) Write(writer KeyDataWriter) error {
+ return writer.Commit()
+}
diff --git a/secboot/keys/plainkey_test.go b/secboot/keys/plainkey_test.go
new file mode 100644
index 00000000000..5b353d87b7d
--- /dev/null
+++ b/secboot/keys/plainkey_test.go
@@ -0,0 +1,121 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+//go:build !nosecboot
+
+/*
+ * Copyright (C) 2024 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 keys_test
+
+import (
+ "bytes"
+ "crypto"
+ "io"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ sb "github.com/snapcore/secboot"
+
+ "github.com/snapcore/snapd/secboot/keys"
+)
+
+type plainkeySuite struct {
+}
+
+var _ = Suite(&plainkeySuite{})
+
+func (s *plainkeySuite) SetUpTest(c *C) {
+}
+
+type MyKeyDataWriter struct {
+ *bytes.Buffer
+}
+
+func NewMyKeyDataWriter() *MyKeyDataWriter {
+ return &MyKeyDataWriter{
+ Buffer: bytes.NewBuffer([]byte{}),
+ }
+}
+
+func (kdw *MyKeyDataWriter) Commit() error {
+ return nil
+}
+
+type testCase struct {
+ nilPrimaryKey bool
+}
+
+func (s *plainkeySuite) testPlainKey(c *C, tc *testCase) {
+ restore := keys.MockSbNewProtectedKey(func(rand io.Reader, protectorKey []byte, primaryKey sb.PrimaryKey) (protectedKey *sb.KeyData, primaryKeyOut sb.PrimaryKey, unlockKey sb.DiskUnlockKey, err error) {
+ if tc.nilPrimaryKey {
+ c.Check(primaryKey, IsNil)
+ primaryKeyOut = []byte("generated-primary-key")
+ } else {
+ c.Check(primaryKey, NotNil)
+ primaryKeyOut = primaryKey
+ }
+ kd, err := sb.NewKeyData(&sb.KeyParams{
+ Handle: nil,
+ Role: "run",
+ PlatformName: "fakePlatform",
+ KDFAlg: crypto.SHA256,
+ })
+ c.Assert(err, IsNil)
+ return kd, primaryKeyOut, []byte("unlock-key"), nil
+ })
+ defer restore()
+
+ protectorKey, err := keys.NewProtectorKey()
+ c.Assert(err, IsNil)
+ var primaryKeyIn []byte
+ if !tc.nilPrimaryKey {
+ primaryKeyIn = []byte("primary-in")
+ }
+ protectedKey, primaryKeyOut, unlockKey, err := protectorKey.CreateProtectedKey(primaryKeyIn)
+ c.Assert(err, IsNil)
+ if tc.nilPrimaryKey {
+ c.Check(primaryKeyOut, DeepEquals, []byte("generated-primary-key"))
+ } else {
+ c.Check(primaryKeyOut, DeepEquals, []byte("primary-in"))
+ }
+ c.Check(unlockKey, DeepEquals, []byte("unlock-key"))
+
+ kdw := NewMyKeyDataWriter()
+ protectedKey.Write(kdw)
+
+ c.Check(string(kdw.Bytes()), Equals, `{"generation":2,"platform_name":"fakePlatform","platform_handle":null,"role":"run","kdf_alg":"sha256","encrypted_payload":null}`+"\n")
+
+ root := c.MkDir()
+
+ path := filepath.Join(root, "somedir", "somefile")
+ err = protectorKey.SaveToFile(path)
+ c.Assert(err, IsNil)
+ savedKey, err := os.ReadFile(path)
+ c.Assert(err, IsNil)
+ c.Check(savedKey, DeepEquals, []byte(protectorKey))
+}
+
+func (s *plainkeySuite) TestPlainKey(c *C) {
+ s.testPlainKey(c, &testCase{})
+}
+
+func (s *plainkeySuite) TestPlainKeyNilPrimaryKeyIn(c *C) {
+ s.testPlainKey(c, &testCase{
+ nilPrimaryKey: true,
+ })
+}