diff options
Diffstat (limited to 'src/cryptsetup/cryptsetup.c')
-rw-r--r-- | src/cryptsetup/cryptsetup.c | 288 |
1 files changed, 205 insertions, 83 deletions
diff --git a/src/cryptsetup/cryptsetup.c b/src/cryptsetup/cryptsetup.c index 1822beb..85897ae 100644 --- a/src/cryptsetup/cryptsetup.c +++ b/src/cryptsetup/cryptsetup.c @@ -101,10 +101,12 @@ static uint32_t arg_tpm2_pcr_mask = UINT32_MAX; static char *arg_tpm2_signature = NULL; static bool arg_tpm2_pin = false; static char *arg_tpm2_pcrlock = NULL; -static bool arg_headless = false; static usec_t arg_token_timeout_usec = 30*USEC_PER_SEC; static unsigned arg_tpm2_measure_pcr = UINT_MAX; /* This and the following field is about measuring the unlocked volume key to the local TPM */ static char **arg_tpm2_measure_banks = NULL; +static char *arg_link_keyring = NULL; +static char *arg_link_key_type = NULL; +static char *arg_link_key_description = NULL; STATIC_DESTRUCTOR_REGISTER(arg_cipher, freep); STATIC_DESTRUCTOR_REGISTER(arg_hash, freep); @@ -118,6 +120,9 @@ STATIC_DESTRUCTOR_REGISTER(arg_tpm2_device, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_signature, freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_measure_banks, strv_freep); STATIC_DESTRUCTOR_REGISTER(arg_tpm2_pcrlock, freep); +STATIC_DESTRUCTOR_REGISTER(arg_link_keyring, freep); +STATIC_DESTRUCTOR_REGISTER(arg_link_key_type, freep); +STATIC_DESTRUCTOR_REGISTER(arg_link_key_description, freep); static const char* const passphrase_type_table[_PASSPHRASE_TYPE_MAX] = { [PASSPHRASE_REGULAR] = "passphrase", @@ -338,7 +343,7 @@ static int parse_one_option(const char *option) { arg_pkcs11_uri_auto = true; } else { if (!pkcs11_uri_valid(val)) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing"); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "pkcs11-uri= parameter expects a PKCS#11 URI, refusing."); r = free_and_strdup(&arg_pkcs11_uri, val); if (r < 0) @@ -368,7 +373,7 @@ static int parse_one_option(const char *option) { _cleanup_free_ void *cid = NULL; size_t cid_size; - r = unbase64mem(val, SIZE_MAX, &cid, &cid_size); + r = unbase64mem(val, &cid, &cid_size); if (r < 0) return log_error_errno(r, "Failed to decode FIDO2 CID data: %m"); @@ -498,9 +503,9 @@ static int parse_one_option(const char *option) { return 0; } - arg_headless = r; + SET_FLAG(arg_ask_password_flags, ASK_PASSWORD_HEADLESS, r); } else if (streq(option, "headless")) - arg_headless = true; + arg_ask_password_flags |= ASK_PASSWORD_HEADLESS; else if ((val = startswith(option, "token-timeout="))) { @@ -508,6 +513,56 @@ static int parse_one_option(const char *option) { if (r < 0) log_warning_errno(r, "Failed to parse %s, ignoring: %m", option); + } else if ((val = startswith(option, "link-volume-key="))) { +#ifdef HAVE_CRYPT_SET_KEYRING_TO_LINK + const char *sep, *c; + _cleanup_free_ char *keyring = NULL, *key_type = NULL, *key_description = NULL; + + /* Stick with cryptsetup --link-vk-to-keyring format + * <keyring_description>::%<key_type>:<key_description>, + * where %<key_type> is optional and defaults to 'user'. + */ + if (!(sep = strstr(val, "::"))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse link-volume-key= option value: %s", val); + + /* cryptsetup (cli) supports <keyring_description> passed in various formats: + * - well-known keyrings prefixed with '@' (@u user, @s session, etc) + * - text descriptions prefixed with "%:" or "%keyring:". + * - text description with no prefix. + * - numeric keyring id (ignored in current patch set). */ + if (IN_SET(*val, '@', '%')) + keyring = strndup(val, sep - val); + else + /* add type prefix if missing (crypt_set_keyring_to_link() expects it) */ + keyring = strnappend("%:", val, sep - val); + if (!keyring) + return log_oom(); + + sep += 2; + + /* %<key_type> is optional (and defaults to 'user') */ + if (*sep == '%') { + /* must be separated by colon */ + if (!(c = strchr(sep, ':'))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Failed to parse link-volume-key= option value: %s", val); + + key_type = strndup(sep + 1, c - sep - 1); + if (!key_type) + return log_oom(); + + sep = c + 1; + } + + key_description = strdup(sep); + if (!key_description) + return log_oom(); + + free_and_replace(arg_link_keyring, keyring); + free_and_replace(arg_link_key_type, key_type); + free_and_replace(arg_link_key_description, key_description); +#else + log_error("Build lacks libcryptsetup support for linking volume keys in user specified kernel keyrings upon device activation, ignoring: %s", option); +#endif } else if (!streq(option, "x-initrd.attach")) log_warning("Encountered unknown /etc/crypttab option '%s', ignoring.", option); @@ -742,17 +797,16 @@ static int get_password( PassphraseType passphrase_type, char ***ret) { - _cleanup_free_ char *friendly = NULL, *text = NULL, *disk_path = NULL; + _cleanup_free_ char *friendly = NULL, *text = NULL, *disk_path = NULL, *id = NULL; _cleanup_strv_free_erase_ char **passwords = NULL; - char *id; - int r = 0; AskPasswordFlags flags = arg_ask_password_flags | ASK_PASSWORD_PUSH_CACHE; + int r; assert(vol); assert(src); assert(ret); - if (arg_headless) + if (FLAGS_SET(arg_ask_password_flags, ASK_PASSWORD_HEADLESS)) return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "Password querying disabled via 'headless' option."); friendly = friendly_disk_name(src, vol); @@ -766,11 +820,23 @@ static int get_password( if (!disk_path) return log_oom(); - id = strjoina("cryptsetup:", disk_path); + id = strjoin("cryptsetup:", disk_path); + if (!id) + return log_oom(); + + AskPasswordRequest req = { + .message = text, + .icon = "drive-harddisk", + .id = id, + .keyring = "cryptsetup", + .credential = "cryptsetup.passphrase", + }; - r = ask_password_auto(text, "drive-harddisk", id, "cryptsetup", "cryptsetup.passphrase", until, - flags | (accept_cached*ASK_PASSWORD_ACCEPT_CACHED), - &passwords); + r = ask_password_auto( + &req, + until, + flags | (accept_cached*ASK_PASSWORD_ACCEPT_CACHED), + &passwords); if (r < 0) return log_error_errno(r, "Failed to query password: %m"); @@ -779,12 +845,19 @@ static int get_password( assert(strv_length(passwords) == 1); + text = mfree(text); if (asprintf(&text, "Please enter %s for disk %s (verification):", passphrase_type_to_string(passphrase_type), friendly) < 0) return log_oom(); - id = strjoina("cryptsetup-verification:", disk_path); + free(id); + id = strjoin("cryptsetup-verification:", disk_path); + if (!id) + return log_oom(); - r = ask_password_auto(text, "drive-harddisk", id, "cryptsetup", "cryptsetup.passphrase", until, flags, &passwords2); + req.message = text; + req.id = id; + + r = ask_password_auto(&req, until, flags, &passwords2); if (r < 0) return log_error_errno(r, "Failed to query verification password: %m"); @@ -1192,12 +1265,11 @@ static int crypt_activate_by_token_pin_ask_password( const char *name, const char *type, usec_t until, - bool headless, void *userdata, uint32_t activation_flags, const char *message, - const char *key_name, - const char *credential_name) { + const char *keyring, + const char *credential) { #if HAVE_LIBCRYPTSETUP_PLUGINS AskPasswordFlags flags = arg_ask_password_flags | ASK_PASSWORD_PUSH_CACHE | ASK_PASSWORD_ACCEPT_CACHED; @@ -1222,12 +1294,20 @@ static int crypt_activate_by_token_pin_ask_password( return r; } - if (headless) + if (FLAGS_SET(arg_ask_password_flags, ASK_PASSWORD_HEADLESS)) return log_error_errno(SYNTHETIC_ERRNO(ENOPKG), "PIN querying disabled via 'headless' option. Use the '$PIN' environment variable."); for (;;) { pins = strv_free_erase(pins); - r = ask_password_auto(message, "drive-harddisk", /* id= */ NULL, key_name, credential_name, until, flags, &pins); + + AskPasswordRequest req = { + .message = message, + .icon = "drive-harddisk", + .keyring = keyring, + .credential = credential, + }; + + r = ask_password_auto(&req, until, flags, &pins); if (r < 0) return r; @@ -1251,7 +1331,6 @@ static int attach_luks2_by_fido2_via_plugin( struct crypt_device *cd, const char *name, usec_t until, - bool headless, void *userdata, uint32_t activation_flags) { @@ -1260,7 +1339,6 @@ static int attach_luks2_by_fido2_via_plugin( name, "systemd-fido2", until, - headless, userdata, activation_flags, "Please enter security token PIN:", @@ -1315,7 +1393,7 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( for (;;) { if (use_libcryptsetup_plugin && !arg_fido2_cid) { - r = attach_luks2_by_fido2_via_plugin(cd, name, until, arg_headless, arg_fido2_device, flags); + r = attach_luks2_by_fido2_via_plugin(cd, name, until, arg_fido2_device, flags); if (IN_SET(r, -ENOTUNIQ, -ENXIO, -ENOENT)) return log_debug_errno(SYNTHETIC_ERRNO(EAGAIN), "Automatic FIDO2 metadata discovery was not possible because missing or not unique, falling back to traditional unlocking."); @@ -1331,10 +1409,11 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( key_file, arg_keyfile_size, arg_keyfile_offset, key_data, key_data_size, until, - arg_headless, required, - &decrypted_key, &decrypted_key_size, - arg_ask_password_flags); + "cryptsetup.fido2-pin", + arg_ask_password_flags, + &decrypted_key, + &decrypted_key_size); else r = acquire_fido2_key_auto( cd, @@ -1342,9 +1421,10 @@ static int attach_luks_or_plain_or_bitlk_by_fido2( friendly, arg_fido2_device, until, - arg_headless, - &decrypted_key, &decrypted_key_size, - arg_ask_password_flags); + "cryptsetup.fido2-pin", + arg_ask_password_flags, + &decrypted_key, + &decrypted_key_size); if (r >= 0) break; } @@ -1405,7 +1485,7 @@ static int attach_luks2_by_pkcs11_via_plugin( const char *name, const char *friendly_name, usec_t until, - bool headless, + const char *askpw_credential, uint32_t flags) { #if HAVE_LIBCRYPTSETUP_PLUGINS @@ -1417,7 +1497,7 @@ static int attach_luks2_by_pkcs11_via_plugin( systemd_pkcs11_plugin_params params = { .friendly_name = friendly_name, .until = until, - .headless = headless, + .askpw_credential = askpw_credential, .askpw_flags = arg_ask_password_flags, }; @@ -1481,7 +1561,13 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( for (;;) { if (use_libcryptsetup_plugin && arg_pkcs11_uri_auto) - r = attach_luks2_by_pkcs11_via_plugin(cd, name, friendly, until, arg_headless, flags); + r = attach_luks2_by_pkcs11_via_plugin( + cd, + name, + friendly, + until, + "cryptsetup.pkcs11-pin", + flags); else { r = decrypt_pkcs11_key( name, @@ -1490,7 +1576,7 @@ static int attach_luks_or_plain_or_bitlk_by_pkcs11( key_file, arg_keyfile_size, arg_keyfile_offset, key_data, key_data_size, until, - arg_headless, + arg_ask_password_flags, &decrypted_key, &decrypted_key_size); if (r >= 0) break; @@ -1615,7 +1701,6 @@ static int attach_luks2_by_tpm2_via_plugin( struct crypt_device *cd, const char *name, usec_t until, - bool headless, uint32_t flags) { #if HAVE_LIBCRYPTSETUP_PLUGINS @@ -1635,7 +1720,6 @@ static int attach_luks2_by_tpm2_via_plugin( name, "systemd-tpm2", until, - headless, ¶ms, flags, "Please enter TPM2 PIN:", @@ -1650,18 +1734,16 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( struct crypt_device *cd, const char *name, const char *key_file, - const void *key_data, - size_t key_data_size, + const struct iovec *key_data, usec_t until, uint32_t flags, bool pass_volume_key) { _cleanup_(sd_device_monitor_unrefp) sd_device_monitor *monitor = NULL; - _cleanup_(erase_and_freep) void *decrypted_key = NULL; + _cleanup_(iovec_done_erase) struct iovec decrypted_key = {}; _cleanup_(sd_event_unrefp) sd_event *event = NULL; _cleanup_free_ char *friendly = NULL; int keyslot = arg_key_slot, r; - size_t decrypted_key_size; assert(cd); assert(name); @@ -1672,7 +1754,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( return log_oom(); for (;;) { - if (key_file || key_data) { + if (key_file || iovec_is_set(key_data)) { /* If key data is specified, use that */ r = acquire_tpm2_key( @@ -1680,21 +1762,22 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( arg_tpm2_device, arg_tpm2_pcr_mask == UINT32_MAX ? TPM2_PCR_MASK_DEFAULT : arg_tpm2_pcr_mask, UINT16_MAX, - /* pubkey= */ NULL, /* pubkey_size= */ 0, + /* pubkey= */ NULL, /* pubkey_pcr_mask= */ 0, /* signature_path= */ NULL, /* pcrlock_path= */ NULL, /* primary_alg= */ 0, key_file, arg_keyfile_size, arg_keyfile_offset, - key_data, key_data_size, - /* policy_hash= */ NULL, /* policy_hash_size= */ 0, /* we don't know the policy hash */ - /* salt= */ NULL, /* salt_size= */ 0, - /* srk_buf= */ NULL, /* srk_buf_size= */ 0, + key_data, + /* policy_hash= */ NULL, /* we don't know the policy hash */ + /* salt= */ NULL, + /* srk= */ NULL, + /* pcrlock_nv= */ NULL, arg_tpm2_pin ? TPM2_FLAGS_USE_PIN : 0, until, - arg_headless, + "cryptsetup.tpm2-pin", arg_ask_password_flags, - &decrypted_key, &decrypted_key_size); + &decrypted_key); if (r >= 0) break; if (IN_SET(r, -EACCES, -ENOLCK)) @@ -1707,7 +1790,7 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( return -EAGAIN; /* Mangle error code: let's make any form of TPM2 failure non-fatal. */ } } else { - r = attach_luks2_by_tpm2_via_plugin(cd, name, until, arg_headless, flags); + r = attach_luks2_by_tpm2_via_plugin(cd, name, until, flags); if (r >= 0) return 0; /* EAGAIN means: no tpm2 chip found @@ -1725,8 +1808,6 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( } if (r == -EOPNOTSUPP) { /* Plugin not available, let's process TPM2 stuff right here instead */ - _cleanup_free_ void *blob = NULL, *policy_hash = NULL; - size_t blob_size, policy_hash_size; bool found_some = false; int token = 0; /* first token to look at */ @@ -1735,8 +1816,8 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( * works. */ for (;;) { - _cleanup_free_ void *pubkey = NULL, *salt = NULL, *srk_buf = NULL; - size_t pubkey_size = 0, salt_size = 0, srk_buf_size = 0; + _cleanup_(iovec_done) struct iovec pubkey = {}, salt = {}, srk = {}, pcrlock_nv = {}; + _cleanup_(iovec_done) struct iovec blob = {}, policy_hash = {}; uint32_t hash_pcr_mask, pubkey_pcr_mask; uint16_t pcr_bank, primary_alg; TPM2Flags tpm2_flags; @@ -1747,13 +1828,14 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( token, /* search for the token with this index, or any later index than this */ &hash_pcr_mask, &pcr_bank, - &pubkey, &pubkey_size, + &pubkey, &pubkey_pcr_mask, &primary_alg, - &blob, &blob_size, - &policy_hash, &policy_hash_size, - &salt, &salt_size, - &srk_buf, &srk_buf_size, + &blob, + &policy_hash, + &salt, + &srk, + &pcrlock_nv, &tpm2_flags, &keyslot, &token); @@ -1778,21 +1860,22 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( arg_tpm2_device, hash_pcr_mask, pcr_bank, - pubkey, pubkey_size, + &pubkey, pubkey_pcr_mask, arg_tpm2_signature, arg_tpm2_pcrlock, primary_alg, /* key_file= */ NULL, /* key_file_size= */ 0, /* key_file_offset= */ 0, /* no key file */ - blob, blob_size, - policy_hash, policy_hash_size, - salt, salt_size, - srk_buf, srk_buf_size, + &blob, + &policy_hash, + &salt, + &srk, + &pcrlock_nv, tpm2_flags, until, - arg_headless, + "cryptsetup.tpm2-pin", arg_ask_password_flags, - &decrypted_key, &decrypted_key_size); + &decrypted_key); if (IN_SET(r, -EACCES, -ENOLCK)) return log_notice_errno(SYNTHETIC_ERRNO(EAGAIN), "TPM2 PIN unlock failed, falling back to traditional unlocking."); if (r != -EPERM) @@ -1837,17 +1920,16 @@ static int attach_luks_or_plain_or_bitlk_by_tpm2( log_debug("Got one or more potentially relevant udev events, rescanning for TPM2..."); } - assert(decrypted_key); if (pass_volume_key) - r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key, decrypted_key_size, flags); + r = measured_crypt_activate_by_volume_key(cd, name, decrypted_key.iov_base, decrypted_key.iov_len, flags); else { _cleanup_(erase_and_freep) char *base64_encoded = NULL; ssize_t base64_encoded_size; /* Before using this key as passphrase we base64 encode it, for compat with homed */ - base64_encoded_size = base64mem(decrypted_key, decrypted_key_size, &base64_encoded); + base64_encoded_size = base64mem(decrypted_key.iov_base, decrypted_key.iov_len, &base64_encoded); if (base64_encoded_size < 0) return log_oom(); @@ -2045,7 +2127,7 @@ static int attach_luks_or_plain_or_bitlk( crypt_get_device_name(cd)); if (arg_tpm2_device || arg_tpm2_device_auto) - return attach_luks_or_plain_or_bitlk_by_tpm2(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); + return attach_luks_or_plain_or_bitlk_by_tpm2(cd, name, key_file, &IOVEC_MAKE(key_data, key_data_size), until, flags, pass_volume_key); if (arg_fido2_device || arg_fido2_device_auto) return attach_luks_or_plain_or_bitlk_by_fido2(cd, name, key_file, key_data, key_data_size, until, flags, pass_volume_key); if (arg_pkcs11_uri || arg_pkcs11_uri_auto) @@ -2289,6 +2371,15 @@ static int run(int argc, char *argv[]) { if (r < 0) return log_error_errno(r, "Failed to load LUKS superblock on device %s: %m", crypt_get_device_name(cd)); +/* since cryptsetup 2.7.0 (Jan 2024) */ +#if HAVE_CRYPT_SET_KEYRING_TO_LINK + if (arg_link_key_description) { + r = crypt_set_keyring_to_link(cd, arg_link_key_description, NULL, arg_link_key_type, arg_link_keyring); + if (r < 0) + log_warning_errno(r, "Failed to set keyring or key description to link volume key in, ignoring: %m"); + } +#endif + if (arg_header) { r = crypt_set_data_device(cd, source); if (r < 0) @@ -2302,7 +2393,6 @@ static int run(int argc, char *argv[]) { volume, /* type= */ NULL, until, - arg_headless, /* userdata= */ NULL, flags, "Please enter LUKS2 token PIN:", @@ -2326,8 +2416,10 @@ static int run(int argc, char *argv[]) { } #endif + bool use_cached_passphrase = true; + _cleanup_strv_free_erase_ char **passwords = NULL; for (tries = 0; arg_tries == 0 || tries < arg_tries; tries++) { - _cleanup_strv_free_erase_ char **passwords = NULL; + log_debug("Beginning attempt %u to unlock.", tries); /* When we were able to acquire multiple keys, let's always process them in this order: * @@ -2338,7 +2430,9 @@ static int run(int argc, char *argv[]) { * 5. We enquire the user for a password */ - if (!key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto && !arg_fido2_device && !arg_fido2_device_auto && !arg_tpm2_device && !arg_tpm2_device_auto) { + if (!passwords && !key_file && !key_data && !arg_pkcs11_uri && !arg_pkcs11_uri_auto && !arg_fido2_device && !arg_fido2_device_auto && !arg_tpm2_device && !arg_tpm2_device_auto) { + + /* If we have nothing to try anymore, then acquire a new password */ if (arg_try_empty_password) { /* Hmm, let's try an empty password now, but only once */ @@ -2358,7 +2452,8 @@ static int run(int argc, char *argv[]) { return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No passphrase or recovery key registered."); } - r = get_password(volume, source, until, tries == 0 && !arg_verify, passphrase_type, &passwords); + r = get_password(volume, source, until, use_cached_passphrase && !arg_verify, passphrase_type, &passwords); + use_cached_passphrase = false; if (r == -EAGAIN) continue; if (r < 0) @@ -2375,17 +2470,44 @@ static int run(int argc, char *argv[]) { if (r != -EAGAIN) return r; - /* Key not correct? Let's try again! */ + /* Key not correct? Let's try again, but let's invalidate one of the passed fields, + * so that we fallback to the next best thing. */ - key_file = NULL; - key_data = erase_and_free(key_data); - key_data_size = 0; - arg_pkcs11_uri = mfree(arg_pkcs11_uri); - arg_pkcs11_uri_auto = false; - arg_fido2_device = mfree(arg_fido2_device); - arg_fido2_device_auto = false; - arg_tpm2_device = mfree(arg_tpm2_device); - arg_tpm2_device_auto = false; + if (arg_tpm2_device || arg_tpm2_device_auto) { + arg_tpm2_device = mfree(arg_tpm2_device); + arg_tpm2_device_auto = false; + continue; + } + + if (arg_fido2_device || arg_fido2_device_auto) { + arg_fido2_device = mfree(arg_fido2_device); + arg_fido2_device_auto = false; + continue; + } + + if (arg_pkcs11_uri || arg_pkcs11_uri_auto) { + arg_pkcs11_uri = mfree(arg_pkcs11_uri); + arg_pkcs11_uri_auto = false; + continue; + } + + if (key_data) { + key_data = erase_and_free(key_data); + key_data_size = 0; + continue; + } + + if (key_file) { + key_file = NULL; + continue; + } + + if (passwords) { + passwords = strv_free_erase(passwords); + continue; + } + + log_debug("Prepared for next attempt to unlock."); } if (arg_tries != 0 && tries >= arg_tries) @@ -2395,7 +2517,7 @@ static int run(int argc, char *argv[]) { const char *volume = ASSERT_PTR(argv[optind + 1]); if (argc - optind >= 3) - return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "attach does not accept more than one argument."); + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "detach does not accept more than one argument."); if (!filename_is_valid(volume)) return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Volume name '%s' is not valid.", volume); |