Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 1 | // Copyright 2023, The Android Open Source Project |
| 2 | // |
| 3 | // Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | // you may not use this file except in compliance with the License. |
| 5 | // You may obtain a copy of the License at |
| 6 | // |
| 7 | // http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | // |
| 9 | // Unless required by applicable law or agreed to in writing, software |
| 10 | // distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | // See the License for the specific language governing permissions and |
| 13 | // limitations under the License. |
| 14 | |
| 15 | //! Support for reading and writing to the instance.img. |
| 16 | |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 17 | use crate::crypto; |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 18 | use crate::crypto::AeadCtx; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 19 | use crate::dice::PartialInputs; |
| 20 | use crate::gpt; |
| 21 | use crate::gpt::Partition; |
| 22 | use crate::gpt::Partitions; |
Alice Wang | 947f3f7 | 2023-09-29 09:04:07 +0000 | [diff] [blame] | 23 | use bssl_avf::{self, hkdf, Digester}; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 24 | use core::fmt; |
| 25 | use core::mem::size_of; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 26 | use diced_open_dice::DiceMode; |
| 27 | use diced_open_dice::Hash; |
| 28 | use diced_open_dice::Hidden; |
| 29 | use log::trace; |
| 30 | use uuid::Uuid; |
Alice Wang | 0e08623 | 2023-06-12 13:47:40 +0000 | [diff] [blame] | 31 | use virtio_drivers::transport::{pci::bus::PciRoot, DeviceType, Transport}; |
Pierre-Clément Tosi | 3e3d733 | 2023-06-22 10:44:29 +0000 | [diff] [blame] | 32 | use vmbase::rand; |
Alice Wang | eacb738 | 2023-06-05 12:53:54 +0000 | [diff] [blame] | 33 | use vmbase::util::ceiling_div; |
Alice Wang | 0e08623 | 2023-06-12 13:47:40 +0000 | [diff] [blame] | 34 | use vmbase::virtio::pci::{PciTransportIterator, VirtIOBlk}; |
Alice Wang | 7c55c7d | 2023-07-05 14:51:40 +0000 | [diff] [blame] | 35 | use vmbase::virtio::HalImpl; |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 36 | use zerocopy::AsBytes; |
| 37 | use zerocopy::FromBytes; |
Frederick Mayle | 2e77994 | 2023-10-15 18:27:31 +0000 | [diff] [blame] | 38 | use zerocopy::FromZeroes; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 39 | |
| 40 | pub enum Error { |
| 41 | /// Unexpected I/O error while accessing the underlying disk. |
| 42 | FailedIo(gpt::Error), |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 43 | /// Failed to decrypt the entry. |
| 44 | FailedOpen(crypto::ErrorIterator), |
Pierre-Clément Tosi | a59103d | 2023-02-02 14:46:55 +0000 | [diff] [blame] | 45 | /// Failed to generate a random salt to be stored. |
| 46 | FailedSaltGeneration(rand::Error), |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 47 | /// Failed to encrypt the entry. |
| 48 | FailedSeal(crypto::ErrorIterator), |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 49 | /// Impossible to create a new instance.img entry. |
| 50 | InstanceImageFull, |
| 51 | /// Badly formatted instance.img header block. |
| 52 | InvalidInstanceImageHeader, |
| 53 | /// No instance.img ("vm-instance") partition found. |
| 54 | MissingInstanceImage, |
| 55 | /// The instance.img doesn't contain a header. |
| 56 | MissingInstanceImageHeader, |
| 57 | /// Authority hash found in the pvmfw instance.img entry doesn't match the trusted public key. |
| 58 | RecordedAuthHashMismatch, |
| 59 | /// Code hash found in the pvmfw instance.img entry doesn't match the inputs. |
| 60 | RecordedCodeHashMismatch, |
| 61 | /// DICE mode found in the pvmfw instance.img entry doesn't match the current one. |
| 62 | RecordedDiceModeMismatch, |
| 63 | /// Size of the instance.img entry being read or written is not supported. |
| 64 | UnsupportedEntrySize(usize), |
Alice Wang | 0e08623 | 2023-06-12 13:47:40 +0000 | [diff] [blame] | 65 | /// Failed to create VirtIO Block device. |
| 66 | VirtIOBlkCreationFailed(virtio_drivers::Error), |
Alice Wang | 947f3f7 | 2023-09-29 09:04:07 +0000 | [diff] [blame] | 67 | /// An error happened during the interaction with BoringSSL. |
| 68 | BoringSslFailed(bssl_avf::Error), |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 69 | } |
| 70 | |
| 71 | impl fmt::Display for Error { |
| 72 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
| 73 | match self { |
| 74 | Self::FailedIo(e) => write!(f, "Failed I/O to disk: {e}"), |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 75 | Self::FailedOpen(e_iter) => { |
| 76 | writeln!(f, "Failed to open the instance.img partition:")?; |
| 77 | for e in *e_iter { |
| 78 | writeln!(f, "\t{e}")?; |
| 79 | } |
| 80 | Ok(()) |
| 81 | } |
Pierre-Clément Tosi | a59103d | 2023-02-02 14:46:55 +0000 | [diff] [blame] | 82 | Self::FailedSaltGeneration(e) => write!(f, "Failed to generate salt: {e}"), |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 83 | Self::FailedSeal(e_iter) => { |
| 84 | writeln!(f, "Failed to seal the instance.img partition:")?; |
| 85 | for e in *e_iter { |
| 86 | writeln!(f, "\t{e}")?; |
| 87 | } |
| 88 | Ok(()) |
| 89 | } |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 90 | Self::InstanceImageFull => write!(f, "Failed to obtain a free instance.img partition"), |
| 91 | Self::InvalidInstanceImageHeader => write!(f, "instance.img header is invalid"), |
| 92 | Self::MissingInstanceImage => write!(f, "Failed to find the instance.img partition"), |
| 93 | Self::MissingInstanceImageHeader => write!(f, "instance.img header is missing"), |
| 94 | Self::RecordedAuthHashMismatch => write!(f, "Recorded authority hash doesn't match"), |
| 95 | Self::RecordedCodeHashMismatch => write!(f, "Recorded code hash doesn't match"), |
| 96 | Self::RecordedDiceModeMismatch => write!(f, "Recorded DICE mode doesn't match"), |
| 97 | Self::UnsupportedEntrySize(sz) => write!(f, "Invalid entry size: {sz}"), |
Alice Wang | 0e08623 | 2023-06-12 13:47:40 +0000 | [diff] [blame] | 98 | Self::VirtIOBlkCreationFailed(e) => { |
| 99 | write!(f, "Failed to create VirtIO Block device: {e}") |
| 100 | } |
Alice Wang | 947f3f7 | 2023-09-29 09:04:07 +0000 | [diff] [blame] | 101 | Self::BoringSslFailed(e) => { |
| 102 | write!(f, "An error happened during the interaction with BoringSSL: {e}") |
| 103 | } |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 104 | } |
| 105 | } |
| 106 | } |
| 107 | |
Alice Wang | 947f3f7 | 2023-09-29 09:04:07 +0000 | [diff] [blame] | 108 | impl From<bssl_avf::Error> for Error { |
| 109 | fn from(e: bssl_avf::Error) -> Self { |
| 110 | Self::BoringSslFailed(e) |
| 111 | } |
| 112 | } |
| 113 | |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 114 | pub type Result<T> = core::result::Result<T, Error>; |
| 115 | |
| 116 | pub fn get_or_generate_instance_salt( |
| 117 | pci_root: &mut PciRoot, |
| 118 | dice_inputs: &PartialInputs, |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 119 | secret: &[u8], |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 120 | ) -> Result<(bool, Hidden)> { |
| 121 | let mut instance_img = find_instance_img(pci_root)?; |
| 122 | |
| 123 | let entry = locate_entry(&mut instance_img)?; |
| 124 | trace!("Found pvmfw instance.img entry: {entry:?}"); |
| 125 | |
Alice Wang | 947f3f7 | 2023-09-29 09:04:07 +0000 | [diff] [blame] | 126 | let key = hkdf::<32>(secret, /* salt= */ &[], b"vm-instance", Digester::sha512())?; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 127 | let mut blk = [0; BLK_SIZE]; |
| 128 | match entry { |
| 129 | PvmfwEntry::Existing { header_index, payload_size } => { |
| 130 | if payload_size > blk.len() { |
| 131 | // We currently only support single-blk entries. |
| 132 | return Err(Error::UnsupportedEntrySize(payload_size)); |
| 133 | } |
| 134 | let payload_index = header_index + 1; |
| 135 | instance_img.read_block(payload_index, &mut blk).map_err(Error::FailedIo)?; |
| 136 | |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 137 | let payload = &blk[..payload_size]; |
| 138 | let mut entry = [0; size_of::<EntryBody>()]; |
Alice Wang | ccc52e5 | 2023-10-03 13:14:09 +0000 | [diff] [blame] | 139 | let aead = |
| 140 | AeadCtx::new_aes_256_gcm_randnonce(key.as_slice()).map_err(Error::FailedOpen)?; |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 141 | let decrypted = aead.open(&mut entry, payload).map_err(Error::FailedOpen)?; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 142 | |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 143 | let body = EntryBody::read_from(decrypted).unwrap(); |
Alice Wang | 12e4c86 | 2023-12-11 13:52:27 +0000 | [diff] [blame^] | 144 | if dice_inputs.rkp_vm_marker { |
| 145 | // The RKP VM is allowed to run if it has passed the verified boot check and |
| 146 | // contains the expected version in its AVB footer. |
| 147 | // The comparison below with the previous boot information is skipped to enable the |
| 148 | // simultaneous update of the pvmfw and RKP VM. |
| 149 | // For instance, when both the pvmfw and RKP VM are updated, the code hash of the |
| 150 | // RKP VM will differ from the one stored in the instance image. In this case, the |
| 151 | // RKP VM is still allowed to run. |
| 152 | // This ensures that the updated RKP VM will retain the same CDIs in the next stage. |
| 153 | return Ok((false, body.salt)); |
| 154 | } |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 155 | if body.code_hash != dice_inputs.code_hash { |
| 156 | Err(Error::RecordedCodeHashMismatch) |
| 157 | } else if body.auth_hash != dice_inputs.auth_hash { |
| 158 | Err(Error::RecordedAuthHashMismatch) |
| 159 | } else if body.mode() != dice_inputs.mode { |
| 160 | Err(Error::RecordedDiceModeMismatch) |
| 161 | } else { |
| 162 | Ok((false, body.salt)) |
| 163 | } |
| 164 | } |
| 165 | PvmfwEntry::New { header_index } => { |
Pierre-Clément Tosi | a59103d | 2023-02-02 14:46:55 +0000 | [diff] [blame] | 166 | let salt = rand::random_array().map_err(Error::FailedSaltGeneration)?; |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 167 | let body = EntryBody::new(dice_inputs, &salt); |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 168 | |
Alice Wang | ccc52e5 | 2023-10-03 13:14:09 +0000 | [diff] [blame] | 169 | let aead = |
| 170 | AeadCtx::new_aes_256_gcm_randnonce(key.as_slice()).map_err(Error::FailedSeal)?; |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 171 | // We currently only support single-blk entries. |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 172 | let plaintext = body.as_bytes(); |
| 173 | assert!(plaintext.len() + aead.aead().unwrap().max_overhead() < blk.len()); |
| 174 | let encrypted = aead.seal(&mut blk, plaintext).map_err(Error::FailedSeal)?; |
Pierre-Clément Tosi | 90cd4f1 | 2023-02-17 11:19:56 +0000 | [diff] [blame] | 175 | let payload_size = encrypted.len(); |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 176 | let payload_index = header_index + 1; |
| 177 | instance_img.write_block(payload_index, &blk).map_err(Error::FailedIo)?; |
| 178 | |
| 179 | let header = EntryHeader::new(PvmfwEntry::UUID, payload_size); |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 180 | header.write_to_prefix(blk.as_mut_slice()).unwrap(); |
| 181 | blk[header.as_bytes().len()..].fill(0); |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 182 | instance_img.write_block(header_index, &blk).map_err(Error::FailedIo)?; |
| 183 | |
| 184 | Ok((true, salt)) |
| 185 | } |
| 186 | } |
| 187 | } |
| 188 | |
Frederick Mayle | 2e77994 | 2023-10-15 18:27:31 +0000 | [diff] [blame] | 189 | #[derive(FromZeroes, FromBytes)] |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 190 | #[repr(C, packed)] |
| 191 | struct Header { |
| 192 | magic: [u8; Header::MAGIC.len()], |
| 193 | version: u16, |
| 194 | } |
| 195 | |
| 196 | impl Header { |
| 197 | const MAGIC: &[u8] = b"Android-VM-instance"; |
| 198 | const VERSION_1: u16 = 1; |
| 199 | |
| 200 | pub fn is_valid(&self) -> bool { |
| 201 | self.magic == Self::MAGIC && self.version() == Self::VERSION_1 |
| 202 | } |
| 203 | |
| 204 | fn version(&self) -> u16 { |
| 205 | u16::from_le(self.version) |
| 206 | } |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 207 | } |
| 208 | |
| 209 | fn find_instance_img(pci_root: &mut PciRoot) -> Result<Partition> { |
Alice Wang | 7c55c7d | 2023-07-05 14:51:40 +0000 | [diff] [blame] | 210 | for transport in PciTransportIterator::<HalImpl>::new(pci_root) |
| 211 | .filter(|t| DeviceType::Block == t.device_type()) |
Alice Wang | 0e08623 | 2023-06-12 13:47:40 +0000 | [diff] [blame] | 212 | { |
Alice Wang | 7c55c7d | 2023-07-05 14:51:40 +0000 | [diff] [blame] | 213 | let device = |
| 214 | VirtIOBlk::<HalImpl>::new(transport).map_err(Error::VirtIOBlkCreationFailed)?; |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 215 | match Partition::get_by_name(device, "vm-instance") { |
| 216 | Ok(Some(p)) => return Ok(p), |
| 217 | Ok(None) => {} |
| 218 | Err(e) => log::warn!("error while reading from disk: {e}"), |
| 219 | }; |
| 220 | } |
| 221 | |
| 222 | Err(Error::MissingInstanceImage) |
| 223 | } |
| 224 | |
| 225 | #[derive(Debug)] |
| 226 | enum PvmfwEntry { |
| 227 | Existing { header_index: usize, payload_size: usize }, |
| 228 | New { header_index: usize }, |
| 229 | } |
| 230 | |
| 231 | const BLK_SIZE: usize = Partitions::LBA_SIZE; |
| 232 | |
| 233 | impl PvmfwEntry { |
| 234 | const UUID: Uuid = Uuid::from_u128(0x90d2174a038a4bc6adf3824848fc5825); |
| 235 | } |
| 236 | |
| 237 | fn locate_entry(partition: &mut Partition) -> Result<PvmfwEntry> { |
| 238 | let mut blk = [0; BLK_SIZE]; |
| 239 | let mut indices = partition.indices(); |
| 240 | let header_index = indices.next().ok_or(Error::MissingInstanceImageHeader)?; |
| 241 | partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?; |
| 242 | // The instance.img header is only used for discovery/validation. |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 243 | let header = Header::read_from_prefix(blk.as_slice()).unwrap(); |
| 244 | if !header.is_valid() { |
| 245 | return Err(Error::InvalidInstanceImageHeader); |
| 246 | } |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 247 | |
| 248 | while let Some(header_index) = indices.next() { |
| 249 | partition.read_block(header_index, &mut blk).map_err(Error::FailedIo)?; |
| 250 | |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 251 | let header = EntryHeader::read_from_prefix(blk.as_slice()).unwrap(); |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 252 | match (header.uuid(), header.payload_size()) { |
| 253 | (uuid, _) if uuid.is_nil() => return Ok(PvmfwEntry::New { header_index }), |
| 254 | (PvmfwEntry::UUID, payload_size) => { |
| 255 | return Ok(PvmfwEntry::Existing { header_index, payload_size }) |
| 256 | } |
| 257 | (uuid, payload_size) => { |
| 258 | trace!("Skipping instance.img entry {uuid}: {payload_size:?} bytes"); |
| 259 | let n = ceiling_div(payload_size, BLK_SIZE).unwrap(); |
| 260 | if n > 0 { |
| 261 | let _ = indices.nth(n - 1); // consume |
| 262 | } |
| 263 | } |
| 264 | }; |
| 265 | } |
| 266 | |
| 267 | Err(Error::InstanceImageFull) |
| 268 | } |
| 269 | |
| 270 | /// Marks the start of an instance.img entry. |
| 271 | /// |
| 272 | /// Note: Virtualization/microdroid_manager/src/instance.rs uses the name "partition". |
Frederick Mayle | 2e77994 | 2023-10-15 18:27:31 +0000 | [diff] [blame] | 273 | #[derive(AsBytes, FromZeroes, FromBytes)] |
Pierre-Clément Tosi | 8ad980f | 2023-04-25 18:23:11 +0100 | [diff] [blame] | 274 | #[repr(C, packed)] |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 275 | struct EntryHeader { |
| 276 | uuid: u128, |
| 277 | payload_size: u64, |
| 278 | } |
| 279 | |
| 280 | impl EntryHeader { |
| 281 | fn new(uuid: Uuid, payload_size: usize) -> Self { |
Pierre-Clément Tosi | afb126a | 2023-03-29 14:42:19 +0100 | [diff] [blame] | 282 | Self { uuid: uuid.to_u128_le(), payload_size: u64::try_from(payload_size).unwrap().to_le() } |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 283 | } |
| 284 | |
| 285 | fn uuid(&self) -> Uuid { |
Pierre-Clément Tosi | afb126a | 2023-03-29 14:42:19 +0100 | [diff] [blame] | 286 | Uuid::from_u128_le(self.uuid) |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 287 | } |
| 288 | |
| 289 | fn payload_size(&self) -> usize { |
| 290 | usize::try_from(u64::from_le(self.payload_size)).unwrap() |
| 291 | } |
| 292 | } |
| 293 | |
Frederick Mayle | 2e77994 | 2023-10-15 18:27:31 +0000 | [diff] [blame] | 294 | #[derive(AsBytes, FromZeroes, FromBytes)] |
Pierre-Clément Tosi | 1cc5eb7 | 2023-02-02 11:09:18 +0000 | [diff] [blame] | 295 | #[repr(C)] |
| 296 | struct EntryBody { |
| 297 | code_hash: Hash, |
| 298 | auth_hash: Hash, |
| 299 | salt: Hidden, |
| 300 | mode: u8, |
| 301 | } |
| 302 | |
| 303 | impl EntryBody { |
| 304 | fn new(dice_inputs: &PartialInputs, salt: &Hidden) -> Self { |
| 305 | let mode = match dice_inputs.mode { |
| 306 | DiceMode::kDiceModeNotInitialized => 0, |
| 307 | DiceMode::kDiceModeNormal => 1, |
| 308 | DiceMode::kDiceModeDebug => 2, |
| 309 | DiceMode::kDiceModeMaintenance => 3, |
| 310 | }; |
| 311 | |
| 312 | Self { |
| 313 | code_hash: dice_inputs.code_hash, |
| 314 | auth_hash: dice_inputs.auth_hash, |
| 315 | salt: *salt, |
| 316 | mode, |
| 317 | } |
| 318 | } |
| 319 | |
| 320 | fn mode(&self) -> DiceMode { |
| 321 | match self.mode { |
| 322 | 1 => DiceMode::kDiceModeNormal, |
| 323 | 2 => DiceMode::kDiceModeDebug, |
| 324 | 3 => DiceMode::kDiceModeMaintenance, |
| 325 | _ => DiceMode::kDiceModeNotInitialized, |
| 326 | } |
| 327 | } |
| 328 | } |