// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.

use std::{
    convert::TryFrom,
    fmt::{self, Debug},
    io,
    path::{Path, PathBuf},
    sync::Arc,
};

use data_encoding::BASE64URL_NOPAD;
use either::Either;
use serde_json::{Map, Value};
use sha1::{Digest, Sha1};

use devicemapper::Sectors;
use libcryptsetup_rs::{
    c_uint, CryptActivateFlags, CryptDeactivateFlags, CryptDevice, CryptInit, CryptStatusInfo,
    CryptVolumeKeyFlags, CryptWipePattern, EncryptionFormat, KeyslotsSize, LibcryptErr,
    MetadataSize, TokenInput,
};

use crate::{
    engine::{
        strat_engine::{
            cmd::{clevis_decrypt, clevis_luks_bind, clevis_luks_unbind, clevis_luks_unlock},
            keys::{self, MemoryPrivateFilesystem},
            metadata::StratisIdentifiers,
            names::format_crypt_name,
        },
        types::{
            BlockDevPath, DevUuid, EncryptionInfo, KeyDescription, PoolUuid, SizedKeyMemory,
            UnlockMethod,
        },
    },
    stratis::{StratisError, StratisResult},
};

// Stratis token JSON keys
const TOKEN_TYPE_KEY: &str = "type";
const TOKEN_KEYSLOTS_KEY: &str = "keyslots";
const STRATIS_TOKEN_DEVNAME_KEY: &str = "activation_name";
const STRATIS_TOKEN_POOL_UUID_KEY: &str = "pool_uuid";
const STRATIS_TOKEN_DEV_UUID_KEY: &str = "device_uuid";

const STRATIS_TOKEN_ID: c_uint = 0;
const LUKS2_TOKEN_ID: c_uint = 1;
const CLEVIS_LUKS_TOKEN_ID: c_uint = 2;

const LUKS2_TOKEN_TYPE: &str = "luks2-keyring";
const STRATIS_TOKEN_TYPE: &str = "stratis";

/// The size of the media encryption key generated by cryptsetup for
/// each block device.
const STRATIS_MEK_SIZE: usize = 512 / 8;

/// Sector size as determined in `cryptsetup/lib/internal.h`
const SECTOR_SIZE: u64 = 512;

/// Path to logical devices for encrypted devices
const DEVICEMAPPER_PATH: &str = "/dev/mapper";

/// Key in clevis configuration for tang indicating that the URL of the
/// tang server does not need to be verified.
pub const CLEVIS_TANG_TRUST_URL: &str = "stratis:tang:trust_url";

const DEFAULT_CRYPT_METADATA_SIZE: u64 = 16384;
const DEFAULT_CRYPT_KEYSLOTS_SIZE: u64 = 16_744_448;

macro_rules! log_on_failure {
    ($op:expr, $fmt:tt $(, $arg:expr)*) => {{
        let result = $op;
        if let Err(ref e) = result {
            warn!(
                concat!($fmt, "; failed with error: {}"),
                $($arg,)*
                e
            );
        }
        result?
    }}
}

struct StratisLuks2Token {
    devname: String,
    identifiers: StratisIdentifiers,
}

impl Into<Value> for StratisLuks2Token {
    fn into(self) -> Value {
        json!({
            TOKEN_TYPE_KEY: STRATIS_TOKEN_TYPE,
            TOKEN_KEYSLOTS_KEY: [],
            STRATIS_TOKEN_DEVNAME_KEY: self.devname,
            STRATIS_TOKEN_POOL_UUID_KEY: self.identifiers.pool_uuid.to_string(),
            STRATIS_TOKEN_DEV_UUID_KEY: self.identifiers.device_uuid.to_string(),
        })
    }
}

macro_rules! check_key {
    ($condition:expr, $key:tt, $value:tt) => {
        if $condition {
            return Err($crate::stratis::StratisError::Error(format!(
                "Stratis token key '{}' requires a value of '{}'",
                $key, $value,
            )));
        }
    };
}

macro_rules! check_and_get_key {
    ($get:expr, $key:tt) => {
        if let Some(v) = $get {
            v
        } else {
            return Err($crate::stratis::StratisError::Error(format!(
                "Stratis token is missing key '{}' or the value is of the wrong type",
                $key
            )));
        }
    };
    ($get:expr, $func:expr, $key:tt, $ty:ty) => {
        if let Some(ref v) = $get {
            $func(v).map_err(|e| {
                $crate::stratis::StratisError::Error(format!(
                    "Failed to convert value for key '{}' to type {}: {}",
                    $key,
                    stringify!($ty),
                    e
                ))
            })?
        } else {
            return Err($crate::stratis::StratisError::Error(format!(
                "Stratis token is missing key '{}' or the value is of the wrong type",
                $key
            )));
        }
    };
}

impl<'a> TryFrom<&'a Value> for StratisLuks2Token {
    type Error = StratisError;

    fn try_from(v: &Value) -> StratisResult<StratisLuks2Token> {
        let map = if let Value::Object(m) = v {
            m
        } else {
            return Err(StratisError::Crypt(LibcryptErr::InvalidConversion));
        };

        check_key!(
            map.get(TOKEN_TYPE_KEY).and_then(|v| v.as_str()) != Some(STRATIS_TOKEN_TYPE),
            "type",
            STRATIS_TOKEN_TYPE
        );
        check_key!(
            map.get(TOKEN_KEYSLOTS_KEY).and_then(|v| v.as_array()) != Some(&Vec::new()),
            "keyslots",
            "[]"
        );
        let devname = check_and_get_key!(
            map.get(STRATIS_TOKEN_DEVNAME_KEY)
                .and_then(|s| s.as_str())
                .map(|s| s.to_string()),
            STRATIS_TOKEN_DEVNAME_KEY
        );
        let pool_uuid = check_and_get_key!(
            map.get(STRATIS_TOKEN_POOL_UUID_KEY)
                .and_then(|s| s.as_str())
                .map(|s| s.to_string()),
            PoolUuid::parse_str,
            STRATIS_TOKEN_POOL_UUID_KEY,
            PoolUuid
        );
        let dev_uuid = check_and_get_key!(
            map.get(STRATIS_TOKEN_DEV_UUID_KEY)
                .and_then(|s| s.as_str())
                .map(|s| s.to_string()),
            DevUuid::parse_str,
            STRATIS_TOKEN_DEV_UUID_KEY,
            DevUuid
        );
        Ok(StratisLuks2Token {
            devname,
            identifiers: StratisIdentifiers::new(pool_uuid, dev_uuid),
        })
    }
}

/// Handle for initialization actions on a physical device.
pub struct CryptInitializer {
    physical_path: PathBuf,
    identifiers: StratisIdentifiers,
    activation_name: String,
}

impl CryptInitializer {
    pub fn new(physical_path: PathBuf, pool_uuid: PoolUuid, dev_uuid: DevUuid) -> CryptInitializer {
        CryptInitializer {
            physical_path,
            activation_name: format_crypt_name(&dev_uuid),
            identifiers: StratisIdentifiers::new(pool_uuid, dev_uuid),
        }
    }

    /// Acquire a crypt device using the registered LUKS2 device path.
    fn acquire_crypt_device(&self) -> StratisResult<CryptDevice> {
        acquire_crypt_device(&self.physical_path)
    }

    /// Initialize a device with the provided key description and Clevis info.
    pub fn initialize(
        self,
        key_description: Option<&KeyDescription>,
        clevis_info: Option<(&str, &Value)>,
    ) -> StratisResult<CryptHandle> {
        let mut clevis_info_owned =
            clevis_info.map(|(pin, config)| (pin.to_owned(), config.clone()));
        let clevis_parsed = match clevis_info_owned {
            Some((ref pin, ref mut config)) => {
                let yes = interpret_clevis_config(pin, config)?;
                Some((pin.as_str(), &*config, yes))
            }
            None => None,
        };

        let mut device = log_on_failure!(
            CryptInit::init(&self.physical_path),
            "Failed to acquire context for device {} while initializing; \
            nothing to clean up",
            self.physical_path.display()
        );
        device.settings_handle().set_metadata_size(
            MetadataSize::try_from(DEFAULT_CRYPT_METADATA_SIZE)?,
            KeyslotsSize::try_from(DEFAULT_CRYPT_KEYSLOTS_SIZE)?,
        )?;
        let result = self.initialize_with_err(device, key_description, clevis_parsed);
        let mut device = match self.acquire_crypt_device() {
            Ok(d) => d,
            Err(e) => {
                warn!(
                    "Failed to roll back crypt device initialization; you \
                    may need to manually wipe this device: {}",
                    e,
                );
                return Err(e);
            }
        };

        result
            .and_then(|activated_path| {
                Ok(CryptHandle::new(
                    self.physical_path.clone(),
                    activated_path,
                    self.identifiers,
                    EncryptionInfo {
                        key_description: key_description.cloned(),
                        clevis_info: clevis_info_from_metadata(&mut device)?,
                    },
                    self.activation_name.clone(),
                ))
            })
            .map_err(|e| {
                if let Err(err) =
                    Self::rollback(&mut device, &self.physical_path, self.activation_name)
                {
                    warn!(
                        "Failed to roll back crypt device initialization; you may need to manually wipe this device: {}",
                        err
                    );
                }
                e
            })
    }

    /// Initialize with a passphrase in the kernel keyring only.
    fn initialize_with_keyring(
        &self,
        device: &mut CryptDevice,
        key_description: &KeyDescription,
    ) -> StratisResult<()> {
        add_keyring_keyslot(device, key_description, None)?;

        Ok(())
    }

    /// Initialize with Clevis only.
    fn initialize_with_clevis(
        &self,
        mut device: CryptDevice,
        (pin, json, yes): (&str, &Value, bool),
    ) -> StratisResult<()> {
        let fs = log_on_failure!(
            MemoryPrivateFilesystem::new(),
            "Failed to initialize in memory filesystem for temporary keyfile for
            Clevis binding"
        );
        let keyfile = log_on_failure!(
            fs.rand_key(),
            "Failed to generate a key with random data for Clevis initialization"
        );

        let keyslot = log_on_failure!(
            device.keyslot_handle().add_by_key(
                None,
                None,
                keyfile.as_ref(),
                CryptVolumeKeyFlags::empty(),
            ),
            "Failed to initialize keyslot with provided key in keyring"
        );
        drop(device);

        clevis_luks_bind(
            &self.physical_path,
            keyfile.keyfile_path(),
            CLEVIS_LUKS_TOKEN_ID,
            pin,
            json,
            yes,
        )?;

        // Need to reacquire device here to refresh the state of the device
        // after being modified by Clevis.
        let mut device = self.acquire_crypt_device()?;
        device.keyslot_handle().destroy(keyslot)?;

        Ok(())
    }

    /// Initialize with both a passphrase in the kernel keyring and Clevis.
    fn initialize_with_both(
        &self,
        mut device: CryptDevice,
        key_description: &KeyDescription,
        (pin, json, yes): (&str, &Value, bool),
    ) -> StratisResult<()> {
        self.initialize_with_keyring(&mut device, key_description)?;

        let fs = MemoryPrivateFilesystem::new()?;
        fs.key_op(key_description, |kf| {
            clevis_luks_bind(
                &self.physical_path,
                kf,
                CLEVIS_LUKS_TOKEN_ID,
                pin,
                json,
                yes,
            )
        })?;

        Ok(())
    }

    fn initialize_with_err(
        &self,
        mut device: CryptDevice,
        key_description: Option<&KeyDescription>,
        clevis_info: Option<(&str, &Value, bool)>,
    ) -> StratisResult<PathBuf> {
        log_on_failure!(
            device.context_handle().format::<()>(
                EncryptionFormat::Luks2,
                ("aes", "xts-plain64"),
                None,
                libcryptsetup_rs::Either::Right(STRATIS_MEK_SIZE),
                None,
            ),
            "Failed to format device {} with LUKS2 header",
            self.physical_path.display()
        );

        let mut device = match (key_description, clevis_info) {
            (Some(kd), Some(ci)) => {
                self.initialize_with_both(device, kd, ci)?;
                self.acquire_crypt_device()?
            }
            (Some(kd), _) => {
                self.initialize_with_keyring(&mut device, kd)?;
                device
            }
            (_, Some(ci)) => {
                self.initialize_with_clevis(device, ci)?;
                self.acquire_crypt_device()?
            }
            (_, _) => unreachable!(),
        };

        // Initialize stratis token
        log_on_failure!(
            device.token_handle().json_set(TokenInput::ReplaceToken(
                STRATIS_TOKEN_ID,
                &StratisLuks2Token {
                    devname: self.activation_name.clone(),
                    identifiers: self.identifiers,
                }
                .into(),
            )),
            "Failed to create the Stratis token"
        );

        activate(
            if let Some(kd) = key_description {
                Either::Left((&mut device, kd))
            } else {
                Either::Right(&self.physical_path)
            },
            &self.activation_name,
        )
    }

    pub fn rollback(
        device: &mut CryptDevice,
        physical_path: &Path,
        name: String,
    ) -> StratisResult<()> {
        ensure_wiped(device, physical_path, &name)
    }
}

/// Handle for activating a locked encrypted device.
pub struct CryptActivationHandle;

impl CryptActivationHandle {
    /// Check whether the given physical device can be unlocked with the current
    /// environment (e.g. the proper key is in the kernel keyring, the device
    /// is formatted as a LUKS2 device, etc.)
    pub fn can_unlock(
        physical_path: &Path,
        try_unlock_keyring: bool,
        try_unlock_clevis: bool,
    ) -> bool {
        fn can_unlock_with_failures(
            physical_path: &Path,
            try_unlock_keyring: bool,
            try_unlock_clevis: bool,
        ) -> StratisResult<bool> {
            let mut device = acquire_crypt_device(physical_path)?;

            if try_unlock_keyring {
                let key_description = key_desc_from_metadata(&mut device);

                if key_description.is_some() {
                    check_luks2_token(&mut device)?;
                }
            }
            if try_unlock_clevis {
                let token = device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok();
                let jwe = token.as_ref().and_then(|t| t.get("jwe"));
                if let Some(jwe) = jwe {
                    let pass = clevis_decrypt(jwe)?;
                    if let Some(keyslot) = get_keyslot_number(&mut device, CLEVIS_LUKS_TOKEN_ID)?
                        .and_then(|k| k.into_iter().next())
                    {
                        log_on_failure!(
                            device.activate_handle().activate_by_passphrase(
                                None,
                                Some(keyslot),
                                pass.as_ref(),
                                CryptActivateFlags::empty(),
                            ),
                            "libcryptsetup reported that the decrypted Clevis passphrase \
                            is unable to open the encrypted device"
                        );
                    } else {
                        return Err(StratisError::Error(
                            "Clevis JWE was found in the Stratis metadata but was \
                            not associated with any keyslots"
                                .to_string(),
                        ));
                    }
                }
            }
            Ok(true)
        }

        can_unlock_with_failures(physical_path, try_unlock_keyring, try_unlock_clevis)
            .map_err(|e| {
                warn!(
                    "stratisd was unable to simulate opening the given device \
                    in the current environment: {}",
                    e,
                );
            })
            .unwrap_or(false)
    }

    /// Query the device metadata to reconstruct a handle for performing operations
    /// on an existing encrypted device.
    ///
    /// This method will check that the metadata on the given device is
    /// for the LUKS2 format and that the LUKS2 metadata is formatted
    /// properly as a Stratis encrypted device. If it is properly
    /// formatted it will return the device identifiers (pool and device UUIDs).
    ///
    /// NOTE: This method attempts to activate the device and thus returns a CryptHandle
    ///
    /// The checks include:
    /// * is a LUKS2 device
    /// * has a valid Stratis LUKS2 token
    /// * has a token of the proper type for LUKS2 keyring unlocking
    pub fn setup(
        physical_path: &Path,
        unlock_method: UnlockMethod,
    ) -> StratisResult<Option<CryptHandle>> {
        setup_crypt_handle(physical_path, Some(unlock_method))
    }
}

/// Handle for performing operations on an encrypted device.
///
/// This device assumes that its logical, unlocked device path has been activated and
/// is present. This checked in all mechanisms that yield a CryptHandle.
/// * CryptInitializer will ensure that the newly formatted device is activated.
/// * CryptActivationHandle requires the user to activate a device to yield a CryptHandle.
/// * CryptHandle::setup() fails if the device is not active.
pub struct CryptHandle {
    path: Arc<BlockDevPath>,
    identifiers: StratisIdentifiers,
    encryption_info: EncryptionInfo,
    name: String,
}

impl Debug for CryptHandle {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(
            f,
            "CryptHandle {{ device: CryptDevice, physical_path: {}, identifiers: {}, \
            encryption_info: {}, name: {} }}",
            self.luks2_device_path().display(),
            self.identifiers,
            self.encryption_info,
            self.name,
        )
    }
}

impl CryptHandle {
    fn new(
        physical_path: PathBuf,
        activated_path: PathBuf,
        identifiers: StratisIdentifiers,
        encryption_info: EncryptionInfo,
        name: String,
    ) -> CryptHandle {
        let path = BlockDevPath::node_with_children(
            activated_path,
            vec![BlockDevPath::leaf(physical_path)],
        );
        CryptHandle {
            path,
            identifiers,
            encryption_info,
            name,
        }
    }

    /// Acquire the crypt device handle for the physical path in this `CryptHandle`.
    fn acquire_crypt_device(&self) -> StratisResult<CryptDevice> {
        acquire_crypt_device(self.luks2_device_path())
    }

    /// Query the device metadata to reconstruct a handle for performing operations
    /// on an existing encrypted device.
    ///
    /// This method will check that the metadata on the given device is
    /// for the LUKS2 format and that the LUKS2 metadata is formatted
    /// properly as a Stratis encrypted device. If it is properly
    /// formatted it will return the device identifiers (pool and device UUIDs).
    ///
    /// NOTE: This will not validate that the proper key is in the kernel
    /// keyring. For that, use `CryptActivationHandle::can_unlock()`.
    ///
    /// The checks include:
    /// * is a LUKS2 device
    /// * has a valid Stratis LUKS2 token
    /// * has a token of the proper type for LUKS2 keyring unlocking
    pub fn setup(physical_path: &Path) -> StratisResult<Option<CryptHandle>> {
        setup_crypt_handle(physical_path, None)
    }

    /// Get the encryption info for this encrypted device.
    pub fn encryption_info(&self) -> &EncryptionInfo {
        &self.encryption_info
    }

    /// Get a reference to the `BlockDevPath` node representing the physical device.
    pub fn get_physical_path_ref(&self) -> Arc<BlockDevPath> {
        self.path
            .children()
            .next()
            .expect("crypt devices have exactly one child")
    }

    /// Return the path to the device node of the underlying storage device
    /// for the encrypted device. If storage layers are added between
    /// the crypt device and the physical device, this method will still work
    /// properly as it will provide the path to the device that exposes the LUKS2
    /// metadata.
    pub fn luks2_device_path(&self) -> &Path {
        self.path
            .child_paths()
            .next()
            .expect("crypt devices have exactly one child")
    }

    /// Return the path to the device node of the decrypted contents of the encrypted
    /// storage device. In an encrypted pool, this is the path that can be used to read
    /// the Stratis blockdev metatdata.
    pub fn activated_device_path(&self) -> &Path {
        self.path.path()
    }

    /// Get the Stratis device identifiers for a given encrypted device.
    pub fn device_identifiers(&self) -> &StratisIdentifiers {
        &self.identifiers
    }

    /// Get the keyslot associated with the given token ID.
    pub fn keyslots(&mut self, token_id: c_uint) -> StratisResult<Option<Vec<c_uint>>> {
        get_keyslot_number(&mut self.acquire_crypt_device()?, token_id)
    }

    /// Get info for the clevis binding.
    pub fn clevis_info(&mut self) -> StratisResult<Option<(String, Value)>> {
        clevis_info_from_metadata(&mut self.acquire_crypt_device()?)
    }

    /// Bind the given device using clevis.
    pub fn clevis_bind(&mut self, pin: &str, json: &Value) -> StratisResult<()> {
        let mut json_owned = json.clone();
        let yes = interpret_clevis_config(pin, &mut json_owned)?;

        let key_desc = self
            .encryption_info
            .key_description
            .as_ref()
            .ok_or_else(|| {
                StratisError::Error(
                    "Clevis binding requires a registered key description for the device \
                    but none was found"
                        .to_string(),
                )
            })?;
        let memfs = MemoryPrivateFilesystem::new()?;
        memfs.key_op(key_desc, |keyfile_path| {
            clevis_luks_bind(
                self.luks2_device_path(),
                keyfile_path,
                CLEVIS_LUKS_TOKEN_ID,
                pin,
                &json_owned,
                yes,
            )
        })?;
        self.encryption_info.clevis_info = Some((pin.to_string(), json_owned));
        Ok(())
    }

    /// Unbind the given device using clevis.
    pub fn clevis_unbind(&mut self) -> StratisResult<()> {
        if self.encryption_info.key_description.is_none() {
            return Err(StratisError::Error(
                "No kernel keyring binding found; removing the Clevis binding \
                would remove the ability to open this device; aborting"
                    .to_string(),
            ));
        }

        let keyslots = self.keyslots(CLEVIS_LUKS_TOKEN_ID)?.ok_or_else(|| {
            StratisError::Error(format!(
                "Token slot {} appears to be empty; could not determine keyslots",
                CLEVIS_LUKS_TOKEN_ID,
            ))
        })?;
        for keyslot in keyslots {
            log_on_failure!(
                clevis_luks_unbind(self.luks2_device_path(), keyslot),
                "Failed to unbind device {} from Clevis",
                self.luks2_device_path().display()
            );
        }
        self.encryption_info.clevis_info = None;
        Ok(())
    }

    /// Add a keyring binding to the underlying LUKS2 volume.
    pub fn bind_keyring(&mut self, key_desc: &KeyDescription) -> StratisResult<()> {
        let mut device = self.acquire_crypt_device()?;
        let key = Self::clevis_decrypt(&mut device)?.ok_or_else(|| {
            StratisError::Error(
                "The Clevis token appears to have been wiped outside of \
                    Stratis; cannot add a keyring key binding without an existing \
                    passphrase to unlock the device"
                    .to_string(),
            )
        })?;

        add_keyring_keyslot(&mut device, key_desc, Some(key))?;

        self.encryption_info.key_description = Some(key_desc.clone());
        Ok(())
    }

    /// Add a keyring binding to the underlying LUKS2 volume.
    pub fn unbind_keyring(&mut self) -> StratisResult<()> {
        if self.encryption_info.clevis_info.is_none() {
            return Err(StratisError::Error(
                "No Clevis binding was found; removing the keyring binding would \
                remove the ability to open this device; aborting"
                    .to_string(),
            ));
        }

        let mut device = self.acquire_crypt_device()?;
        let keyslots = get_keyslot_number(&mut device, LUKS2_TOKEN_ID)?
            .ok_or_else(|| StratisError::Error("No LUKS2 keyring token was found".to_string()))?;
        for keyslot in keyslots {
            log_on_failure!(
                device.keyslot_handle().destroy(keyslot),
                "Failed partway through the kernel keyring unbinding operation \
                which cannot be rolled back; manual intervention may be required"
            )
        }
        device
            .token_handle()
            .json_set(TokenInput::RemoveToken(LUKS2_TOKEN_ID))?;

        self.encryption_info.key_description = None;

        Ok(())
    }

    /// Decrypt a Clevis passphrase and return it securely.
    fn clevis_decrypt(device: &mut CryptDevice) -> StratisResult<Option<SizedKeyMemory>> {
        let mut token = match device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok() {
            Some(t) => t,
            None => return Ok(None),
        };
        let jwe = token
            .as_object_mut()
            .and_then(|map| map.remove("jwe"))
            .ok_or_else(|| {
                StratisError::Error(format!(
                    "Token slot {} is occupied but does not appear to be a Clevis \
                    token; aborting",
                    CLEVIS_LUKS_TOKEN_ID,
                ))
            })?;
        clevis_decrypt(&jwe).map(Some)
    }

    /// Deactivate the device referenced by the current device handle.
    #[cfg(test)]
    pub fn deactivate(&mut self) -> StratisResult<()> {
        let name = self.name.to_owned();
        ensure_inactive(&mut self.acquire_crypt_device()?, &name)
    }

    /// Wipe all LUKS2 metadata on the device safely using libcryptsetup.
    pub fn wipe(&mut self) -> StratisResult<()> {
        let path = self.luks2_device_path().to_owned();
        let name = self.name.to_owned();
        ensure_wiped(&mut self.acquire_crypt_device()?, &path, &name)
    }

    /// Get the size of the logical device built on the underlying encrypted physical
    /// device. `devicemapper` will return the size in terms of number of sectors.
    pub fn logical_device_size(&mut self) -> StratisResult<Sectors> {
        let name = self.name.clone();
        let active_device = log_on_failure!(
            self.acquire_crypt_device()?
                .runtime_handle(&name)
                .get_active_device(),
            "Failed to get device size for encrypted logical device"
        );
        Ok(Sectors(active_device.size))
    }
}

/// Acquire a crypt device handle or return an error. This serves as a wrapper
/// around device_from_physical_path removing the Option type.
fn acquire_crypt_device(physical_path: &Path) -> StratisResult<CryptDevice> {
    device_from_physical_path(physical_path)?.ok_or_else(|| {
        StratisError::Error(format!(
            "Physical device {} underneath encrypted Stratis has been \
                determined not to be formatted as a LUKS2 Stratis device",
            physical_path.display(),
        ))
    })
}

// Precondition: if clevis_pass.is_none(), device must have the volume key stored
// in memory (this is automatically done when formatting a LUKS2 device).
fn add_keyring_keyslot(
    device: &mut CryptDevice,
    key_description: &KeyDescription,
    clevis_pass: Option<SizedKeyMemory>,
) -> StratisResult<()> {
    let key_option = log_on_failure!(
        read_key(key_description),
        "Failed to read key with key description {} from keyring",
        key_description.as_application_str()
    );
    let key = if let Some(key) = key_option {
        key
    } else {
        return Err(StratisError::Error(format!(
            "Key with key description {} was not found",
            key_description.as_application_str(),
        )));
    };

    let keyslot = match clevis_pass {
        Some(ref pass) => {
            log_on_failure!(
                device
                    .keyslot_handle()
                    .add_by_passphrase(None, pass.as_ref(), key.as_ref(),),
                "Failed to initialize keyslot with existing Clevis key"
            )
        }
        None => {
            log_on_failure!(
                device.keyslot_handle().add_by_key(
                    None,
                    None,
                    key.as_ref(),
                    CryptVolumeKeyFlags::empty(),
                ),
                "Failed to initialize keyslot with provided key in keyring"
            )
        }
    };

    log_on_failure!(
        device
            .token_handle()
            .luks2_keyring_set(Some(LUKS2_TOKEN_ID), &key_description.to_system_string()),
        "Failed to initialize the LUKS2 token for driving keyring activation operations"
    );
    log_on_failure!(
        device
            .token_handle()
            .assign_keyslot(LUKS2_TOKEN_ID, Some(keyslot)),
        "Failed to assign the LUKS2 keyring token to the Stratis keyslot"
    );

    Ok(())
}

/// Set up a handle to a crypt device using either Clevis or the keyring to activate
/// the device.
fn setup_crypt_handle(
    physical_path: &Path,
    unlock_method: Option<UnlockMethod>,
) -> StratisResult<Option<CryptHandle>> {
    let device_result = device_from_physical_path(physical_path);
    let mut device = match device_result {
        Ok(None) => return Ok(None),
        Ok(Some(mut dev)) => {
            if !is_encrypted_stratis_device(&mut dev) {
                return Ok(None);
            } else {
                dev
            }
        }
        Err(e) => return Err(e),
    };

    let identifiers = identifiers_from_metadata(&mut device)?;
    let key_description = key_desc_from_metadata(&mut device);
    let key_description = match key_description
        .as_ref()
        .map(|kd| KeyDescription::from_system_key_desc(kd))
    {
        Some(Some(Ok(description))) => Some(description),
        Some(Some(Err(e))) => {
            return Err(StratisError::Error(format!(
                "key description {} found on devnode {} is not a valid Stratis key description: {}",
                key_description.expect("key_desc_from_metadata determined to be Some(_) above"),
                physical_path.display(),
                e,
            )));
        }
        Some(None) => {
            warn!("Key description stored on device {} does not appear to be a Stratis key description; ignoring", physical_path.display());
            None
        }
        None => None,
    };
    let clevis_info = clevis_info_from_metadata(&mut device)?;
    let name = name_from_metadata(&mut device)?;

    let activated_path = match unlock_method {
        Some(UnlockMethod::Keyring) => {
            activate(Either::Left((
                &mut device,
                key_description.as_ref()
                    .ok_or_else(|| {
                        StratisError::Error("Unlock action was specified to be keyring but not key description is present in the metadata".to_string())
                    })?,
            )), &name)?
        }
        Some(UnlockMethod::Clevis) => activate(Either::Right(physical_path), &name)?,
        None => [DEVICEMAPPER_PATH, &name].iter().collect(),
    };

    Ok(Some(CryptHandle {
        path: BlockDevPath::node_with_children(
            activated_path,
            vec![BlockDevPath::leaf(physical_path.to_owned())],
        ),
        identifiers,
        encryption_info: EncryptionInfo {
            key_description,
            clevis_info,
        },
        name,
    }))
}

/// Create a device handle and load the LUKS2 header into memory from
/// a physical path.
fn device_from_physical_path(physical_path: &Path) -> StratisResult<Option<CryptDevice>> {
    let mut device = log_on_failure!(
        CryptInit::init(physical_path),
        "Failed to acquire a context for device {}",
        physical_path.display()
    );
    if device
        .context_handle()
        .load::<()>(Some(EncryptionFormat::Luks2), None)
        .is_err()
    {
        Ok(None)
    } else {
        Ok(Some(device))
    }
}

/// Get the Clevis binding information from the device metadata.
///
/// This method returns:
/// * Ok(Some(_)) if a Clevis token was detected
/// * Ok(None) if no token in the Clevis slot was detected or a token was detected
/// but does not appear to be a Clevis token
/// * Err(_) if the token appears to be a Clevis token but is malformed in some way
fn clevis_info_from_metadata(device: &mut CryptDevice) -> StratisResult<Option<(String, Value)>> {
    let json = match device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok() {
        Some(j) => j,
        None => return Ok(None),
    };
    let json_b64 = match json
        .get("jwe")
        .and_then(|map| map.get("protected"))
        .and_then(|string| string.as_str())
    {
        Some(s) => s.to_owned(),
        None => return Ok(None),
    };
    let json_bytes = BASE64URL_NOPAD.decode(json_b64.as_bytes())?;

    let subjson: Value = serde_json::from_slice(json_bytes.as_slice())?;

    pin_dispatch(&subjson).map(Some)
}

/// Interpret non-Clevis keys that may contain additional information about
/// how to configure Clevis when binding. Remove any expected non-Clevis keys
/// from the configuration.
/// The only value to be returned is whether or not the bind command should be
/// passed the argument yes.
pub fn interpret_clevis_config(pin: &str, clevis_config: &mut Value) -> StratisResult<bool> {
    let yes = if pin == "tang" {
        if let Some(map) = clevis_config.as_object_mut() {
            map.remove(CLEVIS_TANG_TRUST_URL)
                .and_then(|v| v.as_bool())
                .unwrap_or(false)
        } else {
            return Err(StratisError::Error(format!(
                "configuration for Clevis is is not in JSON object format: {}",
                clevis_config
            )));
        }
    } else {
        false
    };

    Ok(yes)
}

/// Generate tang JSON
fn tang_dispatch(json: &Value) -> StratisResult<Value> {
    let object = json
        .get("clevis")
        .and_then(|map| map.get("tang"))
        .and_then(|val| val.as_object())
        .ok_or_else(|| {
            StratisError::Error("Expected an object for value of clevis.tang".to_string())
        })?;
    let url = object.get("url").and_then(|s| s.as_str()).ok_or_else(|| {
        StratisError::Error("Expected a string for value of clevis.tang.url".to_string())
    })?;

    let keys = object
        .get("adv")
        .and_then(|adv| adv.get("keys"))
        .and_then(|keys| keys.as_array())
        .ok_or_else(|| {
            StratisError::Error("Expected an array for value of clevis.tang.adv.keys".to_string())
        })?;
    let mut key = keys
        .iter()
        .cloned()
        .find(|obj| obj.get("key_ops") == Some(&Value::Array(vec![Value::from("verify")])))
        .ok_or_else(|| {
            StratisError::Error("Verification key not found in clevis metadata".to_string())
        })?;

    let map = if let Some(m) = key.as_object_mut() {
        m
    } else {
        return Err(StratisError::Error(
            "Key value is not in JSON object format".to_string(),
        ));
    };
    map.remove("key_ops");
    map.remove("alg");

    let thp = key.to_string();
    let mut hasher = Sha1::new();
    hasher.update(thp.as_bytes());
    let array = hasher.finalize();
    let thp = BASE64URL_NOPAD.encode(array.as_slice());

    Ok(json!({"url": url.to_owned(), "thp": thp}))
}

/// Generate Shamir secret sharing JSON
fn sss_dispatch(json: &Value) -> StratisResult<Value> {
    let object = json
        .get("clevis")
        .and_then(|map| map.get("sss"))
        .and_then(|val| val.as_object())
        .ok_or_else(|| {
            StratisError::Error("Expected an object for value of clevis.sss".to_string())
        })?;

    let threshold = object
        .get("t")
        .and_then(|val| val.as_u64())
        .ok_or_else(|| {
            StratisError::Error("Expected an int for value of clevis.sss.t".to_string())
        })?;
    let jwes = object
        .get("jwe")
        .and_then(|val| val.as_array())
        .ok_or_else(|| {
            StratisError::Error("Expected an array for value of clevis.sss.jwe".to_string())
        })?;

    let mut sss_map = Map::new();
    sss_map.insert("t".to_string(), Value::from(threshold));

    let mut pin_map = Map::new();
    for jwe in jwes {
        if let Value::String(ref s) = jwe {
            // NOTE: Workaround for the on-disk format for Shamir secret sharing
            // as written by clevis. The base64 encoded string delimits the end
            // of the JSON blob with a period.
            let json_s = s.splitn(2, '.').next().ok_or_else(|| {
                StratisError::Error(format!(
                    "Splitting string {} on character '.' did not result in \
                    at least one string segment.",
                    s,
                ))
            })?;

            let json_bytes = BASE64URL_NOPAD.decode(json_s.as_bytes())?;
            let value: Value = serde_json::from_slice(&json_bytes)?;
            let (pin, value) = pin_dispatch(&value)?;
            match pin_map.get_mut(&pin) {
                Some(Value::Array(ref mut vec)) => vec.push(value),
                None => {
                    pin_map.insert(pin, Value::from(vec![value]));
                }
                _ => {
                    return Err(StratisError::Error(format!(
                        "There appears to be a data type that is not an array in \
                        the data structure being used to construct the sss JSON config
                        under pin name {}",
                        pin,
                    )))
                }
            };
        } else {
            return Err(StratisError::Error(
                "Expected a string for each value in the array at clevis.sss.jwe".to_string(),
            ));
        }
    }
    sss_map.insert("pins".to_string(), Value::from(pin_map));

    Ok(Value::from(sss_map))
}

/// Match pin for existing JWE
fn pin_dispatch(decoded_jwe: &Value) -> StratisResult<(String, Value)> {
    let pin_value = decoded_jwe
        .get("clevis")
        .and_then(|map| map.get("pin"))
        .ok_or_else(|| {
            StratisError::Error("Key .clevis.pin not found in clevis JSON token".to_string())
        })?;
    match pin_value.as_str() {
        Some("tang") => tang_dispatch(decoded_jwe).map(|val| ("tang".to_owned(), val)),
        Some("sss") => sss_dispatch(decoded_jwe).map(|val| ("sss".to_owned(), val)),
        Some("tpm2") => Ok(("tpm2".to_owned(), json!({}))),
        _ => Err(StratisError::Error("Unsupported clevis pin".to_string())),
    }
}

/// Check whether the physical device path corresponds to an encrypted
/// Stratis device.
///
/// This method works on activated and deactivated encrypted devices.
///
/// This device will only return true if the device was initialized
/// with encryption by Stratis. This requires that:
/// * the device is a LUKS2 encrypted device.
/// * the device has a valid Stratis LUKS2 token.
fn is_encrypted_stratis_device(device: &mut CryptDevice) -> bool {
    fn device_operations(device: &mut CryptDevice) -> StratisResult<()> {
        let stratis_token = device.token_handle().json_get(STRATIS_TOKEN_ID).ok();
        let luks_token = device.token_handle().json_get(LUKS2_TOKEN_ID).ok();
        let clevis_token = device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).ok();
        if stratis_token.is_none() || (luks_token.is_none() && clevis_token.is_none()) {
            return Err(StratisError::Error(
                "Device appears to be missing some of the required Stratis LUKS2 tokens"
                    .to_string(),
            ));
        }
        if let Some(ref lt) = luks_token {
            if !luks2_token_type_is_valid(lt) {
                return Err(StratisError::Error("LUKS2 token is invalid".to_string()));
            }
        }
        if let Some(ref st) = stratis_token {
            if !stratis_token_is_valid(st) {
                return Err(StratisError::Error("Stratis token is invalid".to_string()));
            }
        }
        Ok(())
    }

    device_operations(device)
        .map(|_| true)
        .map_err(|e| {
            debug!(
                "Operations querying device to determine if it is a Stratis device \
                failed with an error: {}; reporting as not a Stratis device.",
                e
            );
        })
        .unwrap_or(false)
}

fn device_is_active(device: Option<&mut CryptDevice>, device_name: &str) -> StratisResult<()> {
    match libcryptsetup_rs::status(device, device_name) {
        Ok(CryptStatusInfo::Active) => Ok(()),
        Ok(CryptStatusInfo::Busy) => {
            info!(
                "Newly activated device {} reported that it was busy; you may see \
                temporary failures due to the device being busy.",
                device_name,
            );
            Ok(())
        }
        Ok(CryptStatusInfo::Inactive) => {
            warn!(
                "Newly activated device {} reported that it is inactive; device \
                activation appears to have failed",
                device_name,
            );
            Err(StratisError::Error(format!(
                "Device {} was activated but is reporting that it is inactive",
                device_name,
            )))
        }
        Ok(CryptStatusInfo::Invalid) => {
            warn!(
                "Newly activated device {} reported that its status is invalid; \
                device activation appears to have failed",
                device_name,
            );
            Err(StratisError::Error(format!(
                "Device {} was activated but is reporting an invalid status",
                device_name,
            )))
        }
        Err(e) => Err(StratisError::Error(format!(
            "Failed to fetch status for device name {}: {}",
            device_name, e,
        ))),
    }
}

/// Activate device by LUKS2 keyring token.
///
/// Precondition: The key description has been verfified to be present in the keyring
/// if matches!(unlock_method, UnlockMethod::Keyring).
fn activate_with_keyring(crypt_device: &mut CryptDevice, name: &str) -> StratisResult<()> {
    // Activate by token
    log_on_failure!(
        crypt_device.token_handle().activate_by_token::<()>(
            Some(name),
            Some(LUKS2_TOKEN_ID),
            None,
            CryptActivateFlags::empty(),
        ),
        "Failed to activate device with name {}",
        name
    );
    Ok(())
}

/// Activate encrypted Stratis device using the name stored in the
/// Stratis token.
fn activate(
    unlock_param: Either<(&mut CryptDevice, &KeyDescription), &Path>,
    name: &str,
) -> StratisResult<PathBuf> {
    let crypt_device = match unlock_param {
        Either::Left((device, kd)) => {
            let key_description_missing = keys::search_key_persistent(kd)
                .map_err(|_| {
                    StratisError::Error(format!(
                        "Searching the persistent keyring for the key description {} failed.",
                        kd.as_application_str(),
                    ))
                })?
                .is_none();
            if key_description_missing {
                warn!(
                    "Key description {} was not found in the keyring",
                    kd.as_application_str()
                );
                return Err(StratisError::Error(format!(
                    "The key description \"{}\" is not currently set.",
                    kd.as_application_str(),
                )));
            }
            activate_with_keyring(device, name)?;
            Some(device)
        }
        Either::Right(path) => {
            clevis_luks_unlock(path, name)?;
            None
        }
    };

    // Check activation status.
    device_is_active(crypt_device, name)?;

    // Checking that the symlink was created may also be valuable in case a race
    // condition occurs with udev.
    let mut activated_path = PathBuf::from(DEVICEMAPPER_PATH);
    activated_path.push(name);

    // Can potentially use inotify with a timeout to wait for the symlink
    // if race conditions become a problem.
    if activated_path.exists() {
        Ok(activated_path)
    } else {
        Err(StratisError::Io(io::Error::from(io::ErrorKind::NotFound)))
    }
}

/// Get a list of all keyslots associated with the LUKS2 token.
/// This is necessary because attempting to destroy an uninitialized
/// keyslot will result in an error.
fn get_keyslot_number(
    device: &mut CryptDevice,
    token_id: c_uint,
) -> StratisResult<Option<Vec<c_uint>>> {
    let json = match device.token_handle().json_get(token_id) {
        Ok(j) => j,
        Err(_) => return Ok(None),
    };
    let vec = json
        .get(TOKEN_KEYSLOTS_KEY)
        .and_then(|k| k.as_array())
        .ok_or_else(|| StratisError::Error("keyslots value was malformed".to_string()))?;
    Ok(Some(
        vec.iter()
            .filter_map(|int_val| {
                let as_str = int_val.as_str();
                if as_str.is_none() {
                    warn!(
                        "Discarding invalid value in LUKS2 token keyslot array: {}",
                        int_val
                    );
                }
                let s = match as_str {
                    Some(s) => s,
                    None => return None,
                };
                let as_c_uint = s.parse::<c_uint>();
                if let Err(ref e) = as_c_uint {
                    warn!(
                        "Discarding invalid value in LUKS2 token keyslot array: {}; \
                    failed to convert it to an integer: {}",
                        s, e,
                    );
                }
                as_c_uint.ok()
            })
            .collect::<Vec<_>>(),
    ))
}

/// Deactivate an encrypted Stratis device but do not wipe it. This is not
/// a destructive action. `name` should be the name of the device as registered
/// with devicemapper and cryptsetup. This method is idempotent and leaves
/// the state as inactive.
fn ensure_inactive(device: &mut CryptDevice, name: &str) -> StratisResult<()> {
    if log_on_failure!(
        libcryptsetup_rs::status(Some(device), name),
        "Failed to determine status of device with name {}",
        name
    ) == CryptStatusInfo::Active
    {
        log_on_failure!(
            device
                .activate_handle()
                .deactivate(name, CryptDeactivateFlags::empty()),
            "Failed to deactivate the crypt device with name {}",
            name
        );
    }
    Ok(())
}

/// Align the number of bytes to the nearest multiple of `SECTOR_SIZE`
/// above the current value.
fn ceiling_sector_size_alignment(bytes: u64) -> u64 {
    bytes + (SECTOR_SIZE - (bytes % SECTOR_SIZE))
}

/// Deactivate an encrypted Stratis device and wipe it. This is
/// a destructive action and data will be unrecoverable from this device
/// after this operation. `name` should be the name of the device as registered
/// with devicemapper and cryptsetup. `physical_path` should be the path to
/// the device node of the physical storage backing the encrypted volume.
/// This method is idempotent and leaves the disk as wiped.
fn ensure_wiped(device: &mut CryptDevice, physical_path: &Path, name: &str) -> StratisResult<()> {
    ensure_inactive(device, name)?;
    let keyslot_number = get_keyslot_number(device, LUKS2_TOKEN_ID);
    match keyslot_number {
        Ok(Some(nums)) => {
            for i in nums.iter() {
                log_on_failure!(
                    device.keyslot_handle().destroy(*i),
                    "Failed to destroy keyslot at index {}",
                    i
                );
            }
        }
        Ok(None) => {
            info!(
                "Token ID for keyslots to be wiped appears to be empty; the keyslot \
                area will still be wiped in the next step."
            );
        }
        Err(e) => {
            info!(
                "Keyslot numbers were not found; skipping explicit \
                destruction of keyslots; the keyslot area will still \
                be wiped in the next step: {}",
                e,
            );
        }
    }

    let (md_size, ks_size) = log_on_failure!(
        device.settings_handle().get_metadata_size(),
        "Failed to acquire LUKS2 metadata size"
    );
    debug!("Metadata size of LUKS2 device: {}", *md_size);
    debug!("Keyslot area size of LUKS2 device: {}", *ks_size);
    let total_luks2_metadata_size = ceiling_sector_size_alignment(*md_size * 2 + *ks_size);
    debug!("Aligned total size: {}", total_luks2_metadata_size);

    log_on_failure!(
        device.wipe_handle().wipe::<()>(
            physical_path,
            CryptWipePattern::Zero,
            0,
            total_luks2_metadata_size,
            convert_const!(SECTOR_SIZE, u64, usize),
            false,
            None,
            None,
        ),
        "Failed to wipe device with name {}",
        name
    );
    Ok(())
}

/// Check that the token can open the device.
///
/// No activation will actually occur, only validation.
fn check_luks2_token(device: &mut CryptDevice) -> StratisResult<()> {
    log_on_failure!(
        device.token_handle().activate_by_token::<()>(
            None,
            Some(LUKS2_TOKEN_ID),
            None,
            CryptActivateFlags::empty(),
        ),
        "libcryptsetup reported that the LUKS2 token is unable to \
        open the encrypted device; this could be due to a malformed \
        LUKS2 keyring token on the device or a missing or inaccessible \
        key in the keyring"
    );
    Ok(())
}

/// Validate that the LUKS2 token is present and valid
///
/// May not be necessary. See the comment above the invocation.
fn luks2_token_type_is_valid(json: &Value) -> bool {
    json.get(TOKEN_TYPE_KEY)
        .and_then(|type_val| type_val.as_str())
        .map(|type_str| type_str == LUKS2_TOKEN_TYPE)
        .unwrap_or(false)
}

/// Validate that the Stratis token is present and valid
fn stratis_token_is_valid(json: &Value) -> bool {
    debug!("Stratis LUKS2 token: {}", json);

    let result = StratisLuks2Token::try_from(json);
    if let Err(ref e) = result {
        debug!(
            "LUKS2 token in the Stratis token slot does not appear \
            to be a Stratis token: {}.",
            e,
        );
    }
    result.is_ok()
}

/// Read key from keyring with the given key description.
///
/// Returns a safe owned memory segment that will clear itself when dropped.
///
/// A return result of `Ok(None)` indicates that the key was not found
/// but no error occurred.
///
/// Requires cryptsetup 2.3
fn read_key(key_description: &KeyDescription) -> StratisResult<Option<SizedKeyMemory>> {
    let read_key_result = keys::read_key_persistent(key_description);
    if read_key_result.is_err() {
        warn!(
            "Failed to read the key with key description {}; encryption cannot \
            continue",
            key_description.as_application_str(),
        );
    }
    read_key_result.map(|opt| opt.map(|(_, mem)| mem))
}

/// Query the Stratis metadata for the device activation name.
fn name_from_metadata(device: &mut CryptDevice) -> StratisResult<String> {
    let json = log_on_failure!(
        device.token_handle().json_get(STRATIS_TOKEN_ID),
        "Failed to get Stratis JSON token from LUKS2 metadata"
    );
    let name = log_on_failure!(
        json.get(STRATIS_TOKEN_DEVNAME_KEY)
            .and_then(|type_val| type_val.as_str())
            .map(|type_str| type_str.to_string())
            .ok_or_else(|| {
                StratisError::Error(
                    "Malformed or missing JSON value for activation_name".to_string(),
                )
            }),
        "Could not get value for key activation_name from Stratis JSON token"
    );
    Ok(name)
}

/// Query the Stratis metadata for the key description used to unlock the
/// physical device.
fn key_desc_from_metadata(device: &mut CryptDevice) -> Option<String> {
    device.token_handle().luks2_keyring_get(LUKS2_TOKEN_ID).ok()
}

/// Query the Stratis metadata for the device identifiers.
fn identifiers_from_metadata(device: &mut CryptDevice) -> StratisResult<StratisIdentifiers> {
    let json = log_on_failure!(
        device.token_handle().json_get(STRATIS_TOKEN_ID),
        "Failed to get Stratis JSON token from LUKS2 metadata"
    );
    let pool_uuid = log_on_failure!(
        json.get(STRATIS_TOKEN_POOL_UUID_KEY)
            .and_then(|type_val| type_val.as_str())
            .and_then(|type_str| PoolUuid::parse_str(type_str).ok())
            .ok_or_else(|| {
                StratisError::Error(
                    "Malformed or missing JSON value for activation_name".to_string(),
                )
            }),
        "Could not get value for key {} from Stratis JSON token",
        STRATIS_TOKEN_POOL_UUID_KEY
    );
    let dev_uuid = log_on_failure!(
        json.get(STRATIS_TOKEN_DEV_UUID_KEY)
            .and_then(|type_val| type_val.as_str())
            .and_then(|type_str| DevUuid::parse_str(type_str).ok())
            .ok_or_else(|| {
                StratisError::Error(
                    "Malformed or missing JSON value for activation_name".to_string(),
                )
            }),
        "Could not get value for key {} from Stratis JSON token",
        STRATIS_TOKEN_DEV_UUID_KEY
    );
    Ok(StratisIdentifiers::new(pool_uuid, dev_uuid))
}

// Bytes occupied by crypt metadata
pub fn crypt_metadata_size() -> u64 {
    2 * DEFAULT_CRYPT_METADATA_SIZE + DEFAULT_CRYPT_KEYSLOTS_SIZE
}

#[cfg(test)]
mod tests {
    use std::{
        env,
        error::Error,
        ffi::CString,
        fs::{File, OpenOptions},
        io::{Read, Write},
        mem::MaybeUninit,
        ptr, slice,
    };

    use crate::{
        engine::strat_engine::{
            keys::MemoryFilesystem,
            tests::{crypt, loopbacked, real},
        },
        stratis::StratisError,
    };

    use super::*;

    /// If this method is called without a key with the specified key description
    /// in the kernel ring, it should always fail and allow us to test the rollback
    /// of failed initializations.
    fn test_failed_init(paths: &[&Path]) {
        assert_eq!(paths.len(), 1);

        let path = paths.get(0).expect("There must be exactly one path");
        let key_description =
            KeyDescription::try_from("I am not a key".to_string()).expect("no semi-colons");

        let pool_uuid = PoolUuid::new_v4();
        let dev_uuid = DevUuid::new_v4();

        let result = CryptInitializer::new((*path).to_owned(), pool_uuid, dev_uuid)
            .initialize(Some(&key_description), None);

        // Initialization cannot occur with a non-existent key
        assert!(result.is_err());

        assert!(CryptHandle::setup(path).unwrap().is_none());

        // TODO: Check actual superblock with libblkid
    }

    #[test]
    fn loop_test_failed_init() {
        loopbacked::test_with_spec(
            &loopbacked::DeviceLimits::Exactly(1, None),
            test_failed_init,
        );
    }

    #[test]
    fn real_test_failed_init() {
        real::test_with_spec(
            &real::DeviceLimits::Exactly(1, None, Some(Sectors(1024 * 1024 * 1024 / 512))),
            test_failed_init,
        );
    }

    /// Test the method `can_unlock` works on an initialized device in both
    /// active and inactive states.
    fn test_can_unlock(paths: &[&Path]) {
        fn crypt_test(
            paths: &[&Path],
            key_desc: &KeyDescription,
            _: (),
        ) -> std::result::Result<(), Box<dyn Error>> {
            let mut handles = vec![];

            let pool_uuid = PoolUuid::new_v4();
            for path in paths {
                let dev_uuid = DevUuid::new_v4();

                let handle = CryptInitializer::new((*path).to_owned(), pool_uuid, dev_uuid)
                    .initialize(Some(key_desc), None)?;
                handles.push(handle);
            }

            for path in paths {
                if !CryptActivationHandle::can_unlock(path, true, false) {
                    return Err(Box::new(StratisError::Error(
                        "All devices should be able to be unlocked".to_string(),
                    )));
                }
            }

            for handle in handles.iter_mut() {
                handle.deactivate()?;
            }

            for path in paths {
                if !CryptActivationHandle::can_unlock(path, true, false) {
                    return Err(Box::new(StratisError::Error(
                        "All devices should be able to be unlocked".to_string(),
                    )));
                }
            }

            for handle in handles.iter_mut() {
                handle.wipe()?;
            }

            for path in paths {
                if CryptActivationHandle::can_unlock(path, true, false) {
                    return Err(Box::new(StratisError::Error(
                        "All devices should no longer be able to be unlocked".to_string(),
                    )));
                }
            }

            Ok(())
        }

        crypt::insert_and_cleanup_key(paths, crypt_test)
    }

    #[test]
    fn loop_test_can_unlock() {
        loopbacked::test_with_spec(
            &loopbacked::DeviceLimits::Range(1, 3, None),
            test_can_unlock,
        );
    }

    #[test]
    fn real_test_can_unlock() {
        real::test_with_spec(
            &real::DeviceLimits::Range(1, 3, None, None),
            test_can_unlock,
        );
    }

    /// Test initializing and activating an encrypted device using
    /// the utilities provided here.
    ///
    /// The overall format of the test involves generating a random byte buffer
    /// of size 1 MiB, encrypting it on disk, and then ensuring that the plaintext
    /// cannot be found on the encrypted disk by doing a scan of the disk using
    /// a sliding window.
    ///
    /// The sliding window size of 1 MiB was chosen to lower the number of
    /// searches that need to be done compared to a smaller sliding window
    /// and also to decrease the probability of the random sequence being found
    /// on the disk due to leftover data from other tests.
    // TODO: Rewrite libc calls using nix crate.
    fn test_crypt_device_ops(paths: &[&Path]) {
        fn crypt_test(
            paths: &[&Path],
            key_desc: &KeyDescription,
            _: (),
        ) -> std::result::Result<(), Box<dyn Error>> {
            let path = paths.get(0).ok_or_else(|| {
                Box::new(StratisError::Error(
                    "This test only accepts a single device".to_string(),
                ))
            })?;

            let pool_uuid = PoolUuid::new_v4();
            let dev_uuid = DevUuid::new_v4();

            let mut handle = CryptInitializer::new((*path).to_owned(), pool_uuid, dev_uuid)
                .initialize(Some(key_desc), None)?;
            let logical_path = handle.activated_device_path();

            const WINDOW_SIZE: usize = 1024 * 1024;
            let mut devicenode = OpenOptions::new().write(true).open(logical_path)?;
            let mut random_buffer = Box::new([0; WINDOW_SIZE]);
            File::open("/dev/urandom")?.read_exact(&mut *random_buffer)?;
            devicenode.write_all(&*random_buffer)?;
            std::mem::drop(devicenode);

            let dev_path_cstring = CString::new(path.to_str().ok_or_else(|| {
                Box::new(io::Error::new(
                    io::ErrorKind::Other,
                    "Failed to convert path to string",
                ))
            })?)
            .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
            let fd = unsafe { libc::open(dev_path_cstring.as_ptr(), libc::O_RDONLY) };
            if fd < 0 {
                return Err(Box::new(io::Error::last_os_error()));
            }

            let mut stat: MaybeUninit<libc::stat> = MaybeUninit::zeroed();
            let fstat_result = unsafe { libc::fstat(fd, stat.as_mut_ptr()) };
            if fstat_result < 0 {
                return Err(Box::new(io::Error::last_os_error()));
            }
            let device_size =
                convert_int!(unsafe { stat.assume_init() }.st_size, libc::off_t, usize)?;
            let mapped_ptr = unsafe {
                libc::mmap(
                    ptr::null_mut(),
                    device_size,
                    libc::PROT_READ,
                    libc::MAP_SHARED,
                    fd,
                    0,
                )
            };
            if mapped_ptr.is_null() {
                return Err(Box::new(io::Error::new(
                    io::ErrorKind::Other,
                    "mmap failed",
                )));
            }

            {
                let disk_buffer =
                    unsafe { slice::from_raw_parts(mapped_ptr as *const u8, device_size) };
                for window in disk_buffer.windows(WINDOW_SIZE) {
                    if window == &*random_buffer as &[u8] {
                        unsafe {
                            libc::munmap(mapped_ptr, device_size);
                            libc::close(fd);
                        };
                        return Err(Box::new(io::Error::new(
                            io::ErrorKind::Other,
                            "Disk was not encrypted!",
                        )));
                    }
                }
            }

            unsafe {
                libc::munmap(mapped_ptr, device_size);
                libc::close(fd);
            };

            let device_name = handle.name.clone();
            loop {
                match libcryptsetup_rs::status(
                    Some(&mut handle.acquire_crypt_device().unwrap()),
                    &device_name,
                ) {
                    Ok(CryptStatusInfo::Busy) => (),
                    Ok(CryptStatusInfo::Active) => break,
                    Ok(s) => {
                        return Err(Box::new(io::Error::new(
                            io::ErrorKind::Other,
                            format!("Crypt device is in invalid state {:?}", s),
                        )))
                    }
                    Err(e) => {
                        return Err(Box::new(io::Error::new(
                            io::ErrorKind::Other,
                            format!("Checking device status returned error: {}", e),
                        )))
                    }
                }
            }

            handle.deactivate()?;

            let mut handle = CryptActivationHandle::setup(path, UnlockMethod::Keyring)?
                .ok_or_else(|| {
                    Box::new(io::Error::new(
                        io::ErrorKind::Other,
                        format!(
                            "Device {} no longer appears to be a LUKS2 device",
                            path.display(),
                        ),
                    ))
                })?;
            handle.wipe()?;

            Ok(())
        }

        assert_eq!(paths.len(), 1);

        crypt::insert_and_cleanup_key(paths, crypt_test);
    }

    #[test]
    fn real_test_crypt_device_ops() {
        real::test_with_spec(
            &real::DeviceLimits::Exactly(1, None, Some(Sectors(1024 * 1024 * 1024 / 512))),
            test_crypt_device_ops,
        );
    }

    #[test]
    fn loop_test_crypt_metadata_defaults() {
        fn test_defaults(paths: &[&Path]) {
            let mut context = CryptInit::init(paths[0]).unwrap();
            context
                .context_handle()
                .format::<()>(
                    EncryptionFormat::Luks2,
                    ("aes", "xts-plain64"),
                    None,
                    Either::Right(STRATIS_MEK_SIZE),
                    None,
                )
                .unwrap();
            let (metadata, keyslot) = context.settings_handle().get_metadata_size().unwrap();
            assert_eq!(DEFAULT_CRYPT_METADATA_SIZE, *metadata);
            assert_eq!(DEFAULT_CRYPT_KEYSLOTS_SIZE, *keyslot);
        }

        loopbacked::test_with_spec(&loopbacked::DeviceLimits::Exactly(1, None), test_defaults);
    }

    fn test_both_initialize(paths: &[&Path]) {
        fn both_initialize(
            paths: &[&Path],
            key_desc: &KeyDescription,
            _: (),
        ) -> Result<(), Box<dyn Error>> {
            let _memfs = MemoryFilesystem::new()?;
            let path = paths
                .get(0)
                .copied()
                .ok_or_else(|| StratisError::Error("Expected exactly one path".to_string()))?;
            let handle =
                CryptInitializer::new(path.to_owned(), PoolUuid::new_v4(), DevUuid::new_v4())
                    .initialize(
                        Some(key_desc),
                        Some((
                            "tang",
                            &json!({"url": env::var("TANG_URL")?, "stratis:tang:trust_url": true}),
                        )),
                    )?;

            let mut device = acquire_crypt_device(handle.luks2_device_path())?;
            device.token_handle().json_get(LUKS2_TOKEN_ID)?;
            device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID)?;
            Ok(())
        }

        crypt::insert_and_cleanup_key(paths, both_initialize);
    }

    #[test]
    fn clevis_real_test_both_initialize() {
        real::test_with_spec(
            &real::DeviceLimits::Exactly(1, None, Some(Sectors(1024 * 1024 * 1024 / 512))),
            test_both_initialize,
        );
    }

    #[test]
    fn clevis_loop_test_both_initialize() {
        loopbacked::test_with_spec(
            &loopbacked::DeviceLimits::Exactly(1, None),
            test_both_initialize,
        );
    }

    fn test_clevis_initialize(paths: &[&Path]) {
        let _memfs = MemoryFilesystem::new().unwrap();
        let path = paths[0];
        let handle = CryptInitializer::new(path.to_owned(), PoolUuid::new_v4(), DevUuid::new_v4())
            .initialize(
                None,
                Some((
                    "tang",
                    &json!({"url": env::var("TANG_URL").unwrap(), "stratis:tang:trust_url": true}),
                )),
            )
            .unwrap();

        let mut device = acquire_crypt_device(handle.luks2_device_path()).unwrap();
        assert!(device.token_handle().json_get(CLEVIS_LUKS_TOKEN_ID).is_ok());
        assert!(device.token_handle().json_get(LUKS2_TOKEN_ID).is_err());
    }

    #[test]
    fn clevis_real_test_initialize() {
        real::test_with_spec(
            &real::DeviceLimits::Exactly(1, None, Some(Sectors(1024 * 1024 * 1024 / 512))),
            test_clevis_initialize,
        );
    }

    #[test]
    fn clevis_loop_test_initialize() {
        loopbacked::test_with_spec(
            &loopbacked::DeviceLimits::Exactly(1, None),
            test_clevis_initialize,
        );
    }
}
