use std::{
convert::TryFrom,
io,
path::{Path, PathBuf},
};
use data_encoding::BASE64URL_NOPAD;
use either::Either;
use serde_json::{Map, Value};
use sha1::{Digest, Sha1};
use libcryptsetup_rs::{
c_uint, CryptActivateFlags, CryptDeactivateFlags, CryptDevice, CryptInit, CryptStatusInfo,
CryptVolumeKeyFlags, CryptWipePattern, EncryptionFormat, LibcryptErr,
};
use crate::{
engine::{
strat_engine::{
backstore::crypt::{
consts::{
CLEVIS_LUKS_TOKEN_ID, CLEVIS_TANG_TRUST_URL, DEFAULT_CRYPT_KEYSLOTS_SIZE,
DEFAULT_CRYPT_METADATA_SIZE, DEVICEMAPPER_PATH, LUKS2_TOKEN_ID,
LUKS2_TOKEN_TYPE, SECTOR_SIZE, STRATIS_TOKEN_DEVNAME_KEY,
STRATIS_TOKEN_DEV_UUID_KEY, STRATIS_TOKEN_ID, STRATIS_TOKEN_POOL_UUID_KEY,
STRATIS_TOKEN_TYPE, TOKEN_KEYSLOTS_KEY, TOKEN_TYPE_KEY,
},
handle::CryptHandle,
},
cmd::clevis_luks_unlock,
keys,
metadata::StratisIdentifiers,
},
types::{DevUuid, EncryptionInfo, KeyDescription, PoolUuid, SizedKeyMemory, UnlockMethod},
},
stratis::{StratisError, StratisResult},
};
pub struct StratisLuks2Token {
pub devname: String,
pub 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(),
})
}
}
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),
})
}
}
pub 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(),
))
})
}
pub 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(())
}
pub 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::new(
physical_path.to_owned(),
activated_path,
identifiers,
EncryptionInfo {
key_description,
clevis_info,
},
name,
)))
}
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))
}
}
pub 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)
}
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)
}
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}))
}
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 {
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))
}
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())),
}
}
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,
))),
}
}
fn activate_with_keyring(crypt_device: &mut CryptDevice, name: &str) -> StratisResult<()> {
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(())
}
pub 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
}
};
device_is_active(crypt_device, name)?;
let mut activated_path = PathBuf::from(DEVICEMAPPER_PATH);
activated_path.push(name);
if activated_path.exists() {
Ok(activated_path)
} else {
Err(StratisError::Io(io::Error::from(io::ErrorKind::NotFound)))
}
}
pub 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<_>>(),
))
}
pub 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(())
}
fn ceiling_sector_size_alignment(bytes: u64) -> u64 {
bytes + (SECTOR_SIZE - (bytes % SECTOR_SIZE))
}
pub 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(())
}
pub 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(())
}
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)
}
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()
}
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))
}
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)
}
pub fn key_desc_from_metadata(device: &mut CryptDevice) -> Option<String> {
device.token_handle().luks2_keyring_get(LUKS2_TOKEN_ID).ok()
}
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))
}
pub fn crypt_metadata_size() -> u64 {
2 * DEFAULT_CRYPT_METADATA_SIZE + DEFAULT_CRYPT_KEYSLOTS_SIZE
}