From 27cb7e7f6abc17e4df60fc80085c560a058a0c72 Mon Sep 17 00:00:00 2001 From: Shikha Panwar Date: Thu, 13 Oct 2022 20:34:45 +0000 Subject: [PATCH] Extend libdm_rust to build dm-crypt device Add crypt module which supports building DmCryptTarget based dm table entry. Also expose this via create_crypt_device() Also, add unit test that use loopdevice as the backing device. Test: atest libdm_rust.test Bug: 250880499 Change-Id: Ibf09ca267cbcc7a2dc00dd835f2d8b781040f130 --- libs/devicemapper/Android.bp | 2 +- libs/devicemapper/src/crypt.rs | 153 ++++++++++++++++++++++++++++ libs/devicemapper/src/lib.rs | 162 +++++++++++++++++++++++++++--- libs/devicemapper/src/util.rs | 15 +++ libs/devicemapper/testdata/rand8k | Bin 0 -> 8192 bytes 5 files changed, 316 insertions(+), 16 deletions(-) create mode 100644 libs/devicemapper/src/crypt.rs create mode 100644 libs/devicemapper/testdata/rand8k diff --git a/libs/devicemapper/Android.bp b/libs/devicemapper/Android.bp index 088b3203..783fa797 100644 --- a/libs/devicemapper/Android.bp +++ b/libs/devicemapper/Android.bp @@ -36,5 +36,5 @@ rust_test { "libscopeguard", "libtempfile", ], - data: ["tests/data/*"], + data: ["testdata/*"], } diff --git a/libs/devicemapper/src/crypt.rs b/libs/devicemapper/src/crypt.rs new file mode 100644 index 00000000..9b715a55 --- /dev/null +++ b/libs/devicemapper/src/crypt.rs @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2022 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/// `crypt` module implements the "crypt" target in the device mapper framework. Specifically, +/// it provides `DmCryptTargetBuilder` struct which is used to construct a `DmCryptTarget` struct +/// which is then given to `DeviceMapper` to create a mapper device. +use crate::util::*; +use crate::DmTargetSpec; + +use anyhow::{bail, Context, Result}; +use data_model::DataInit; +use std::io::Write; +use std::mem::size_of; +use std::path::Path; + +const SECTOR_SIZE: u64 = 512; + +// The UAPI for the crypt target is at: +// Documentation/admin-guide/device-mapper/dm-crypt.rst + +/// Supported ciphers +pub enum CipherType { + // TODO(b/253394457) Include ciphers with authenticated modes as well + AES256XTS, +} + +pub struct DmCryptTarget(Box<[u8]>); + +impl DmCryptTarget { + /// Flatten into slice + pub fn as_slice(&self) -> &[u8] { + self.0.as_ref() + } +} + +pub struct DmCryptTargetBuilder<'a> { + cipher: CipherType, + key: Option<&'a [u8]>, + iv_offset: u64, + device_path: Option<&'a Path>, + offset: u64, + device_size: u64, + // TODO(b/238179332) Extend this to include opt_params, in particular 'integrity' +} + +impl<'a> Default for DmCryptTargetBuilder<'a> { + fn default() -> Self { + DmCryptTargetBuilder { + cipher: CipherType::AES256XTS, + key: None, + iv_offset: 0, + device_path: None, + offset: 0, + device_size: 0, + } + } +} + +impl<'a> DmCryptTargetBuilder<'a> { + /// Sets the device that will be used as the data device (i.e. providing actual data). + pub fn data_device(&mut self, p: &'a Path, size: u64) -> &mut Self { + self.device_path = Some(p); + self.device_size = size; + self + } + + /// Sets the encryption cipher. + pub fn cipher(&mut self, cipher: CipherType) -> &mut Self { + self.cipher = cipher; + self + } + + /// Sets the key used for encryption. Input is byte array. + pub fn key(&mut self, key: &'a [u8]) -> &mut Self { + self.key = Some(key); + self + } + + /// The IV offset is a sector count that is added to the sector number before creating the IV. + pub fn iv_offset(&mut self, iv_offset: u64) -> &mut Self { + self.iv_offset = iv_offset; + self + } + + /// Starting sector within the device where the encrypted data begins + pub fn offset(&mut self, offset: u64) -> &mut Self { + self.offset = offset; + self + } + + /// Constructs a `DmCryptTarget`. + pub fn build(&self) -> Result { + // The `DmCryptTarget` struct actually is a flattened data consisting of a header and + // body. The format of the header is `dm_target_spec` as defined in + // include/uapi/linux/dm-ioctl.h. + let device_path = self + .device_path + .context("data device is not set")? + .to_str() + .context("data device path is not encoded in utf8")?; + + let key = + if let Some(key) = self.key { hexstring_from(key) } else { bail!("key is not set") }; + + // Step2: serialize the information according to the spec, which is ... + // DmTargetSpec{...} + // \ + // [<#opt_params> ] + let mut body = String::new(); + use std::fmt::Write; + write!(&mut body, "{} ", get_kernel_crypto_name(&self.cipher))?; + write!(&mut body, "{} ", key)?; + write!(&mut body, "{} ", self.iv_offset)?; + write!(&mut body, "{} ", device_path)?; + write!(&mut body, "{} ", self.offset)?; + write!(&mut body, "\0")?; // null terminator + + let size = size_of::() + body.len(); + let aligned_size = (size + 7) & !7; // align to 8 byte boundaries + let padding = aligned_size - size; + + let mut header = DmTargetSpec::new("crypt")?; + header.sector_start = 0; + header.length = self.device_size / SECTOR_SIZE; // number of 512-byte sectors + header.next = aligned_size as u32; + + let mut buf = Vec::with_capacity(aligned_size); + buf.write_all(header.as_slice())?; + buf.write_all(body.as_bytes())?; + buf.write_all(vec![0; padding].as_slice())?; + + Ok(DmCryptTarget(buf.into_boxed_slice())) + } +} + +fn get_kernel_crypto_name(cipher: &CipherType) -> &str { + match cipher { + CipherType::AES256XTS => "aes-xts-plain64", + } +} diff --git a/libs/devicemapper/src/lib.rs b/libs/devicemapper/src/lib.rs index 938ca0f2..b9fb5c35 100644 --- a/libs/devicemapper/src/lib.rs +++ b/libs/devicemapper/src/lib.rs @@ -38,6 +38,8 @@ use std::mem::size_of; use std::os::unix::io::AsRawFd; use std::path::{Path, PathBuf}; +/// Exposes DmCryptTarget & related builder +pub mod crypt; /// Expose util functions pub mod util; /// Exposes the DmVerityTarget & related builder @@ -46,9 +48,10 @@ pub mod verity; pub mod loopdevice; mod sys; +use crypt::DmCryptTarget; use sys::*; use util::*; -use verity::*; +use verity::DmVerityTarget; nix::ioctl_readwrite!(_dm_dev_create, DM_IOCTL, Cmd::DM_DEV_CREATE, DmIoctl); nix::ioctl_readwrite!(_dm_dev_suspend, DM_IOCTL, Cmd::DM_DEV_SUSPEND, DmIoctl); @@ -151,27 +154,55 @@ impl DeviceMapper { Ok(DeviceMapper(f)) } - /// Creates a device mapper device and configure it according to the `target` specification. + /// Creates a (crypt) device and configure it according to the `target` specification. + /// The path to the generated device is "/dev/mapper/". + pub fn create_crypt_device(&self, name: &str, target: &DmCryptTarget) -> Result { + self.create_device(name, target.as_slice(), uuid("crypto".as_bytes())?, true) + } + + /// Creates a (verity) device and configure it according to the `target` specification. /// The path to the generated device is "/dev/mapper/". pub fn create_verity_device(&self, name: &str, target: &DmVerityTarget) -> Result { + self.create_device(name, target.as_slice(), uuid("apkver".as_bytes())?, false) + } + + /// Removes a mapper device. + pub fn delete_device_deferred(&self, name: &str) -> Result<()> { + let mut data = DmIoctl::new(name)?; + data.flags |= Flag::DM_DEFERRED_REMOVE; + dm_dev_remove(self, &mut data) + .context(format!("failed to remove device with name {}", &name))?; + Ok(()) + } + + fn create_device( + &self, + name: &str, + target: &[u8], + uid: String, + writable: bool, + ) -> Result { // Step 1: create an empty device let mut data = DmIoctl::new(name)?; - data.set_uuid(&uuid("apkver".as_bytes())?)?; + data.set_uuid(&uid)?; dm_dev_create(self, &mut data) .context(format!("failed to create an empty device with name {}", &name))?; // Step 2: load table onto the device - let payload_size = size_of::() + target.as_slice().len(); + let payload_size = size_of::() + target.len(); let mut data = DmIoctl::new(name)?; data.data_size = payload_size as u32; data.data_start = size_of::() as u32; data.target_count = 1; - data.flags |= Flag::DM_READONLY_FLAG; + + if !writable { + data.flags |= Flag::DM_READONLY_FLAG; + } let mut payload = Vec::with_capacity(payload_size); payload.extend_from_slice(data.as_slice()); - payload.extend_from_slice(target.as_slice()); + payload.extend_from_slice(target); dm_table_load(self, payload.as_mut_ptr() as *mut DmIoctl) .context("failed to load table")?; @@ -185,15 +216,6 @@ impl DeviceMapper { wait_for_path(&path)?; Ok(path) } - - /// Removes a mapper device - pub fn delete_device_deferred(&self, name: &str) -> Result<()> { - let mut data = DmIoctl::new(name)?; - data.flags |= Flag::DM_DEFERRED_REMOVE; - dm_dev_remove(self, &mut data) - .context(format!("failed to remove device with name {}", &name))?; - Ok(()) - } } /// Used to derive a UUID that uniquely identifies a device mapper device when creating it. @@ -208,3 +230,113 @@ fn uuid(node_id: &[u8]) -> Result { let uuid = Uuid::new_v1(ts, node_id)?; Ok(String::from(uuid.to_hyphenated().encode_lower(&mut Uuid::encode_buffer()))) } + +#[cfg(test)] +mod tests { + use super::*; + use crypt::DmCryptTargetBuilder; + use std::fs::{read, File, OpenOptions}; + use std::io::Write; + + const KEY: &[u8; 32] = b"thirtytwobyteslongreallylongword"; + const DIFFERENT_KEY: &[u8; 32] = b"drowgnolyllaergnolsetybowtytriht"; + + // Create a file in given temp directory with given size + fn prepare_tmpfile(test_dir: &Path, filename: &str, sz: u64) -> PathBuf { + let filepath = test_dir.join(filename); + let f = File::create(&filepath).unwrap(); + f.set_len(sz).unwrap(); + filepath + } + + fn write_to_dev(path: &Path, data: &[u8]) { + let mut f = OpenOptions::new().read(true).write(true).open(&path).unwrap(); + f.write_all(data).unwrap(); + } + + fn delete_device(dm: &DeviceMapper, name: &str) -> Result<()> { + dm.delete_device_deferred(name)?; + wait_for_path_disappears(Path::new(MAPPER_DEV_ROOT).join(&name))?; + Ok(()) + } + + #[test] + fn mapping_again_keeps_data() { + // This test creates 2 different crypt devices using same key backed by same data_device + // -> Write data on dev1 -> Check the data is visible & same on dev2 + let dm = DeviceMapper::new().unwrap(); + let inputimg = include_bytes!("../testdata/rand8k"); + let sz = inputimg.len() as u64; + + let test_dir = tempfile::TempDir::new().unwrap(); + let backing_file = prepare_tmpfile(test_dir.path(), "storage", sz); + let data_device = loopdevice::attach( + backing_file, + 0, + sz, + /*direct_io*/ true, + /*writable*/ true, + ) + .unwrap(); + scopeguard::defer! { + loopdevice::detach(&data_device).unwrap(); + _ = delete_device(&dm, "crypt1"); + _ = delete_device(&dm, "crypt2"); + } + + let target = + DmCryptTargetBuilder::default().data_device(&data_device, sz).key(KEY).build().unwrap(); + + let mut crypt_device = dm.create_crypt_device("crypt1", &target).unwrap(); + write_to_dev(&crypt_device, inputimg); + + // Recreate another device using same target spec & check if the content is the same + crypt_device = dm.create_crypt_device("crypt2", &target).unwrap(); + + let crypt = read(crypt_device).unwrap(); + assert_eq!(inputimg.len(), crypt.len()); // fail early if the size doesn't match + assert_eq!(inputimg, crypt.as_slice()); + } + + #[test] + fn data_inaccessible_with_diff_key() { + // This test creates 2 different crypt devices using different keys backed + // by same data_device -> Write data on dev1 -> Check the data is visible but not the same on dev2 + let dm = DeviceMapper::new().unwrap(); + let inputimg = include_bytes!("../testdata/rand8k"); + let sz = inputimg.len() as u64; + + let test_dir = tempfile::TempDir::new().unwrap(); + let backing_file = prepare_tmpfile(test_dir.path(), "storage", sz); + let data_device = loopdevice::attach( + backing_file, + 0, + sz, + /*direct_io*/ true, + /*writable*/ true, + ) + .unwrap(); + scopeguard::defer! { + loopdevice::detach(&data_device).unwrap(); + _ = delete_device(&dm, "crypt3"); + _ = delete_device(&dm, "crypt4"); + } + + let target = + DmCryptTargetBuilder::default().data_device(&data_device, sz).key(KEY).build().unwrap(); + let target2 = DmCryptTargetBuilder::default() + .data_device(&data_device, sz) + .key(DIFFERENT_KEY) + .build() + .unwrap(); + + let mut crypt_device = dm.create_crypt_device("crypt3", &target).unwrap(); + + write_to_dev(&crypt_device, inputimg); + + // Recreate the crypt device again diff key & check if the content is changed + crypt_device = dm.create_crypt_device("crypt4", &target2).unwrap(); + let crypt = read(crypt_device).unwrap(); + assert_ne!(inputimg, crypt.as_slice()); + } +} diff --git a/libs/devicemapper/src/util.rs b/libs/devicemapper/src/util.rs index 913f8278..e8df4244 100644 --- a/libs/devicemapper/src/util.rs +++ b/libs/devicemapper/src/util.rs @@ -37,6 +37,21 @@ pub fn wait_for_path>(path: P) -> Result<()> { Ok(()) } +/// Wait for the path to disappear +#[cfg(test)] +pub fn wait_for_path_disappears>(path: P) -> Result<()> { + const TIMEOUT: Duration = Duration::from_secs(1); + const INTERVAL: Duration = Duration::from_millis(10); + let begin = Instant::now(); + while !path.as_ref().exists() { + if begin.elapsed() > TIMEOUT { + bail!("{:?} not disappearing. TIMEOUT.", path.as_ref()); + } + thread::sleep(INTERVAL); + } + Ok(()) +} + /// Returns hexadecimal reprentation of a given byte array. pub fn hexstring_from(s: &[u8]) -> String { s.iter().map(|byte| format!("{:02x}", byte)).reduce(|i, j| i + &j).unwrap_or_default() diff --git a/libs/devicemapper/testdata/rand8k b/libs/devicemapper/testdata/rand8k new file mode 100644 index 0000000000000000000000000000000000000000..4172e33b49ec8fe64c4eb20cecda4dd008084482 GIT binary patch literal 8192 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