1 /*
2  * Copyright (C) 2022 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *      http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 //! `encryptedstore` is a program that (as the name indicates) provides encrypted storage
18 //! solution in a VM. This is based on dm-crypt & requires the (64 bytes') key & the backing device.
19 //! It uses dm_rust lib.
20 
21 use anyhow::{ensure, Context, Result};
22 use clap::arg;
23 use dm::{crypt::CipherType, util};
24 use log::{error, info};
25 use std::ffi::CString;
26 use std::fs::{create_dir_all, OpenOptions};
27 use std::io::{Error, Read, Write};
28 use std::os::unix::ffi::OsStrExt;
29 use std::os::unix::fs::{FileTypeExt, PermissionsExt};
30 use std::path::{Path, PathBuf};
31 use std::process::Command;
32 
33 const MK2FS_BIN: &str = "/system/bin/mke2fs";
34 const UNFORMATTED_STORAGE_MAGIC: &str = "UNFORMATTED-STORAGE";
35 
main()36 fn main() {
37     android_logger::init_once(
38         android_logger::Config::default()
39             .with_tag("encryptedstore")
40             .with_max_level(log::LevelFilter::Info),
41     );
42 
43     if let Err(e) = try_main() {
44         error!("{:?}", e);
45         std::process::exit(1)
46     }
47 }
48 
try_main() -> Result<()>49 fn try_main() -> Result<()> {
50     info!("Starting encryptedstore binary");
51 
52     let matches = clap_command().get_matches();
53 
54     let blkdevice = Path::new(matches.get_one::<String>("blkdevice").unwrap());
55     let key = matches.get_one::<String>("key").unwrap();
56     let mountpoint = Path::new(matches.get_one::<String>("mountpoint").unwrap());
57     // Note this error context is used in MicrodroidTests.
58     encryptedstore_init(blkdevice, key, mountpoint).with_context(|| {
59         format!(
60             "Unable to initialize encryptedstore on {:?} & mount at {:?}",
61             blkdevice, mountpoint
62         )
63     })?;
64     Ok(())
65 }
66 
clap_command() -> clap::Command67 fn clap_command() -> clap::Command {
68     clap::Command::new("encryptedstore").args(&[
69         arg!(--blkdevice <FILE> "the block device backing the encrypted storage").required(true),
70         arg!(--key <KEY> "key (in hex) equivalent to 32 bytes)").required(true),
71         arg!(--mountpoint <MOUNTPOINT> "mount point for the storage").required(true),
72     ])
73 }
74 
encryptedstore_init(blkdevice: &Path, key: &str, mountpoint: &Path) -> Result<()>75 fn encryptedstore_init(blkdevice: &Path, key: &str, mountpoint: &Path) -> Result<()> {
76     ensure!(
77         std::fs::metadata(blkdevice)
78             .with_context(|| format!("Failed to get metadata of {:?}", blkdevice))?
79             .file_type()
80             .is_block_device(),
81         "The path:{:?} is not of a block device",
82         blkdevice
83     );
84 
85     let needs_formatting =
86         needs_formatting(blkdevice).context("Unable to check if formatting is required")?;
87     let crypt_device =
88         enable_crypt(blkdevice, key, "cryptdev").context("Unable to map crypt device")?;
89 
90     // We might need to format it with filesystem if this is a "seen-for-the-first-time" device.
91     if needs_formatting {
92         info!("Freshly formatting the crypt device");
93         format_ext4(&crypt_device)?;
94     }
95     mount(&crypt_device, mountpoint)
96         .with_context(|| format!("Unable to mount {:?}", crypt_device))?;
97     if cfg!(multi_tenant) && needs_formatting {
98         set_root_dir_permissions(mountpoint)?;
99     }
100     Ok(())
101 }
102 
set_root_dir_permissions(mountpoint: &Path) -> Result<()>103 fn set_root_dir_permissions(mountpoint: &Path) -> Result<()> {
104     // mke2fs hardwires the root dir permissions as 0o755 which doesn't match what we want.
105     // We want to allow full access by both root and the payload group, and no access by anything
106     // else. And we want the sticky bit set, so different payload UIDs can create sub-directories
107     // that other payloads can't delete.
108     let permissions = PermissionsExt::from_mode(0o770 | libc::S_ISVTX);
109     std::fs::set_permissions(mountpoint, permissions).context("Failed to chmod root directory")
110 }
111 
enable_crypt(data_device: &Path, key: &str, name: &str) -> Result<PathBuf>112 fn enable_crypt(data_device: &Path, key: &str, name: &str) -> Result<PathBuf> {
113     let dev_size = util::blkgetsize64(data_device)?;
114     let key = hex::decode(key).context("Unable to decode hex key")?;
115 
116     // Create the dm-crypt spec
117     let target = dm::crypt::DmCryptTargetBuilder::default()
118         .data_device(data_device, dev_size)
119         .cipher(CipherType::AES256HCTR2)
120         .key(&key)
121         .opt_param("sector_size:4096")
122         .opt_param("iv_large_sectors")
123         .build()
124         .context("Couldn't build the DMCrypt target")?;
125     let dm = dm::DeviceMapper::new()?;
126     dm.create_crypt_device(name, &target).context("Failed to create dm-crypt device")
127 }
128 
129 // The disk contains UNFORMATTED_STORAGE_MAGIC to indicate we need to format the crypt device.
130 // This function looks for it, zeroing it, if present.
needs_formatting(data_device: &Path) -> Result<bool>131 fn needs_formatting(data_device: &Path) -> Result<bool> {
132     let mut file = OpenOptions::new()
133         .read(true)
134         .write(true)
135         .open(data_device)
136         .with_context(|| format!("Failed to open {:?}", data_device))?;
137 
138     let mut buf = [0; UNFORMATTED_STORAGE_MAGIC.len()];
139     file.read_exact(&mut buf)?;
140 
141     if buf == UNFORMATTED_STORAGE_MAGIC.as_bytes() {
142         buf.fill(0);
143         file.write_all(&buf)?;
144         return Ok(true);
145     }
146     Ok(false)
147 }
148 
format_ext4(device: &Path) -> Result<()>149 fn format_ext4(device: &Path) -> Result<()> {
150     let root_dir_uid_gid = format!(
151         "root_owner={}:{}",
152         microdroid_uids::ROOT_UID,
153         microdroid_uids::MICRODROID_PAYLOAD_GID
154     );
155     let mkfs_options = [
156         "-j", // Create appropriate sized journal
157         /* metadata_csum: enabled for filesystem integrity
158          * extents: Not enabling extents reduces the coverage of metadata checksumming.
159          * 64bit: larger fields afforded by this feature enable full-strength checksumming.
160          */
161         "-O metadata_csum, extents, 64bit",
162         "-b 4096", // block size in the filesystem,
163         "-E",
164         &root_dir_uid_gid,
165     ];
166     let mut cmd = Command::new(MK2FS_BIN);
167     let status = cmd
168         .args(mkfs_options)
169         .arg(device)
170         .status()
171         .with_context(|| format!("failed to execute {}", MK2FS_BIN))?;
172     ensure!(status.success(), "mkfs failed with {:?}", status);
173     Ok(())
174 }
175 
mount(source: &Path, mountpoint: &Path) -> Result<()>176 fn mount(source: &Path, mountpoint: &Path) -> Result<()> {
177     create_dir_all(mountpoint).with_context(|| format!("Failed to create {:?}", &mountpoint))?;
178     let mount_options = CString::new(
179         "fscontext=u:object_r:encryptedstore_fs:s0,context=u:object_r:encryptedstore_file:s0",
180     )
181     .unwrap();
182     let source = CString::new(source.as_os_str().as_bytes())?;
183     let mountpoint = CString::new(mountpoint.as_os_str().as_bytes())?;
184     let fstype = CString::new("ext4").unwrap();
185 
186     // SAFETY: The source, target and filesystemtype are valid C strings. For ext4, data is expected
187     // to be a C string as well, which it is. None of these pointers are retained after mount
188     // returns.
189     let ret = unsafe {
190         libc::mount(
191             source.as_ptr(),
192             mountpoint.as_ptr(),
193             fstype.as_ptr(),
194             libc::MS_NOSUID | libc::MS_NODEV | libc::MS_NOEXEC,
195             mount_options.as_ptr() as *const std::ffi::c_void,
196         )
197     };
198     if ret < 0 {
199         Err(Error::last_os_error()).context("mount failed")
200     } else {
201         Ok(())
202     }
203 }
204 
205 #[cfg(test)]
206 mod tests {
207     use super::*;
208 
209     #[test]
verify_command()210     fn verify_command() {
211         // Check that the command parsing has been configured in a valid way.
212         clap_command().debug_assert();
213     }
214 }
215