diff --git a/.dockerignore b/.dockerignore index 9c55c7b..c4f4f1d 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,11 +1,12 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-08-20T10:44:02Z by kres 7be2a05. +# Generated on 2024-08-22T10:15:10Z by kres 9cc7f48. * !internal !blkid !block +!encryption !partitioning !go.mod !go.sum diff --git a/Dockerfile b/Dockerfile index ebd8983..ede9fd4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,7 +2,7 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-08-20T12:11:08Z by kres 7be2a05. +# Generated on 2024-08-22T10:15:10Z by kres 9cc7f48. ARG TOOLCHAIN @@ -60,6 +60,7 @@ RUN --mount=type=cache,target=/go/pkg go mod verify COPY ./internal ./internal COPY ./blkid ./blkid COPY ./block ./block +COPY ./encryption ./encryption COPY ./partitioning ./partitioning RUN --mount=type=cache,target=/go/pkg go list -mod=readonly all >/dev/null diff --git a/blkid/internal/filesystems/luks/luks.go b/blkid/internal/filesystems/luks/luks.go index 3a83e25..38775fd 100644 --- a/blkid/internal/filesystems/luks/luks.go +++ b/blkid/internal/filesystems/luks/luks.go @@ -15,6 +15,7 @@ import ( "github.com/siderolabs/go-blockdevice/v2/blkid/internal/magic" "github.com/siderolabs/go-blockdevice/v2/blkid/internal/probe" + "github.com/siderolabs/go-blockdevice/v2/internal/luks2" ) var luksMagic = magic.Magic{ @@ -37,13 +38,13 @@ func (p *Probe) Name() string { // Probe runs the further inspection and returns the result if successful. func (p *Probe) Probe(r probe.Reader, _ magic.Magic) (*probe.Result, error) { - buf := make([]byte, LUKS2HEADER_SIZE) + buf := make([]byte, luks2.LUKS2HEADER_SIZE) if _, err := r.ReadAt(buf, 0); err != nil { return nil, err } - hdr := Luks2Header(buf) + hdr := luks2.Luks2Header(buf) if hdr.Get_version() != 2 { return nil, nil //nolint:nilnil diff --git a/encryption/encryption.go b/encryption/encryption.go new file mode 100644 index 0000000..784ff22 --- /dev/null +++ b/encryption/encryption.go @@ -0,0 +1,6 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package encryption provides abstraction level for various disk encryption methods. +package encryption diff --git a/encryption/luks/luks.go b/encryption/luks/luks.go new file mode 100644 index 0000000..f7e0e1a --- /dev/null +++ b/encryption/luks/luks.go @@ -0,0 +1,416 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package luks provides a way to call LUKS2 cryptsetup. +package luks + +import ( + "bytes" + "context" + "encoding/json" + "errors" + "fmt" + "io" + "path/filepath" + "regexp" + "slices" + "strconv" + "strings" + "time" + + "github.com/siderolabs/go-cmd/pkg/cmd" + + "github.com/siderolabs/go-blockdevice/v2/block" + "github.com/siderolabs/go-blockdevice/v2/encryption" + "github.com/siderolabs/go-blockdevice/v2/encryption/token" + "github.com/siderolabs/go-blockdevice/v2/internal/luks2" +) + +// Cipher LUKS2 cipher type. +type Cipher int + +var keySizeDefaults = map[Cipher]uint{ + AESXTSPlain64Cipher: 512, + XChaCha12Cipher: 256, + XChaCha20Cipher: 256, +} + +// String converts to command line string parameter value. +func (c Cipher) String() (string, error) { + switch c { + case AESXTSPlain64Cipher: + return AESXTSPlain64CipherString, nil + case XChaCha12Cipher: + return XChaCha12String, nil + case XChaCha20Cipher: + return XChaCha20String, nil + default: + return "", fmt.Errorf("unknown cipher kind %d", c) + } +} + +// ParseCipherKind converts cipher string into cipher type. +func ParseCipherKind(s string) (Cipher, error) { + switch s { + case "": // default + fallthrough + case AESXTSPlain64CipherString: + return AESXTSPlain64Cipher, nil + case XChaCha12String: + return XChaCha12Cipher, nil + case XChaCha20String: + return XChaCha20Cipher, nil + default: + return 0, fmt.Errorf("unknown cipher kind %s", s) + } +} + +const ( + // AESXTSPlain64CipherString string representation of aes-xts-plain64 cipher. + AESXTSPlain64CipherString = "aes-xts-plain64" + // XChaCha12String string representation of xchacha12 cipher. + XChaCha12String = "xchacha12,aes-adiantum-plain64" + // XChaCha20String string representation of xchacha20 cipher. + XChaCha20String = "xchacha20,aes-adiantum-plain64" + // AESXTSPlain64Cipher represents aes-xts-plain64 encryption cipher. + AESXTSPlain64Cipher Cipher = iota + // XChaCha12Cipher represents xchacha12 encryption cipher. + XChaCha12Cipher + // XChaCha20Cipher represents xchacha20 encryption cipher. + XChaCha20Cipher +) + +const ( + // PerfNoReadWorkqueue sets --perf-no_read_workqueue. + PerfNoReadWorkqueue = "no_read_workqueue" + // PerfNoWriteWorkqueue sets --perf-no_write_workqueue. + PerfNoWriteWorkqueue = "no_write_workqueue" + // PerfSameCPUCrypt sets --perf-same_cpu_crypt. + PerfSameCPUCrypt = "same_cpu_crypt" +) + +// ValidatePerfOption checks that specified string is a valid perf option. +func ValidatePerfOption(value string) error { + switch value { + case PerfNoReadWorkqueue: + fallthrough + case PerfNoWriteWorkqueue: + fallthrough + case PerfSameCPUCrypt: + return nil + } + + return fmt.Errorf("invalid perf option %v", value) +} + +// LUKS implements LUKS2 encryption provider. +type LUKS struct { + perfOptions []string + cipher Cipher + iterTime time.Duration + pbkdfForceIterations uint + pbkdfMemory uint64 + blockSize uint64 + keySize uint +} + +// New creates new LUKS2 encryption provider. +func New(cipher Cipher, options ...Option) *LUKS { + l := &LUKS{ + cipher: cipher, + } + + for _, option := range options { + option(l) + } + + if l.keySize == 0 { + l.keySize = keySizeDefaults[cipher] + } + + return l +} + +// Open runs luksOpen on a device and returns mapped device path. +func (l *LUKS) Open(deviceName, mappedName string, key *encryption.Key) (string, error) { + args := slices.Concat( + []string{"luksOpen", deviceName, mappedName, "--key-file=-"}, + keyslotArgs(key), + l.perfArgs(), + ) + + _, err := l.runCommand(args, key.Value) + if err != nil { + return "", err + } + + return filepath.Join("/dev/mapper", mappedName), nil +} + +// Encrypt implements encryption.Provider. +func (l *LUKS) Encrypt(deviceName string, key *encryption.Key) error { + cipher, err := l.cipher.String() + if err != nil { + return err + } + + args := slices.Concat( + []string{"luksFormat", "--type", "luks2", "--key-file=-", "-c", cipher, deviceName}, + l.argonArgs(), + keyslotArgs(key), + l.encryptionArgs(), + ) + + if l.blockSize != 0 { + args = append(args, fmt.Sprintf("--sector-size=%d", l.blockSize)) + } + + _, err = l.runCommand(args, key.Value) + + return err +} + +// Resize implements encryption.Provider. +func (l *LUKS) Resize(devname string, key *encryption.Key) error { + args := []string{"resize", devname, "--key-file=-"} + + _, err := l.runCommand(args, key.Value) + + return err +} + +// Close implements encryption.Provider. +func (l *LUKS) Close(devname string) error { + _, err := l.runCommand([]string{"luksClose", devname}, nil) + + return err +} + +// AddKey adds a new key at the LUKS encryption slot. +func (l *LUKS) AddKey(devname string, key, newKey *encryption.Key) error { + var buffer bytes.Buffer + + keyfileLen, _ := buffer.Write(key.Value) + buffer.Write(newKey.Value) + + args := slices.Concat( + []string{ + "luksAddKey", + devname, + "--key-file=-", + fmt.Sprintf("--keyfile-size=%d", keyfileLen), + }, + l.argonArgs(), + l.encryptionArgs(), + keyslotArgs(newKey), + ) + + _, err := l.runCommand(args, buffer.Bytes()) + + return err +} + +// SetKey sets new key value at the LUKS encryption slot. +func (l *LUKS) SetKey(devname string, oldKey, newKey *encryption.Key) error { + if oldKey.Slot != newKey.Slot { + return fmt.Errorf("old and new key slots must match") + } + + var buffer bytes.Buffer + + keyfileLen, _ := buffer.Write(oldKey.Value) + buffer.Write(newKey.Value) + + args := slices.Concat( + []string{ + "luksChangeKey", + devname, + "--key-file=-", + fmt.Sprintf("--key-slot=%d", newKey.Slot), + fmt.Sprintf("--keyfile-size=%d", keyfileLen), + }, + l.argonArgs(), + l.perfArgs(), + ) + + _, err := l.runCommand(args, buffer.Bytes()) + + return err +} + +// CheckKey checks if the key is valid. +func (l *LUKS) CheckKey(devname string, key *encryption.Key) (bool, error) { + args := slices.Concat( + []string{"luksOpen", "--test-passphrase", devname, "--key-file=-"}, + keyslotArgs(key), + ) + + _, err := l.runCommand(args, key.Value) + if err != nil { + if err == encryption.ErrEncryptionKeyRejected { //nolint:errorlint + return false, nil + } + + return false, err + } + + return true, nil +} + +// RemoveKey removes a key at the specified LUKS encryption slot. +func (l *LUKS) RemoveKey(devname string, slot int, key *encryption.Key) error { + _, err := l.runCommand([]string{"luksKillSlot", devname, strconv.Itoa(slot), "--key-file=-"}, key.Value) + if err != nil { + return err + } + + if err = l.RemoveToken(devname, slot); err != nil && !errors.Is(err, encryption.ErrTokenNotFound) { + return err + } + + return nil +} + +// ReadKeyslots returns deserialized LUKS2 keyslots JSON. +func (l *LUKS) ReadKeyslots(deviceName string) (*encryption.Keyslots, error) { + bd, err := block.NewFromPath(deviceName) + if err != nil { + return nil, err + } + + defer bd.Close() //nolint:errcheck + + sb := make(luks2.Luks2Header, 4096) + + if _, err = io.ReadFull(bd.File(), sb[:]); err != nil { + return nil, err + } + + jsonArea := make([]byte, int(sb.Get_hdr_size())-len(sb)) + + if _, err = io.ReadFull(bd.File(), jsonArea); err != nil { + return nil, err + } + + jsonArea = bytes.Trim(bytes.TrimSpace(jsonArea), "\x00") + + var keyslots *encryption.Keyslots + + if err = json.Unmarshal(jsonArea, &keyslots); err != nil { + return nil, err + } + + return keyslots, nil +} + +// SetToken adds arbitrary token to the key slot. +// Token id == slot id: only one token per key slot is supported. +func (l *LUKS) SetToken(devname string, slot int, token token.Token) error { + data, err := token.Bytes() + if err != nil { + return err + } + + id := strconv.Itoa(slot) + + _, err = l.runCommand([]string{"token", "import", "-q", devname, "--token-id", id, "--json-file=-", "--token-replace"}, data) + + return err +} + +// ReadToken reads arbitrary token from the luks metadata. +func (l *LUKS) ReadToken(devname string, slot int, token token.Token) error { + stdout, err := l.runCommand([]string{"token", "export", "-q", devname, "--token-id", strconv.Itoa(slot), "--json-file=-"}, nil) + if err != nil { + return err + } + + return token.Decode([]byte(stdout)) +} + +// RemoveToken removes token from the luks metadata. +func (l *LUKS) RemoveToken(devname string, slot int) error { + _, err := l.runCommand([]string{"token", "remove", "--token-id", strconv.Itoa(slot), devname}, nil) + + return err +} + +var notFoundMatcher = regexp.MustCompile("(is not in use|Failed to get token)") + +// runCommand executes cryptsetup with arguments. +func (l *LUKS) runCommand(args []string, stdin []byte) (string, error) { + stdout, err := cmd.RunContext(cmd.WithStdin( + context.Background(), + bytes.NewBuffer(stdin)), "cryptsetup", args...) + if err != nil { + var exitError *cmd.ExitError + + if errors.As(err, &exitError) { + switch exitError.ExitCode { + case 1: + if strings.Contains(string(exitError.Output), "No usable keyslot is available.") { + return "", encryption.ErrEncryptionKeyRejected + } + + if notFoundMatcher.Match(exitError.Output) { + return "", encryption.ErrTokenNotFound + } + case 2: + return "", encryption.ErrEncryptionKeyRejected + case 5: + return "", encryption.ErrDeviceBusy + } + } + + return "", fmt.Errorf("failed to call cryptsetup: %w", err) + } + + return stdout, nil +} + +func (l *LUKS) argonArgs() []string { + args := []string{} + + if l.iterTime != 0 { + args = append(args, fmt.Sprintf("--iter-time=%d", l.iterTime.Milliseconds())) + } + + if l.pbkdfMemory != 0 { + args = append(args, fmt.Sprintf("--pbkdf-memory=%d", l.pbkdfMemory)) + } + + if l.pbkdfForceIterations != 0 { + args = append(args, fmt.Sprintf("--pbkdf-force-iterations=%d", l.pbkdfForceIterations)) + } + + return args +} + +func (l *LUKS) perfArgs() []string { + res := []string{} + + for _, o := range l.perfOptions { + res = append(res, fmt.Sprintf("--perf-%s", o)) + } + + return res +} + +func (l *LUKS) encryptionArgs() []string { + res := []string{} + + if l.keySize != 0 { + res = append(res, fmt.Sprintf("--key-size=%d", l.keySize)) + } + + return append(res, l.perfArgs()...) +} + +func keyslotArgs(key *encryption.Key) []string { + if key.Slot != encryption.AnyKeyslot { + return []string{fmt.Sprintf("--key-slot=%d", key.Slot)} + } + + return []string{} +} diff --git a/encryption/luks/luks_test.go b/encryption/luks/luks_test.go new file mode 100644 index 0000000..fb36c2c --- /dev/null +++ b/encryption/luks/luks_test.go @@ -0,0 +1,232 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package luks_test + +import ( + "errors" + randv2 "math/rand/v2" + "os" + "os/exec" + "path/filepath" + "testing" + "time" + + "github.com/freddierice/go-losetup/v2" + "github.com/google/uuid" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "golang.org/x/sys/unix" + + "github.com/siderolabs/go-blockdevice/v2/block" + "github.com/siderolabs/go-blockdevice/v2/encryption" + "github.com/siderolabs/go-blockdevice/v2/encryption/luks" + "github.com/siderolabs/go-blockdevice/v2/partitioning" + "github.com/siderolabs/go-blockdevice/v2/partitioning/gpt" +) + +const ( + size = 1024 * 1024 * 512 +) + +func testEncrypt(t *testing.T) { + tmpDir := t.TempDir() + + rawImage := filepath.Join(tmpDir, "image.raw") + + f, err := os.Create(rawImage) + require.NoError(t, err) + + require.NoError(t, f.Truncate(int64(size))) + require.NoError(t, f.Close()) + + loDev := losetupAttachHelper(t, rawImage, false) + + t.Cleanup(func() { + assert.NoError(t, loDev.Detach()) + }) + + devPath := loDev.Path() + + blkdev, err := block.NewFromPath(devPath, block.OpenForWrite()) + require.NoError(t, err) + + t.Cleanup(func() { + assert.NoError(t, blkdev.Close()) + }) + + require.NoError(t, blkdev.Lock(true)) + + t.Cleanup(func() { + assert.NoError(t, blkdev.Unlock()) + }) + + gptdev, err := gpt.DeviceFromBlockDevice(blkdev) + require.NoError(t, err) + + partitions, err := gpt.New(gptdev) + require.NoError(t, err) + + const ( + bootSize = 1024 * 512 + configSize = 1024 * 1024 * 32 + ) + + _, _, err = partitions.AllocatePartition(bootSize, "boot", uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")) + require.NoError(t, err) + + _, _, err = partitions.AllocatePartition(configSize, "config", uuid.MustParse("C12A7328-F81F-11D2-BA4B-00A0C93EC93B")) + require.NoError(t, err) + + require.NoError(t, partitions.Write()) + + var _ encryption.Provider = &luks.LUKS{} + + key := encryption.NewKey(0, []byte("changeme")) + keyExtra := encryption.NewKey(1, []byte("helloworld")) + + provider := luks.New( + luks.AESXTSPlain64Cipher, + luks.WithIterTime(time.Millisecond*100), + luks.WithPerfOptions(luks.PerfSameCPUCrypt), + ) + + path := partitioning.DevName(devPath, 2) + mappedName := filepath.Base(path) + "-encrypted" + + t.Logf("unencrypted partition path %s", path) + + require.NoError(t, provider.Encrypt(path, key)) + + encryptedPath, err := provider.Open(path, mappedName, key) + require.NoError(t, err) + + require.NoError(t, provider.Resize(encryptedPath, key)) + + require.NoError(t, provider.AddKey(path, key, keyExtra)) + require.NoError(t, provider.SetKey(path, keyExtra, keyExtra)) + + valid, err := provider.CheckKey(path, keyExtra) + require.NoError(t, err) + require.True(t, valid) + + valid, err = provider.CheckKey(path, encryption.NewKey(1, []byte("nope"))) + require.NoError(t, err) + require.False(t, valid) + + mountPath := t.TempDir() + + cmd := exec.Command("mkfs.vfat", "-F", "32", "-n", "config", encryptedPath) + require.NoError(t, cmd.Run()) + + type SealedKey struct { + SealedKey string `json:"sealed_key"` + } + + token := &luks.Token[SealedKey]{ + UserData: SealedKey{ + SealedKey: "aaaa", + }, + Type: "sealedkey", + } + + err = provider.SetToken(path, 0, token) + require.NoError(t, err) + + err = provider.ReadToken(path, 0, token) + require.NoError(t, err) + + require.Equal(t, token.UserData.SealedKey, "aaaa") + + require.NoError(t, provider.RemoveToken(path, 0)) + require.Error(t, provider.ReadToken(path, 0, token)) + + // create and replace token + err = provider.SetToken(path, 0, token) + require.NoError(t, err) + + token.UserData.SealedKey = "bbbb" + + err = provider.SetToken(path, 0, token) + require.NoError(t, err) + + require.NoError(t, unix.Mount(encryptedPath, mountPath, "vfat", 0, "")) + require.NoError(t, unix.Unmount(mountPath, 0)) + + require.NoError(t, provider.Close(encryptedPath)) + require.Error(t, provider.Close(encryptedPath)) + + // second key slot + encryptedPath, err = provider.Open(path, mappedName, keyExtra) + require.NoError(t, err) + require.NoError(t, provider.Close(encryptedPath)) + + // check keyslots list + keyslots, err := provider.ReadKeyslots(path) + require.NoError(t, err) + + _, ok := keyslots.Keyslots["0"] + require.True(t, ok) + _, ok = keyslots.Keyslots["1"] + require.True(t, ok) + + // remove key slot + err = provider.RemoveKey(path, 1, key) + require.NoError(t, err) + _, err = provider.Open(path, mappedName, keyExtra) + require.Equal(t, err, encryption.ErrEncryptionKeyRejected) + + valid, err = provider.CheckKey(path, key) + require.NoError(t, err) + require.True(t, valid) + + // unhappy cases + _, err = provider.Open(path, mappedName, encryption.NewKey(0, []byte("エクスプロシオン"))) + require.Equal(t, err, encryption.ErrEncryptionKeyRejected) + + _, err = provider.Open("/dev/nosuchdevice", mappedName, encryption.NewKey(0, []byte("エクスプロシオン"))) + require.Error(t, err) + + _, err = provider.Open(loDev.Path(), mappedName, key) + require.Error(t, err) +} + +func TestLUKS(t *testing.T) { + if os.Getuid() != 0 { + t.Skip("can't run the test as non-root") + } + + if hostname, _ := os.Hostname(); hostname == "buildkitsandbox" { //nolint:errcheck + t.Skip("test not supported under buildkit as partition devices are not propagated from /dev") + } + + t.Run("Encrypt", testEncrypt) +} + +func losetupAttachHelper(t *testing.T, rawImage string, readonly bool) losetup.Device { + t.Helper() + + for range 10 { + loDev, err := losetup.Attach(rawImage, 0, readonly) + if err != nil { + if errors.Is(err, unix.EBUSY) { + spraySleep := max(randv2.ExpFloat64(), 2.0) + + t.Logf("retrying after %v seconds", spraySleep) + + time.Sleep(time.Duration(spraySleep * float64(time.Second))) + + continue + } + } + + require.NoError(t, err) + + return loDev + } + + t.Fatal("failed to attach loop device") //nolint:revive + + panic("unreachable") +} diff --git a/encryption/luks/options.go b/encryption/luks/options.go new file mode 100644 index 0000000..01b16d4 --- /dev/null +++ b/encryption/luks/options.go @@ -0,0 +1,53 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package luks provides a way to call LUKS2 cryptsetup. +package luks + +import "time" + +// Option represents luks configuration callback. +type Option func(l *LUKS) + +// WithIterTime sets iter-time parameter. +func WithIterTime(value time.Duration) Option { + return func(l *LUKS) { + l.iterTime = value + } +} + +// WithPBKDFForceIterations sets pbkdf-force-iterations parameter. +func WithPBKDFForceIterations(value uint) Option { + return func(l *LUKS) { + l.pbkdfForceIterations = value + } +} + +// WithPBKDFMemory sets pbkdf-memory parameter. +func WithPBKDFMemory(value uint64) Option { + return func(l *LUKS) { + l.pbkdfMemory = value + } +} + +// WithKeySize sets generated key size. +func WithKeySize(value uint) Option { + return func(l *LUKS) { + l.keySize = value + } +} + +// WithBlockSize sets block size. +func WithBlockSize(value uint64) Option { + return func(l *LUKS) { + l.blockSize = value + } +} + +// WithPerfOptions enables encryption perf options. +func WithPerfOptions(options ...string) Option { + return func(l *LUKS) { + l.perfOptions = options + } +} diff --git a/encryption/luks/token.go b/encryption/luks/token.go new file mode 100644 index 0000000..4c3f0dc --- /dev/null +++ b/encryption/luks/token.go @@ -0,0 +1,26 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package luks + +import "encoding/json" + +// Token defines LUKS2 token. +type Token[UserData any] struct { + UserData UserData + Type string `json:"type"` +} + +// Bytes encodes token into bytes. +func (t *Token[UserData]) Bytes() ([]byte, error) { + return json.Marshal(struct { + *Token[UserData] + KeySlots []string `json:"keyslots"` + }{Token: t, KeySlots: []string{}}) +} + +// Decode reads token data from bytes. +func (t *Token[UserData]) Decode(in []byte) error { + return json.Unmarshal(in, &t) +} diff --git a/encryption/provider.go b/encryption/provider.go new file mode 100644 index 0000000..5977e50 --- /dev/null +++ b/encryption/provider.go @@ -0,0 +1,77 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package encryption + +import ( + "fmt" + + "github.com/siderolabs/go-blockdevice/v2/encryption/token" +) + +const ( + // LUKS2 encryption. + LUKS2 = "luks2" + // Unknown unecrypted or unsupported encryption. + Unknown = "unknown" +) + +// Provider represents encryption utility methods. +type Provider interface { + TokenProvider + Encrypt(devname string, key *Key) error + Open(devname, mappedName string, key *Key) (string, error) + Close(devname string) error + AddKey(devname string, key, newKey *Key) error + SetKey(devname string, key, newKey *Key) error + CheckKey(devname string, key *Key) (bool, error) + RemoveKey(devname string, slot int, key *Key) error + ReadKeyslots(deviceName string) (*Keyslots, error) +} + +// TokenProvider represents token management methods. +type TokenProvider interface { + SetToken(devname string, slot int, token token.Token) error + ReadToken(devname string, slot int, token token.Token) error + RemoveToken(devname string, slot int) error +} + +var ( + // ErrEncryptionKeyRejected triggered when encryption key does not match. + ErrEncryptionKeyRejected = fmt.Errorf("encryption key rejected") + + // ErrDeviceBusy returned when mapped device is still in use. + ErrDeviceBusy = fmt.Errorf("mapped device is still in use") + + // ErrTokenNotFound returned when trying to get/delete not existing token. + ErrTokenNotFound = fmt.Errorf("no token with supplied id exists") +) + +// Keyslots represents LUKS2 keyslots metadata. +type Keyslots struct { + Keyslots map[string]*Keyslot `json:"keyslots"` +} + +// Keyslot represents a single LUKS2 keyslot. +type Keyslot struct { + Type string `json:"type"` + KeySize int64 `json:"key_size"` +} + +// NewKey create a new key. +func NewKey(slot int, value []byte) *Key { + return &Key{ + Value: value, + Slot: slot, + } +} + +// AnyKeyslot tells providers to pick any keyslot. +const AnyKeyslot = -1 + +// Key represents a single key. +type Key struct { + Value []byte + Slot int +} diff --git a/encryption/token/token.go b/encryption/token/token.go new file mode 100644 index 0000000..2ee0e42 --- /dev/null +++ b/encryption/token/token.go @@ -0,0 +1,12 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +// Package token contains token management interfaces. +package token + +// Token defines luks token interface. +type Token interface { + Bytes() ([]byte, error) + Decode(in []byte) error +} diff --git a/go.mod b/go.mod index d4731f3..8048878 100644 --- a/go.mod +++ b/go.mod @@ -7,6 +7,7 @@ require ( github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.17.9 github.com/siderolabs/gen v0.5.0 + github.com/siderolabs/go-cmd v0.1.1 github.com/siderolabs/go-pointer v1.0.0 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 @@ -15,6 +16,7 @@ require ( ) require ( + github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect go.uber.org/multierr v1.10.0 // indirect diff --git a/go.sum b/go.sum index 8b44520..f6ab3e5 100644 --- a/go.sum +++ b/go.sum @@ -1,3 +1,5 @@ +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2 h1:7Ip0wMmLHLRJdrloDxZfhMm0xrLXZS8+COSu2bXmEQs= +github.com/armon/circbuf v0.0.0-20190214190532-5111143e8da2/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/freddierice/go-losetup/v2 v2.0.1 h1:wPDx/Elu9nDV8y/CvIbEDz5Xi5Zo80y4h7MKbi3XaAI= @@ -6,10 +8,16 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= +github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/siderolabs/gen v0.5.0 h1:Afdjx+zuZDf53eH5DB+E+T2JeCwBXGinV66A6osLgQI= github.com/siderolabs/gen v0.5.0/go.mod h1:1GUMBNliW98Xeq8GPQeVMYqQE09LFItE8enR3wgMh3Q= +github.com/siderolabs/go-cmd v0.1.1 h1:nTouZUSxLeiiEe7hFexSVvaTsY/3O8k1s08BxPRrsps= +github.com/siderolabs/go-cmd v0.1.1/go.mod h1:6hY0JG34LxEEwYE8aH2iIHkHX/ir12VRLqfwAf2yJIY= github.com/siderolabs/go-pointer v1.0.0 h1:6TshPKep2doDQJAAtHUuHWXbca8ZfyRySjSBT/4GsMU= github.com/siderolabs/go-pointer v1.0.0/go.mod h1:HTRFUNYa3R+k0FFKNv11zgkaCLzEkWVzoYZ433P3kHc= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= @@ -24,7 +32,8 @@ golang.org/x/sys v0.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b h1:QRR6H1YWRnHb4Y/HeNFCTJLFVxaq6wH4YuVdsUOr75U= +gopkg.in/check.v1 v1.0.0-20200902074654-038fdea0a05b/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/hack/release.sh b/hack/release.sh index 330b4de..a4dcb0b 100755 --- a/hack/release.sh +++ b/hack/release.sh @@ -2,7 +2,7 @@ # THIS FILE WAS AUTOMATICALLY GENERATED, PLEASE DO NOT EDIT. # -# Generated on 2024-02-05T11:19:51Z by kres 0e666ea. +# Generated on 2024-08-22T10:15:10Z by kres 9cc7f48. set -e @@ -44,9 +44,88 @@ function commit { exit 1 fi + if is_on_main_branch; then + update_license_files + fi + git commit -s -m "release($1): prepare release" -m "This is the official $1 release." } +function is_on_main_branch { + main_remotes=("upstream" "origin") + branch_names=("main" "master") + current_branch=$(git rev-parse --abbrev-ref HEAD) + + echo "Check current branch: $current_branch" + + for remote in "${main_remotes[@]}"; do + echo "Fetch remote $remote..." + + if ! git fetch --quiet "$remote" &>/dev/null; then + echo "Failed to fetch $remote, skip..." + + continue + fi + + for branch_name in "${branch_names[@]}"; do + if ! git rev-parse --verify "$branch_name" &>/dev/null; then + echo "Branch $branch_name does not exist, skip..." + + continue + fi + + echo "Branch $remote/$branch_name exists, comparing..." + + merge_base=$(git merge-base "$current_branch" "$remote/$branch_name") + latest_main=$(git rev-parse "$remote/$branch_name") + + if [ "$merge_base" = "$latest_main" ]; then + echo "Current branch is up-to-date with $remote/$branch_name" + + return 0 + else + echo "Current branch is not on $remote/$branch_name" + + return 1 + fi + done + done + + echo "No main or master branch found on any remote" + + return 1 +} + +function update_license_files { + script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" + parent_dir="$(dirname "$script_dir")" + current_year=$(date +"%Y") + change_date=$(date -v+4y +"%Y-%m-%d" 2>/dev/null || date -d "+4 years" +"%Y-%m-%d" 2>/dev/null || date --date="+4 years" +"%Y-%m-%d") + + # Find LICENSE files recursively in the parent directory + find "$parent_dir" -type f -name "LICENSE" | while read -r file; do + # Check if file starts with "Business Source License" + if grep -q "^Business Source License" "$file"; then + temp_file="${file}.tmp" + + # Update the year and the change date + sed -e "s/The Licensed Work is (c) [0-9]\{4\}/The Licensed Work is (c) $current_year/" \ + -e "s/Change Date: [0-9]\{4\}-[0-9]\{2\}-[0-9]\{2\}/Change Date: $change_date/" \ + "$file" > "$temp_file" + + # Check if the file has changed + if ! cmp -s "$file" "$temp_file"; then + mv "$temp_file" "$file" + echo "Updated: $file" + git add "$file" + else + echo "No changes: $file" + rm "$temp_file" + fi + fi + done +} + if declare -f "$1" > /dev/null then cmd="$1" @@ -55,7 +134,7 @@ then else cat <