/* Copyright (c) 2012 The Chromium OS Authors. All rights reserved. * Use of this source code is governed by a BSD-style license that can be * found in the LICENSE file. * * This is a collection of helper utilities for use with the "mount-encrypted" * utility. * */ #define _GNU_SOURCE #define _FILE_OFFSET_BITS 64 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "mount-encrypted.h" #include "mount-helpers.h" static const gchar * const kRootDir = "/"; static const gchar * const kLoopTemplate = "/dev/loop%d"; static const int kLoopMajor = 7; static const int kLoopMax = 8; static const unsigned int kResizeStepSeconds = 2; static const uint64_t kResizeBlocks = 32768 * 10; static const uint64_t kBlocksPerGroup = 32768; static const uint64_t kInodeRatioDefault = 16384; static const uint64_t kInodeRatioMinimum = 2048; static const gchar * const kExt4ExtendedOptions = "discard,lazy_itable_init"; int remove_tree(const char *tree) { const gchar *rm[] = { "/bin/rm", "-rf", tree, NULL }; return runcmd(rm, NULL); } uint64_t blk_size(const char *device) { uint64_t bytes; int fd; if ((fd = open(device, O_RDONLY | O_NOFOLLOW)) < 0) { PERROR("open(%s)", device); return 0; } if (ioctl(fd, BLKGETSIZE64, &bytes)) { PERROR("ioctl(%s, BLKGETSIZE64)", device); return 0; } close(fd); return bytes; } int runcmd(const gchar *argv[], gchar **output) { gint rc; gchar *out = NULL, *errout = NULL; GError *err = NULL; g_spawn_sync(kRootDir, (gchar **)argv, NULL, 0, NULL, NULL, &out, &errout, &rc, &err); if (err) { ERROR("%s: %s", argv[0], err->message); g_error_free(err); return -1; } if (rc) ERROR("%s failed (%d)\n%s\n%s", argv[0], rc, out, errout); if (output) *output = out; else g_free(out); g_free(errout); return rc; } int same_vfs(const char *mnt_a, const char *mnt_b) { struct stat stat_a, stat_b; if (lstat(mnt_a, &stat_a)) { PERROR("lstat(%s)", mnt_a); exit(1); } if (lstat(mnt_b, &stat_b)) { PERROR("lstat(%s)", mnt_b); exit(1); } return (stat_a.st_dev == stat_b.st_dev); } /* Returns allocated string that holds [length]*2 + 1 characters. */ char *stringify_hex(uint8_t *binary, size_t length) { char *string; size_t i; string = malloc(length * 2 + 1); if (!string) { PERROR("malloc"); return NULL; } for (i = 0; i < length; ++i) sprintf(string + (i * 2), "%02x", binary[i]); string[length * 2] = '\0'; return string; } /* Returns allocated byte array that holds strlen([string])/2 bytes. */ uint8_t *hexify_string(char *string, uint8_t *binary, size_t length) { size_t bytes, i; bytes = strlen(string) / 2; if (bytes > length) { ERROR("Hex string too long (%zu) for byte array (%zu)", bytes, length); return NULL; } for (i = 0; i < bytes; ++i) { if (sscanf(&string[i * 2], "%2hhx", &binary[i]) != 1) { ERROR("Invalid hex code at byte %zu.", i); return NULL; } } return binary; } /* Overwrite file contents. Useless on SSD. :( */ void shred(const char *pathname) { uint8_t patterns[] = { 0xA5, 0x5A, 0xFF, 0x00 }; FILE *target; struct stat info; uint8_t *pattern; int fd, i; /* Give up if we can't safely open or stat the target. */ if ((fd = open(pathname, O_WRONLY | O_NOFOLLOW)) < 0) { PERROR(pathname); return; } if (fstat(fd, &info)) { close(fd); PERROR(pathname); return; } if (!(target = fdopen(fd, "w"))) { close(fd); PERROR(pathname); return; } /* Ignore errors here, since there's nothing we can really do. */ pattern = malloc(info.st_size); for (i = 0; i < sizeof(patterns); ++i) { memset(pattern, patterns[i], info.st_size); if (fseek(target, 0, SEEK_SET)) PERROR(pathname); if (fwrite(pattern, info.st_size, 1, target) != 1) PERROR(pathname); if (fflush(target)) PERROR(pathname); if (fdatasync(fd)) PERROR(pathname); } free(pattern); /* fclose() closes the fd too. */ fclose(target); } static int is_loop_device(int fd) { struct stat info; return (fstat(fd, &info) == 0 && S_ISBLK(info.st_mode) && major(info.st_rdev) == kLoopMajor); } static int loop_is_attached(int fd, struct loop_info64 *info) { struct loop_info64 local_info; return ioctl(fd, LOOP_GET_STATUS64, info ? info : &local_info) == 0; } /* Returns either the matching loopback name, or next available, if NULL. */ static int loop_locate(gchar **loopback, const char *name) { int i, fd, namelen = 0; if (name) { namelen = strlen(name); if (namelen >= LO_NAME_SIZE) { ERROR("'%s' too long (>= %d)", name, LO_NAME_SIZE); return -1; } } *loopback = NULL; for (i = 0; i < kLoopMax; ++i) { struct loop_info64 info; int attached; g_free(*loopback); *loopback = g_strdup_printf(kLoopTemplate, i); if (!*loopback) { PERROR("g_strdup_printf"); return -1; } fd = open(*loopback, O_RDONLY | O_NOFOLLOW); if (fd < 0) { PERROR("open(%s)", *loopback); goto failed; } if (!is_loop_device(fd)) { close(fd); continue; } memset(&info, 0, sizeof(info)); attached = loop_is_attached(fd, &info); close(fd); if (attached) DEBUG("Saw %s on %s", info.lo_file_name, *loopback); if ((attached && name && strncmp((char *)info.lo_file_name, name, namelen) == 0) || (!attached && !name)) { DEBUG("Using %s", *loopback); /* Reopen for working on it. */ fd = open(*loopback, O_RDWR | O_NOFOLLOW); if (is_loop_device(fd) && loop_is_attached(fd, NULL) == attached) return fd; } } ERROR("Ran out of loopback devices"); failed: g_free(*loopback); *loopback = NULL; return -1; } static int loop_detach_fd(int fd) { if (ioctl(fd, LOOP_CLR_FD, 0)) { PERROR("LOOP_CLR_FD"); return 0; } return 1; } int loop_detach(const gchar *loopback) { int fd, rc = 1; fd = open(loopback, O_RDONLY | O_NOFOLLOW); if (fd < 0) { PERROR("open(%s)", loopback); return 0; } if (!is_loop_device(fd) || !loop_is_attached(fd, NULL) || !loop_detach_fd(fd)) rc = 0; close (fd); return rc; } int loop_detach_name(const char *name) { gchar *loopback = NULL; int loopfd, rc; loopfd = loop_locate(&loopback, name); if (loopfd < 0) return 0; rc = loop_detach_fd(loopfd); close(loopfd); g_free(loopback); return rc; } /* Closes fd, returns name of loopback device pathname. */ gchar *loop_attach(int fd, const char *name) { gchar *loopback = NULL; int loopfd; struct loop_info64 info; loopfd = loop_locate(&loopback, NULL); if (loopfd < 0) return NULL; if (ioctl(loopfd, LOOP_SET_FD, fd) < 0) { PERROR("LOOP_SET_FD"); goto failed; } memset(&info, 0, sizeof(info)); strncpy((char*)info.lo_file_name, name, LO_NAME_SIZE); if (ioctl(loopfd, LOOP_SET_STATUS64, &info)) { PERROR("LOOP_SET_STATUS64"); goto failed; } close(loopfd); close(fd); return loopback; failed: close(loopfd); close(fd); g_free(loopback); return 0; } int dm_setup(uint64_t sectors, const gchar *encryption_key, const char *name, const gchar *device, const char *path, int discard) { /* Mount loopback device with dm-crypt using the encryption key. */ gchar *table = g_strdup_printf("0 %" PRIu64 " crypt " \ "aes-cbc-essiv:sha256 %s " \ "0 %s 0%s", sectors, encryption_key, device, discard ? " 1 allow_discards" : ""); if (!table) { PERROR("g_strdup_printf"); return 0; } const gchar *argv[] = { "/sbin/dmsetup", "create", name, "--noudevrules", "--noudevsync", "--table", table, NULL }; /* TODO(keescook): replace with call to libdevmapper. */ if (runcmd(argv, NULL) != 0) { g_free(table); return 0; } g_free(table); /* Make sure the dm-crypt device showed up. */ if (access(path, R_OK)) { ERROR("%s does not exist", path); return 0; } return 1; } int dm_teardown(const gchar *device) { const char *argv[] = { "/sbin/dmsetup", "remove", device, "--noudevrules", "--noudevsync", NULL }; /* TODO(keescook): replace with call to libdevmapper. */ if (runcmd(argv, NULL) != 0) return 0; return 1; } char *dm_get_key(const gchar *device) { gchar *output = NULL; char *key; int i; const char *argv[] = { "/sbin/dmsetup", "table", "--showkeys", device, NULL }; /* TODO(keescook): replace with call to libdevmapper. */ if (runcmd(argv, &output) != 0) return NULL; /* Key is 4th field in the output. */ for (i = 0, key = strtok(output, " "); i < 4 && key; ++i, key = strtok(NULL, " ")) { } /* Create a copy of the key and free the output buffer. */ if (key) { key = strdup(key); g_free(output); } return key; } int sparse_create(const char *path, uint64_t bytes) { int sparsefd; sparsefd = open(path, O_RDWR | O_CREAT | O_EXCL | O_NOFOLLOW, S_IRUSR | S_IWUSR); if (sparsefd < 0) goto out; if (ftruncate(sparsefd, bytes)) { int saved_errno = errno; close(sparsefd); unlink(path); errno = saved_errno; sparsefd = -1; } out: return sparsefd; } /* When creating a filesystem that will grow, the inode ratio is calculated * using the starting size not the hinted "resize" size, which means the * number of inodes can be highly constrained on tiny starting filesystems. * Instead, calculate what the correct inode ratio should be for a given * filesystem based on its expected starting and ending sizes. * * inode-ratio_mkfs = * * ceil(blocks_max / group-ratio) * size_mkfs * ------------------------------------------------------------------ * ceil(size_max / inode-ratio_max) * ceil(blocks_mkfs / group-ratio) */ static uint64_t get_inode_ratio(uint64_t block_bytes_in, uint64_t blocks_mkfs_in, uint64_t blocks_max_in) { double block_bytes = (double)block_bytes_in; double blocks_mkfs = (double)blocks_mkfs_in; double blocks_max = (double)blocks_max_in; double size_max, size_mkfs, groups_max, groups_mkfs, inodes_max; double denom, inode_ratio_mkfs; size_max = block_bytes * blocks_max; size_mkfs = block_bytes * blocks_mkfs; groups_max = ceil(blocks_max / kBlocksPerGroup); groups_mkfs = ceil(blocks_mkfs / kBlocksPerGroup); inodes_max = ceil(size_max / kInodeRatioDefault); denom = inodes_max * groups_mkfs; /* Make sure we never trigger divide-by-zero. */ if (denom == 0.0) goto failure; inode_ratio_mkfs = (groups_max * size_mkfs) / denom; /* Make sure we never calculate anything totally huge. */ if (inode_ratio_mkfs > blocks_mkfs) goto failure; /* Make sure we never calculate anything totally tiny. */ if (inode_ratio_mkfs < kInodeRatioMinimum) goto failure; return (uint64_t)inode_ratio_mkfs; failure: return kInodeRatioDefault; } /* Creates an ext4 filesystem. * device: path to block device to create filesystem on. * block_bytes: bytes per block to use for filesystem. * blocks_min: starting number of blocks on filesystem. * blocks_max: largest expected size in blocks of filesystem, for growth hints. * * Returns 1 on success, 0 on failure. */ int filesystem_build(const char *device, uint64_t block_bytes, uint64_t blocks_min, uint64_t blocks_max) { int rc = 0; uint64_t inode_ratio; gchar *blocksize = g_strdup_printf("%" PRIu64, block_bytes); if (!blocksize) { PERROR("g_strdup_printf"); goto out; } gchar *blocks_str; blocks_str = g_strdup_printf("%" PRIu64, blocks_min); if (!blocks_str) { PERROR("g_strdup_printf"); goto free_blocksize; } gchar *extended; if (blocks_min < blocks_max) { extended = g_strdup_printf("%s,resize=%" PRIu64, kExt4ExtendedOptions, blocks_max); } else { extended = g_strdup_printf("%s", kExt4ExtendedOptions); } if (!extended) { PERROR("g_strdup_printf"); goto free_blocks_str; } inode_ratio = get_inode_ratio(block_bytes, blocks_min, blocks_max); gchar *inode_ratio_str = g_strdup_printf("%" PRIu64, inode_ratio); if (!inode_ratio_str) { PERROR("g_strdup_printf"); goto free_extended; } const gchar *mkfs[] = { "/sbin/mkfs.ext4", "-T", "default", "-b", blocksize, "-m", "0", "-O", "^huge_file,^flex_bg", "-i", inode_ratio_str, "-E", extended, device, blocks_str, NULL }; rc = (runcmd(mkfs, NULL) == 0); if (!rc) goto free_inode_ratio_str; const gchar *tune2fs[] = { "/sbin/tune2fs", "-c", "0", "-i", "0", device, NULL }; rc = (runcmd(tune2fs, NULL) == 0); free_inode_ratio_str: g_free(inode_ratio_str); free_extended: g_free(extended); free_blocks_str: g_free(blocks_str); free_blocksize: g_free(blocksize); out: return rc; } /* Spawns a filesystem resizing process. */ int filesystem_resize(const char *device, uint64_t blocks, uint64_t blocks_max) { /* Ignore resizing if we know the filesystem was built to max size. */ if (blocks >= blocks_max) { INFO("Resizing aborted. blocks:%" PRIu64 " >= blocks_max:%" PRIu64, blocks, blocks_max); return 1; } /* TODO(keescook): Read superblock to find out the current size of * the filesystem (since statvfs does not report the correct value). * For now, instead of doing multi-step resizing, just resize to the * full size of the block device in one step. */ blocks = blocks_max; INFO("Resizing started in %d second steps.", kResizeStepSeconds); do { gchar *blocks_str; sleep(kResizeStepSeconds); blocks += kResizeBlocks; if (blocks > blocks_max) blocks = blocks_max; blocks_str = g_strdup_printf("%" PRIu64, blocks); if (!blocks_str) { PERROR("g_strdup_printf"); return 0; } const gchar *resize[] = { "/sbin/resize2fs", "-f", device, blocks_str, NULL }; INFO("Resizing filesystem on %s to %" PRIu64 ".", device, blocks); if (runcmd(resize, NULL)) { ERROR("resize2fs failed"); return 0; } g_free(blocks_str); } while (blocks < blocks_max); INFO("Resizing finished."); return 1; } char *keyfile_read(const char *keyfile, uint8_t *system_key) { char *key = NULL; unsigned char *cipher = NULL; gsize length; uint8_t *plain = NULL; int plain_length, final_len; GError *error = NULL; EVP_CIPHER_CTX ctx; const EVP_CIPHER *algo = EVP_aes_256_cbc(); DEBUG("Reading keyfile %s", keyfile); if (EVP_CIPHER_key_length(algo) != DIGEST_LENGTH) { ERROR("cipher key size mismatch (got %d, want %d)", EVP_CIPHER_key_length(algo), DIGEST_LENGTH); goto out; } if (access(keyfile, R_OK)) { /* This file being missing is handled in caller, so * do not emit error message. */ INFO("%s does not exist.", keyfile); goto out; } if (!g_file_get_contents(keyfile, (gchar **)&cipher, &length, &error)) { ERROR("Unable to read %s: %s", keyfile, error->message); g_error_free(error); goto out; } plain = malloc(length + EVP_CIPHER_block_size(algo)); if (!plain) { PERROR("malloc"); goto free_cipher; } DEBUG("Decrypting keyfile %s", keyfile); /* Use the default IV. */ if (!EVP_DecryptInit(&ctx, algo, system_key, NULL)) { SSL_ERROR("EVP_DecryptInit"); goto free_plain; } if (!EVP_DecryptUpdate(&ctx, plain, &plain_length, cipher, length)) { SSL_ERROR("EVP_DecryptUpdate"); goto free_ctx; } if (!EVP_DecryptFinal(&ctx, plain+plain_length, &final_len)) { SSL_ERROR("EVP_DecryptFinal"); goto free_ctx; } plain_length += final_len; if (plain_length != DIGEST_LENGTH) { ERROR("Decrypted encryption key length (%d) is not %d.", plain_length, DIGEST_LENGTH); goto free_ctx; } debug_dump_hex("encryption key", plain, DIGEST_LENGTH); key = stringify_hex(plain, DIGEST_LENGTH); free_ctx: EVP_CIPHER_CTX_cleanup(&ctx); free_plain: free(plain); free_cipher: g_free(cipher); out: DEBUG("key:%p", key); return key; } int keyfile_write(const char *keyfile, uint8_t *system_key, char *string) { int rc = 0; size_t length; uint8_t plain[DIGEST_LENGTH]; uint8_t *cipher = NULL; int cipher_length, final_len; GError *error = NULL; EVP_CIPHER_CTX ctx; const EVP_CIPHER *algo = EVP_aes_256_cbc(); mode_t mask; DEBUG("Staring to process keyfile %s", keyfile); /* Have key file be read/write only by root user. */ mask = umask(0077); if (EVP_CIPHER_key_length(algo) != DIGEST_LENGTH) { ERROR("cipher key size mismatch (got %d, want %d)", EVP_CIPHER_key_length(algo), DIGEST_LENGTH); goto out; } if (access(keyfile, R_OK) == 0) { ERROR("%s already exists.", keyfile); goto out; } length = strlen(string); if (length != sizeof(plain) * 2) { ERROR("Encryption key string length (%zu) is not %zu.", length, sizeof(plain) * 2); goto out; } length = sizeof(plain); if (!hexify_string(string, plain, length)) { ERROR("Failed to convert encryption key to byte array"); goto out; } debug_dump_hex("encryption key", plain, DIGEST_LENGTH); cipher = malloc(length + EVP_CIPHER_block_size(algo)); if (!cipher) { PERROR("malloc"); goto out; } DEBUG("Encrypting keyfile %s", keyfile); /* Use the default IV. */ if (!EVP_EncryptInit(&ctx, algo, system_key, NULL)) { SSL_ERROR("EVP_EncryptInit"); goto free_cipher; } if (!EVP_EncryptUpdate(&ctx, cipher, &cipher_length, (unsigned char *)plain, length)) { SSL_ERROR("EVP_EncryptUpdate"); goto free_ctx; } if (!EVP_EncryptFinal(&ctx, cipher+cipher_length, &final_len)) { SSL_ERROR("EVP_EncryptFinal"); goto free_ctx; } length = cipher_length + final_len; DEBUG("Writing %zu bytes to %s", length, keyfile); /* TODO(keescook): use fd here, and set secure delete. Unsupported * by ext4 currently. :( * int f; * ioctl(fd, EXT2_IOC_GETFLAGS, &f); * f |= EXT2_SECRM_FL; * ioctl(fd, EXT2_IOC_SETFLAGS, &f); */ if (!g_file_set_contents(keyfile, (gchar *)cipher, length, &error)) { ERROR("Unable to write %s: %s", keyfile, error->message); g_error_free(error); goto free_ctx; } rc = 1; free_ctx: EVP_CIPHER_CTX_cleanup(&ctx); free_cipher: free(cipher); out: umask(mask); DEBUG("keyfile write rc:%d", rc); return rc; }