1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338
|
// Copyright (c) 2018-2023, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package crypt
import (
"bytes"
"errors"
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"time"
"github.com/google/uuid"
"github.com/sylabs/singularity/v4/internal/pkg/util/bin"
"github.com/sylabs/singularity/v4/internal/pkg/util/fs"
"github.com/sylabs/singularity/v4/pkg/sylog"
"github.com/sylabs/singularity/v4/pkg/util/fs/lock"
"github.com/sylabs/singularity/v4/pkg/util/loop"
"golang.org/x/sys/unix"
)
// Device describes a crypt device
type Device struct{}
// Pre-defined error(s)
var (
// ErrUnsupportedCryptsetupVersion is the error raised when the available version
// of cryptsetup is not compatible with the Singularity encryption mechanism.
ErrUnsupportedCryptsetupVersion = errors.New("installed version of cryptsetup is not supported, >=2.0.0 required")
// ErrInvalidPassphrase raised when the passed key is not valid to open requested
// encrypted device.
ErrInvalidPassphrase = errors.New("no key available with this passphrase")
)
// createLoop attaches the specified file to the next available loop
// device and sets the sizelimit on it
func createLoop(path string, offset, size uint64) (string, error) {
loopDev := &loop.Device{
MaxLoopDevices: loop.GetMaxLoopDevices(),
Shared: true,
Info: &unix.LoopInfo64{
Sizelimit: size,
Offset: offset,
Flags: unix.LO_FLAGS_AUTOCLEAR,
},
}
idx := 0
if err := loopDev.AttachFromPath(path, os.O_RDWR, &idx); err != nil {
return "", fmt.Errorf("failed to attach image %s: %s", path, err)
}
return fmt.Sprintf("/dev/loop%d", idx), nil
}
// CloseCryptDevice closes the crypt device
func (crypt *Device) CloseCryptDevice(path string) error {
cryptsetup, err := bin.FindBin("cryptsetup")
if err != nil {
return err
}
if !fs.IsOwner(cryptsetup, 0) {
return fmt.Errorf("%s must be owned by root", cryptsetup)
}
fd, err := lock.Exclusive("/dev/mapper")
if err != nil {
return err
}
defer lock.Release(fd)
cmd := exec.Command(cryptsetup, "close", path)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{Uid: 0, Gid: 0},
}
sylog.Debugf("Running %s %s", cmd.Path, strings.Join(cmd.Args, " "))
err = cmd.Run()
if err != nil {
sylog.Debugf("Unable to delete the crypt device %s", err)
return err
}
return nil
}
func checkCryptsetupVersion(cryptsetup string) error {
if cryptsetup == "" {
return fmt.Errorf("binary path not defined")
}
cmd := exec.Command(cryptsetup, "--version")
out, err := cmd.CombinedOutput()
if err != nil {
return fmt.Errorf("failed to run cryptsetup --version: %s", err)
}
if !strings.Contains(string(out), "cryptsetup 2.") {
return ErrUnsupportedCryptsetupVersion
}
// We successfully ran cryptsetup --version and we know that the
// version is compatible with our needs.
return nil
}
// EncryptFilesystem takes the path to a file containing a non-encrypted
// filesystem, encrypts it using the provided key, and returns a path to
// a file that can be later used as an encrypted volume with cryptsetup.
// NOTE: it is the callers responsibility to remove the returned file that
// contains the crypt header.
func (crypt *Device) EncryptFilesystem(path string, key []byte) (string, error) {
f, err := os.Stat(path)
if err != nil {
return "", fmt.Errorf("failed getting size of %s", path)
}
fSize := f.Size()
// Create a temporary file to format with crypt header
cryptF, err := os.CreateTemp("", "crypt-")
if err != nil {
sylog.Debugf("Error creating temporary crypt file")
return "", err
}
defer cryptF.Close()
// Truncate the file taking the squashfs size and crypt header
// into account. With the options specified below the LUKS header
// is less than 16MB in size. Slightly over-allocate
// to compensate for the encryption overhead itself.
//
// TODO(mem): the encryption overhead might depend on the size
// of the data we are encrypting. For very large images, we
// might not be overallocating enough. Figure out what's the
// actual percentage we need to overallocate.
devSize := fSize + 16*1024*1024
sylog.Debugf("Total device size for encrypted image: %d", devSize)
err = os.Truncate(cryptF.Name(), devSize)
if err != nil {
sylog.Debugf("Unable to truncate crypt file to size %d", devSize)
return "", err
}
cryptF.Close()
// Associate the temporary crypt file with a loop device
loop, err := createLoop(cryptF.Name(), 0, uint64(devSize))
if err != nil {
return "", err
}
// NOTE: This routine runs with root privileges. It's not necessary
// to explicitly set cmd's uid or gid here
// TODO (schebro): Fix #3818, #3821
// Currently we are relying on host's cryptsetup utility to encrypt and decrypt
// the SIF. The possibility to saving a version of cryptsetup inside the container should be
// investigated. To do that, at least one additional partition is required, which is
// not encrypted.
cryptsetup, err := bin.FindBin("cryptsetup")
if err != nil {
return "", err
}
if !fs.IsOwner(cryptsetup, 0) {
return "", fmt.Errorf("%s must be owned by root", cryptsetup)
}
cmd := exec.Command(cryptsetup, "luksFormat", "--batch-mode", "--type", "luks2", "--key-file", "-", loop)
stdin, err := cmd.StdinPipe()
if err != nil {
return "", err
}
go func() {
stdin.Write(key)
stdin.Close()
}()
sylog.Debugf("Running %s %s", cmd.Path, strings.Join(cmd.Args, " "))
out, err := cmd.CombinedOutput()
if err != nil {
err = checkCryptsetupVersion(cryptsetup)
if err == ErrUnsupportedCryptsetupVersion {
// Special case of unsupported version of cryptsetup. We return the raw error
// so it can propagate up and a user-friendly message be displayed. This error
// should trigger an error at the CLI level.
return "", err
}
return "", fmt.Errorf("unable to format crypt device: %s: %s", cryptF.Name(), string(out))
}
nextCrypt, err := crypt.Open(key, loop)
if err != nil {
sylog.Verbosef("Unable to open encrypted device %s: %s", loop, err)
return "", err
}
copyDeviceContents(path, "/dev/mapper/"+nextCrypt, fSize)
cmd = exec.Command(cryptsetup, "close", nextCrypt)
sylog.Debugf("Running %s %s", cmd.Path, strings.Join(cmd.Args, " "))
err = cmd.Run()
if err != nil {
return "", err
}
return cryptF.Name(), err
}
// copyDeviceContents copies the contents of source to destination.
// source and dest can either be a file or a block device
func copyDeviceContents(source, dest string, size int64) error {
sylog.Debugf("Copying %s to %s, size %d", source, dest, size)
sourceFd, err := syscall.Open(source, syscall.O_RDONLY, 0o000)
if err != nil {
return fmt.Errorf("unable to open the file %s", source)
}
defer syscall.Close(sourceFd)
destFd, err := syscall.Open(dest, syscall.O_WRONLY, 0o666)
if err != nil {
return fmt.Errorf("unable to open the file: %s", dest)
}
defer syscall.Close(destFd)
var writtenSoFar int64
buffer := make([]byte, 10240)
for writtenSoFar < size {
buffer = buffer[:cap(buffer)]
numRead, err := syscall.Read(sourceFd, buffer)
if err != nil {
return fmt.Errorf("unable to read the file %s", source)
}
buffer = buffer[:numRead]
for n := 0; n < numRead; {
numWritten, err := syscall.Write(destFd, buffer[n:])
if err != nil {
return fmt.Errorf("unable to write to destination %s", dest)
}
n += numWritten
writtenSoFar += int64(numWritten)
}
}
return nil
}
func getNextAvailableCryptDevice() (string, error) {
id, err := uuid.NewRandom()
if err != nil {
return "", err
}
return id.String(), nil
}
// Open opens the encrypted filesystem specified by path (usually a loop
// device, but any encrypted block device will do) using the given key
// and returns the name assigned to it that can be later used to close
// the device.
func (crypt *Device) Open(key []byte, path string) (string, error) {
fd, err := lock.Exclusive("/dev/mapper")
if err != nil {
return "", fmt.Errorf("unable to acquire lock on /dev/mapper")
}
defer lock.Release(fd)
maxRetries := 3 // Arbitrary number of retries.
cryptsetup, err := bin.FindBin("cryptsetup")
if err != nil {
return "", err
}
if !fs.IsOwner(cryptsetup, 0) {
return "", fmt.Errorf("%s must be owned by root", cryptsetup)
}
for i := 0; i < maxRetries; i++ {
nextCrypt, err := getNextAvailableCryptDevice()
if err != nil {
return "", err
}
cmd := exec.Command(cryptsetup, "open", "--batch-mode", "--type", "luks2", "--key-file", "-", path, nextCrypt)
cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{Uid: 0, Gid: 0}
sylog.Debugf("Running %s %s", cmd.Path, strings.Join(cmd.Args, " "))
cmd.Stdin = bytes.NewBuffer(key)
out, err := cmd.CombinedOutput()
if err != nil {
if strings.Contains(string(out), "Device already exists") {
continue
}
err = checkCryptsetupVersion(cryptsetup)
if err == ErrUnsupportedCryptsetupVersion {
// Special case of unsupported version of cryptsetup. We return the raw error
// so it can propagate up and a user-friendly message be displayed. This error
// should trigger an error at the CLI level.
return "", err
}
if strings.Contains(string(out), "No key available") {
sylog.Debugf("Invalid password")
return "", ErrInvalidPassphrase
}
return "", fmt.Errorf("cryptsetup open failed: %s: %v", string(out), err)
}
for attempt := 0; true; attempt++ {
_, err := os.Stat("/dev/mapper/" + nextCrypt)
if err == nil {
break
}
if !errors.Is(err, os.ErrNotExist) {
return "", err
}
delayNext := 100 * (1 << attempt) * time.Millisecond // power of two exponential back off means
delaySoFar := delayNext - 1 // total delay so far is next delay - 1
if delaySoFar >= 25500*time.Millisecond {
return "", fmt.Errorf("device /dev/mapper/%s did not show up within %d seconds", nextCrypt, delaySoFar/time.Second)
}
time.Sleep(delayNext)
}
sylog.Debugf("Successfully opened encrypted device %s", path)
return nextCrypt, nil
}
return "", errors.New("unable to open crypt device")
}
|