Skip to content

Commit

Permalink
feat: implement luks encryption provider
Browse files Browse the repository at this point in the history
Fixes: siderolabs/talos#3030

Wrap around `cryptsetup` module and support encrypt, open and close
functions as a start.

Signed-off-by: Artem Chernyshev <[email protected]>
  • Loading branch information
Unix4ever authored and talos-bot committed Feb 10, 2021
1 parent b0375e4 commit 04a9851
Show file tree
Hide file tree
Showing 17 changed files with 683 additions and 16 deletions.
4 changes: 3 additions & 1 deletion blockdevice/blockdevice.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@
// Package blockdevice provides a library for working with block devices.
package blockdevice

import "errors"
import (
"errors"
)

// ErrMissingPartitionTable indicates that the the block device does not have a
// partition table.
Expand Down
2 changes: 1 addition & 1 deletion blockdevice/blockdevice_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ func (bd *BlockDevice) Wipe() error {
}

// OpenPartition opens another blockdevice using a partition of this block device.
func (bd *BlockDevice) OpenPartition(label string) (*BlockDevice, error) {
func (bd *BlockDevice) OpenPartition(label string, setters ...Option) (*BlockDevice, error) {
return nil, fmt.Errorf("not implemented")
}

Expand Down
5 changes: 2 additions & 3 deletions blockdevice/blockdevice_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,6 @@ type BlockDevice struct {
// TODO(andrewrynhard): Use BLKGETSIZE ioctl to get the size.
func Open(devname string, setters ...Option) (bd *BlockDevice, err error) {
opts := NewDefaultOptions(setters...)

bd = &BlockDevice{}

var f *os.File
Expand Down Expand Up @@ -274,7 +273,7 @@ func (bd *BlockDevice) Reset() error {
}

// OpenPartition opens another blockdevice using a partition of this block device.
func (bd *BlockDevice) OpenPartition(label string) (*BlockDevice, error) {
func (bd *BlockDevice) OpenPartition(label string, setters ...Option) (*BlockDevice, error) {
g, err := bd.PartitionTable()
if err != nil {
return nil, err
Expand All @@ -290,7 +289,7 @@ func (bd *BlockDevice) OpenPartition(label string) (*BlockDevice, error) {
return nil, err
}

return Open(path)
return Open(path, setters...)
}

// GetPartition returns partition by label if found.
Expand Down
6 changes: 6 additions & 0 deletions blockdevice/encryption/encryption.go
Original file line number Diff line number Diff line change
@@ -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
284 changes: 284 additions & 0 deletions blockdevice/encryption/luks/luks.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,284 @@
// 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"
"encoding/binary"
"encoding/json"
"fmt"
"os"
"os/exec"
"strings"
"time"

"golang.org/x/sys/unix"

"github.com/talos-systems/go-blockdevice/blockdevice/encryption"
"github.com/talos-systems/go-blockdevice/blockdevice/filesystem/luks"
"github.com/talos-systems/go-blockdevice/blockdevice/util"
)

// Cipher LUKS2 cipher type.
type Cipher int

// String converts to command line string parameter value.
func (c Cipher) String() (string, error) {
switch c {
case AESXTSPlain64Cipher:
return AESXTSPlain64CipherString, 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
default:
return 0, fmt.Errorf("unknown cipher kind %s", s)
}
}

const (
// AESXTSPlain64CipherString string representation of aes-xts-plain64 cipher.
AESXTSPlain64CipherString = "aes-xts-plain64"
// AESXTSPlain64Cipher represents aes-xts-plain64 encryption cipher.
AESXTSPlain64Cipher Cipher = iota
)

// LUKS implements LUKS2 encryption provider.
type LUKS struct {
cipher Cipher
iterTime time.Duration
pbkdfForceIterations uint
pbkdfMemory uint64
}

// New creates new LUKS2 encryption provider.
func New(cipher Cipher, options ...Option) *LUKS {
l := &LUKS{
cipher: cipher,
}

for _, option := range options {
option(l)
}

return l
}

// Open runs luksOpen on a device and returns mapped device path.
func (l *LUKS) Open(deviceName string, key *encryption.Key) (string, error) {
parts := strings.Split(deviceName, "/")
mappedPath := util.PartPathEncrypted(parts[len(parts)-1])
parts = strings.Split(mappedPath, "/")
mappedName := parts[len(parts)-1]

args := []string{"luksOpen", deviceName, mappedName, "--key-file=-"}
args = append(args, keyslotArgs(key)...)

err := l.runCommand(args, key.Value)
if err != nil {
return "", err
}

return mappedPath, 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 := []string{"luksFormat", "--type", "luks2", "--key-file=-", "-c", cipher, deviceName}
args = append(args, l.argonArgs()...)
args = append(args, keyslotArgs(key)...)

err = l.runCommand(args, key.Value)
if err != nil {
return err
}

return err
}

// Close implements encryption.Provider.
func (l *LUKS) Close(devname string) error {
return l.runCommand([]string{"luksClose", devname}, nil)
}

// 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) //nolint:errcheck
buffer.Write(newKey.Value) //nolint:errcheck

args := []string{
"luksAddKey",
devname,
"--key-file=-",
fmt.Sprintf("--keyfile-size=%d", keyfileLen),
}

args = append(args, l.argonArgs()...)
args = append(args, keyslotArgs(newKey)...)

return l.runCommand(args, buffer.Bytes())
}

// 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) //nolint:errcheck
buffer.Write(newKey.Value) //nolint:errcheck

args := []string{
"luksChangeKey",
devname,
"--key-file=-",
fmt.Sprintf("--key-slot=%d", newKey.Slot),
fmt.Sprintf("--keyfile-size=%d", keyfileLen),
}

args = append(args, l.argonArgs()...)

return l.runCommand(args, buffer.Bytes())
}

// CheckKey checks if the key is valid.
func (l *LUKS) CheckKey(devname string, key *encryption.Key) (bool, error) {
args := []string{"luksOpen", "--test-passphrase", devname, "--key-file=-"}

args = append(args, 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 adds a new key at the LUKS encryption slot.
func (l *LUKS) RemoveKey(devname string, slot int, key *encryption.Key) error {
return l.runCommand([]string{"luksKillSlot", devname, fmt.Sprintf("%d", slot), "--key-file=-", fmt.Sprintf("--key-slot=%d", key.Slot)}, key.Value)
}

// ReadKeyslots returns deserialized LUKS2 keyslots JSON.
func (l *LUKS) ReadKeyslots(deviceName string) (*encryption.Keyslots, error) {
f, err := os.OpenFile(deviceName, os.O_RDONLY|unix.O_CLOEXEC, os.ModeDevice)
if err != nil {
return nil, err
}

defer f.Close() //nolint:errcheck

sb := &luks.SuperBlock{}

if err = binary.Read(f, binary.BigEndian, sb); err != nil {
return nil, err
}

size := binary.Size(sb)
if _, err = f.Seek(int64(size), 0); err != nil {
return nil, err
}

jsonArea := make([]byte, int(sb.HeaderSize)-size)

if _, err = f.Read(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
}

// CheckKey try using the key

// runCommand executes cryptsetup with arguments.
func (l *LUKS) runCommand(args []string, stdin []byte) error {
cmd := exec.Command("cryptsetup", args...)

var out bytes.Buffer

if stdin != nil {
cmd.Stdin = bytes.NewBuffer(stdin)
}

cmd.Stdout = &out
cmd.Stderr = &out

err := cmd.Run()
if err != nil {
if e, ok := err.(*exec.ExitError); ok { //nolint:errorlint
switch e.ExitCode() {
case 1:
if strings.Contains(out.String(), "Keyslot open failed.\nNo usable keyslot is available.") {
return encryption.ErrEncryptionKeyRejected
}
case 2:
return encryption.ErrEncryptionKeyRejected
case 5:
return encryption.ErrDeviceBusy
}
}

return fmt.Errorf("failed to call cryptsetup: %w, output: %s", err, out.String())
}

return 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 keyslotArgs(key *encryption.Key) []string {
if key.Slot != encryption.AnyKeyslot {
return []string{fmt.Sprintf("--key-slot=%d", key.Slot)}
}

return []string{}
}
Loading

0 comments on commit 04a9851

Please sign in to comment.