// SPDX-FileCopyrightText: 2023 Paul Schaub // // SPDX-License-Identifier: Apache-2.0 package org.pgpainless.key.info import java.util.* import openpgp.openPgpKeyId import org.bouncycastle.openpgp.* import org.pgpainless.PGPainless import org.pgpainless.algorithm.* import org.pgpainless.bouncycastle.extensions.* import org.pgpainless.exception.KeyException.UnboundUserIdException import org.pgpainless.key.OpenPgpFingerprint import org.pgpainless.key.SubkeyIdentifier import org.pgpainless.key.util.KeyRingUtils import org.pgpainless.policy.Policy import org.pgpainless.signature.consumer.SignaturePicker import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil import org.pgpainless.signature.subpackets.SignatureSubpacketsUtil.Companion.getKeyExpirationTimeAsDate import org.pgpainless.util.DateUtil import org.slf4j.LoggerFactory class KeyRingInfo( val keys: PGPKeyRing, val policy: Policy = PGPainless.getPolicy(), val referenceDate: Date = Date() ) { @JvmOverloads constructor( keys: PGPKeyRing, referenceDate: Date = Date() ) : this(keys, PGPainless.getPolicy(), referenceDate) private val signatures: Signatures = Signatures(keys, referenceDate, policy) /** Primary {@link PGPPublicKey}.ยด */ val publicKey: PGPPublicKey = KeyRingUtils.requirePrimaryPublicKeyFrom(keys) /** Primary key ID. */ val keyId: Long = publicKey.keyID /** Primary key fingerprint. */ val fingerprint: OpenPgpFingerprint = OpenPgpFingerprint.of(keys) /** All User-IDs (valid, expired, revoked). */ val userIds: List = KeyRingUtils.getUserIdsIgnoringInvalidUTF8(publicKey) /** Primary User-ID. */ val primaryUserId = findPrimaryUserId() /** Revocation State. */ val revocationState = signatures.primaryKeyRevocation.toRevocationState() /** * Return the date on which the primary key was revoked, or null if it has not yet been revoked. * * @return revocation date or null */ val revocationDate: Date? = if (revocationState.isSoftRevocation()) revocationState.date else null /** * Primary [PGPSecretKey] of this key ring or null if the key ring is not a [PGPSecretKeyRing]. */ val secretKey: PGPSecretKey? = when (keys) { is PGPSecretKeyRing -> keys.secretKey!! else -> null } /** OpenPGP key version. */ val version: Int = publicKey.version /** * Return all {@link PGPPublicKey PGPPublicKeys} of this key ring. The first key in the list * being the primary key. Note that the list is unmodifiable. * * @return list of public keys */ val publicKeys: List = keys.publicKeys.asSequence().toList() /** All secret keys. If the key ring is a [PGPPublicKeyRing], then return an empty list. */ val secretKeys: List = when (keys) { is PGPSecretKeyRing -> keys.secretKeys.asSequence().toList() else -> listOf() } /** List of valid public subkeys. */ val validSubkeys: List = keys.publicKeys.asSequence().filter { isKeyValidlyBound(it.keyID) }.toList() /** List of valid user-IDs. */ val validUserIds: List = userIds.filter { isUserIdBound(it) } /** List of valid and expired user-IDs. */ val validAndExpiredUserIds: List = userIds.filter { val certification = signatures.userIdCertifications[it] ?: return@filter false val revocation = signatures.userIdRevocations[it] ?: return@filter true return@filter !revocation.isHardRevocation && certification.creationTime > revocation.creationTime } /** List of email addresses that can be extracted from the user-IDs. */ val emailAddresses: List = userIds.mapNotNull { PATTERN_EMAIL_FROM_USERID.matcher(it).let { m1 -> if (m1.find()) m1.group(1) else PATTERN_EMAIL_EXPLICIT.matcher(it).let { m2 -> if (m2.find()) m2.group(1) else null } } } /** Newest direct-key self-signature on the primary key. */ val latestDirectKeySelfSignature: PGPSignature? = signatures.primaryKeySelfSignature /** Newest primary-key revocation self-signature. */ val revocationSelfSignature: PGPSignature? = signatures.primaryKeyRevocation /** Public-key encryption-algorithm of the primary key. */ val algorithm: PublicKeyAlgorithm = PublicKeyAlgorithm.requireFromId(publicKey.algorithm) /** Creation date of the primary key. */ val creationDate: Date = publicKey.creationTime!! /** Latest date at which the key was modified (either by adding a subkey or self-signature). */ val lastModified: Date = getMostRecentSignature()?.creationTime ?: getLatestKeyCreationDate() /** True, if the underlying keyring is a [PGPSecretKeyRing]. */ val isSecretKey: Boolean = keys is PGPSecretKeyRing /** True, if there are no encrypted secret keys. */ val isFullyDecrypted: Boolean = !isSecretKey || secretKeys.all { it.hasDummyS2K() || it.isDecrypted() } /** True, if there are only encrypted secret keys. */ val isFullyEncrypted: Boolean = isSecretKey && secretKeys.none { !it.hasDummyS2K() && it.isDecrypted() } /** List of public keys, whose secret key counterparts can be used to decrypt messages. */ val decryptionSubkeys: List = keys.publicKeys .asSequence() .filter { if (it.keyID != keyId) { if (signatures.subkeyBindings[it.keyID] == null) { LOGGER.debug("Subkey ${it.keyID.openPgpKeyId()} has no binding signature.") return@filter false } } if (!it.isEncryptionKey) { LOGGER.debug("(Sub-?)Key ${it.keyID.openPgpKeyId()} is not encryption-capable.") return@filter false } return@filter true } .toList() /** Expiration date of the primary key. */ val primaryKeyExpirationDate: Date? get() { val directKeyExpirationDate: Date? = latestDirectKeySelfSignature?.let { getKeyExpirationTimeAsDate(it, publicKey) } val possiblyExpiredPrimaryUserId = getPossiblyExpiredPrimaryUserId() val primaryUserIdCertification = possiblyExpiredPrimaryUserId?.let { getLatestUserIdCertification(it) } val userIdExpirationDate: Date? = primaryUserIdCertification?.let { getKeyExpirationTimeAsDate(it, publicKey) } if (latestDirectKeySelfSignature == null && primaryUserIdCertification == null) { /* throw NoSuchElementException( "No direct-key signature and no user-id signature found.") */ return null } if (directKeyExpirationDate != null && userIdExpirationDate == null) { return directKeyExpirationDate } if (directKeyExpirationDate == null) { return userIdExpirationDate } return if (directKeyExpirationDate < userIdExpirationDate) directKeyExpirationDate else userIdExpirationDate } /** List of all subkeys that can be used to sign a message. */ val signingSubkeys: List = validSubkeys.filter { getKeyFlagsOf(it.keyID).contains(KeyFlag.SIGN_DATA) } /** Whether the key is usable for encryption. */ val isUsableForEncryption: Boolean = isUsableForEncryption(EncryptionPurpose.ANY) /** * Whether the key is capable of signing messages. This field is also true, if the key contains * a subkey that is capable of signing messages, but where the secret key is unavailable, e.g. * because it was moved to a smart-card. * * To check for keys that are actually usable to sign messages, use [isUsableForSigning]. */ val isSigningCapable: Boolean = isKeyValidlyBound(keyId) && signingSubkeys.isNotEmpty() /** Whether the key is actually usable to sign messages. */ val isUsableForSigning: Boolean = isSigningCapable && signingSubkeys.any { isSecretKeyAvailable(it.keyID) } /** [HashAlgorithm] preferences of the primary user-ID or if absent, of the primary key. */ val preferredHashAlgorithms: Set get() = primaryUserId?.let { getPreferredHashAlgorithms(it) } ?: getPreferredHashAlgorithms(keyId) /** * [SymmetricKeyAlgorithm] preferences of the primary user-ID or if absent of the primary key. */ val preferredSymmetricKeyAlgorithms: Set get() = primaryUserId?.let { getPreferredSymmetricKeyAlgorithms(it) } ?: getPreferredSymmetricKeyAlgorithms(keyId) /** [CompressionAlgorithm] preferences of the primary user-ID or if absent, the primary key. */ val preferredCompressionAlgorithms: Set get() = primaryUserId?.let { getPreferredCompressionAlgorithms(it) } ?: getPreferredCompressionAlgorithms(keyId) /** * Return the expiration date of the subkey with the provided fingerprint. * * @param fingerprint subkey fingerprint * @return expiration date or null */ fun getSubkeyExpirationDate(fingerprint: OpenPgpFingerprint): Date? { return getSubkeyExpirationDate(fingerprint.keyId) } /** * Return the expiration date of the subkey with the provided keyId. * * @param keyId subkey keyId * @return expiration date */ fun getSubkeyExpirationDate(keyId: Long): Date? { if (publicKey.keyID == keyId) return primaryKeyExpirationDate val subkey = getPublicKey(keyId) ?: throw NoSuchElementException( "No subkey with key-ID ${keyId.openPgpKeyId()} found.") val bindingSig = getCurrentSubkeyBindingSignature(keyId) ?: throw AssertionError("Subkey has no valid binding signature.") return bindingSig.getKeyExpirationDate(subkey.creationTime) } /** * Return the date after which the key can no longer be used to perform the given use-case, * caused by expiration. * * @return expiration date for the given use-case */ fun getExpirationDateForUse(use: KeyFlag): Date? { require(use != KeyFlag.SPLIT && use != KeyFlag.SHARED) { "SPLIT and SHARED are not uses, but properties." } val primaryKeyExpiration = primaryKeyExpirationDate val keysWithFlag: List = getKeysWithKeyFlag(use) if (keysWithFlag.isEmpty()) throw NoSuchElementException("No key with the required key flag found.") var nonExpiring = false val latestSubkeyExpiration = keysWithFlag .map { key -> getSubkeyExpirationDate(key.keyID).also { if (it == null) nonExpiring = true } } .filterNotNull() .maxByOrNull { it } if (nonExpiring) return primaryKeyExpiration return if (primaryKeyExpiration == null) latestSubkeyExpiration else if (latestSubkeyExpiration == null) primaryKeyExpiration else minOf(primaryKeyExpiration, latestSubkeyExpiration) } /** * Return true, if the given user-ID is hard-revoked. * * @return true, if the given user-ID is hard-revoked. */ fun isHardRevoked(userId: CharSequence): Boolean { return signatures.userIdRevocations[userId]?.isHardRevocation ?: false } /** * Return a list of all keys which carry the provided key flag in their signature. * * @param flag flag * @return keys with flag */ fun getKeysWithKeyFlag(flag: KeyFlag): List = publicKeys.filter { getKeyFlagsOf(it.keyID).contains(flag) } /** * Return a list of all subkeys which can be used to encrypt a message for the given user-ID. * * @return encryption subkeys */ fun getEncryptionSubkeys( userId: CharSequence?, purpose: EncryptionPurpose ): List { if (userId != null && !isUserIdValid(userId)) { throw UnboundUserIdException( OpenPgpFingerprint.of(keys), userId.toString(), getLatestUserIdCertification(userId), getUserIdRevocation(userId)) } return getEncryptionSubkeys(purpose) } /** * Return a list of all subkeys which can be used to encrypt a message, given the purpose. * * @return subkeys which can be used for encryption */ fun getEncryptionSubkeys(purpose: EncryptionPurpose): List { primaryKeyExpirationDate?.let { if (it < referenceDate) { LOGGER.debug( "Certificate is expired: Primary key is expired on ${DateUtil.formatUTCDate(it)}") return listOf() } } return keys.publicKeys .asSequence() .filter { if (!isKeyValidlyBound(it.keyID)) { LOGGER.debug("(Sub?)-Key ${it.keyID.openPgpKeyId()} is not validly bound.") return@filter false } getSubkeyExpirationDate(it.keyID)?.let { exp -> if (exp < referenceDate) { LOGGER.debug( "(Sub?)-Key ${it.keyID.openPgpKeyId()} is expired on ${DateUtil.formatUTCDate(exp)}.") return@filter false } } if (!it.isEncryptionKey) { LOGGER.debug( "(Sub?)-Key ${it.keyID.openPgpKeyId()} algorithm is not capable of encryption.") return@filter false } val keyFlags = getKeyFlagsOf(it.keyID) when (purpose) { EncryptionPurpose.COMMUNICATIONS -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) EncryptionPurpose.STORAGE -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) EncryptionPurpose.ANY -> return@filter keyFlags.contains(KeyFlag.ENCRYPT_COMMS) || keyFlags.contains(KeyFlag.ENCRYPT_STORAGE) } } .toList() } /** * Return, whether the key is usable for encryption, given the purpose. * * @return true, if the key can be used to encrypt a message according to the * encryption-purpose. */ fun isUsableForEncryption(purpose: EncryptionPurpose): Boolean { return isKeyValidlyBound(keyId) && getEncryptionSubkeys(purpose).isNotEmpty() } /** * Return the primary user-ID, even if it is possibly expired. * * @return possibly expired primary user-ID */ fun getPossiblyExpiredPrimaryUserId(): String? = primaryUserId ?: userIds .mapNotNull { userId -> getLatestUserIdCertification(userId)?.let { userId to it } } .sortedByDescending { it.second.creationTime } .maxByOrNull { it.second.hashedSubPackets.isPrimaryUserID } ?.first /** Return the most-recently created self-signature on the key. */ private fun getMostRecentSignature(): PGPSignature? = setOfNotNull(latestDirectKeySelfSignature, revocationSelfSignature) .asSequence() .plus(signatures.userIdCertifications.values) .plus(signatures.userIdRevocations.values) .plus(signatures.subkeyBindings.values) .plus(signatures.subkeyRevocations.values) .maxByOrNull { creationDate } /** * Return the creation time of the latest added subkey. * * @return latest key creation time */ fun getLatestKeyCreationDate(): Date = validSubkeys.maxByOrNull { creationDate }?.creationTime ?: throw AssertionError("Apparently there is no validly bound key in this key ring.") /** * Return the latest certification self-signature for the given user-ID. * * @return latest self-certification for the given user-ID. */ fun getLatestUserIdCertification(userId: CharSequence): PGPSignature? = signatures.userIdCertifications[userId] /** * Return the latest revocation self-signature for the given user-ID * * @return latest user-ID revocation for the given user-ID */ fun getUserIdRevocation(userId: CharSequence): PGPSignature? = signatures.userIdRevocations[userId] /** * Return the current binding signature for the subkey with the given key-ID. * * @return current subkey binding signature */ fun getCurrentSubkeyBindingSignature(keyId: Long): PGPSignature? = signatures.subkeyBindings[keyId] /** * Return the current revocation signature for the subkey with the given key-ID. * * @return current subkey revocation signature */ fun getSubkeyRevocationSignature(keyId: Long): PGPSignature? = signatures.subkeyRevocations[keyId] /** * Return a list of {@link KeyFlag KeyFlags} that apply to the subkey with the provided key id. * * @param keyId key-id * @return list of key flags */ fun getKeyFlagsOf(keyId: Long): List = if (keyId == publicKey.keyID) { latestDirectKeySelfSignature?.let { sig -> SignatureSubpacketsUtil.parseKeyFlags(sig)?.let { flags -> return flags } } primaryUserId?.let { SignatureSubpacketsUtil.parseKeyFlags(getLatestUserIdCertification(it))?.let { flags -> return flags } } listOf() } else { getCurrentSubkeyBindingSignature(keyId)?.let { SignatureSubpacketsUtil.parseKeyFlags(it)?.let { flags -> return flags } } listOf() } /** * Return a list of {@link KeyFlag KeyFlags} that apply to the given user-id. * * @param userId user-id * @return key flags */ fun getKeyFlagsOf(userId: CharSequence): List = if (!isUserIdValid(userId)) { listOf() } else { getLatestUserIdCertification(userId)?.let { SignatureSubpacketsUtil.parseKeyFlags(it) ?: listOf() } ?: throw AssertionError( "While user-id '$userId' was reported as valid, there appears to be no certification for it.") } /** * Return the public key with the given key id from the provided key ring. * * @param keyId key id * @return public key or null */ fun getPublicKey(keyId: Long): PGPPublicKey? = keys.getPublicKey(keyId) /** * Return the secret key with the given key id. * * @param keyId key id * @return secret key or null */ fun getSecretKey(keyId: Long): PGPSecretKey? = when (keys) { is PGPSecretKeyRing -> keys.getSecretKey(keyId) else -> null } /** * Return true, if the secret-key with the given key-ID is available (i.e. not moved to a * smart-card). * * @return availability of the secret key */ fun isSecretKeyAvailable(keyId: Long): Boolean { return getSecretKey(keyId)?.let { return if (it.s2K == null) true // Unencrypted key else it.s2K.type !in 100..110 // Secret key on smart-card } ?: false // Missing secret key } /** * Return the public key with the given fingerprint. * * @param fingerprint fingerprint * @return public key or null */ fun getPublicKey(fingerprint: OpenPgpFingerprint): PGPPublicKey? = keys.getPublicKey(fingerprint.keyId) /** * Return the secret key with the given fingerprint. * * @param fingerprint fingerprint * @return secret key or null */ fun getSecretKey(fingerprint: OpenPgpFingerprint): PGPSecretKey? = when (keys) { is PGPSecretKeyRing -> keys.getSecretKey(fingerprint.keyId) else -> null } /** * Return the public key matching the given [SubkeyIdentifier]. * * @return public key * @throws IllegalArgumentException if the identifier's primary key does not match the primary * key of the key. */ fun getPublicKey(identifier: SubkeyIdentifier): PGPPublicKey? { require(identifier.primaryKeyId == publicKey.keyID) { "Mismatching primary key ID." } return getPublicKey(identifier.subkeyId) } /** * Return the secret key matching the given [SubkeyIdentifier]. * * @return secret key * @throws IllegalArgumentException if the identifier's primary key does not match the primary * key of the key. */ fun getSecretKey(identifier: SubkeyIdentifier): PGPSecretKey? = when (keys) { is PGPSecretKeyRing -> { require(identifier.primaryKeyId == publicKey.keyID) { "Mismatching primary key ID." } keys.getSecretKey(identifier.subkeyId) } else -> null } /** * Return true if the public key with the given key id is bound to the key ring properly. * * @param keyId key id * @return true if key is bound validly */ fun isKeyValidlyBound(keyId: Long): Boolean { val publicKey = keys.getPublicKey(keyId) ?: return false // Primary key -> Check Primary Key Revocation if (publicKey.keyID == this.publicKey.keyID) { return if (signatures.primaryKeyRevocation != null && signatures.primaryKeyRevocation.isHardRevocation) { false } else signatures.primaryKeyRevocation == null } // Else Subkey -> Check Subkey Revocation val binding = signatures.subkeyBindings[keyId] val revocation = signatures.subkeyRevocations[keyId] // No valid binding if (binding == null || binding.isExpired(referenceDate)) { return false } // Revocation return if (revocation != null) { if (revocation.isHardRevocation) { // Subkey is hard revoked false } else { // Key is soft-revoked, not yet re-bound (revocation.isExpired(referenceDate) || !revocation.creationTime.after(binding.creationTime)) } } else true } /** * Return the current primary user-id of the key ring. * *

* Note: If no user-id is marked as primary key using a {@link PrimaryUserID} packet, this * method returns the first user-id on the key, otherwise null. * * @return primary user-id or null */ private fun findPrimaryUserId(): String? { if (userIds.isEmpty()) { return null } return signatures.userIdCertifications .filter { (_, certification) -> certification.hashedSubPackets.isPrimaryUserID } .entries .maxByOrNull { (_, certification) -> certification.creationTime } ?.key ?: signatures.userIdCertifications.keys.firstOrNull() } /** Return true, if the primary user-ID, as well as the given user-ID are valid and bound. */ fun isUserIdValid(userId: CharSequence) = if (primaryUserId == null) { false } else { isUserIdBound(primaryUserId) && (if (userId == primaryUserId) true else isUserIdBound(userId)) } /** Return true, if the given user-ID is validly bound. */ fun isUserIdBound(userId: CharSequence) = signatures.userIdCertifications[userId]?.let { sig -> if (sig.isExpired(referenceDate)) { // certification expired return false } if (sig.hashedSubPackets.isPrimaryUserID) { getKeyExpirationTimeAsDate(sig, publicKey)?.let { expirationDate -> // key expired? if (expirationDate < referenceDate) return false } } signatures.userIdRevocations[userId]?.let { rev -> if (rev.isHardRevocation) { return false // hard revoked -> invalid } sig.creationTime > rev.creationTime // re-certification after soft revocation? } ?: true // certification, but no revocation } ?: false // no certification /** [HashAlgorithm] preferences of the given user-ID. */ fun getPreferredHashAlgorithms(userId: CharSequence): Set { return getKeyAccessor(userId, keyId).preferredHashAlgorithms } /** [HashAlgorithm] preferences of the given key. */ fun getPreferredHashAlgorithms(keyId: Long): Set { return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)).preferredHashAlgorithms } /** [SymmetricKeyAlgorithm] preferences of the given user-ID. */ fun getPreferredSymmetricKeyAlgorithms(userId: CharSequence): Set { return getKeyAccessor(userId, keyId).preferredSymmetricKeyAlgorithms } /** [SymmetricKeyAlgorithm] preferences of the given key. */ fun getPreferredSymmetricKeyAlgorithms(keyId: Long): Set { return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) .preferredSymmetricKeyAlgorithms } /** [CompressionAlgorithm] preferences of the given user-ID. */ fun getPreferredCompressionAlgorithms(userId: CharSequence): Set { return getKeyAccessor(userId, keyId).preferredCompressionAlgorithms } /** [CompressionAlgorithm] preferences of the given key. */ fun getPreferredCompressionAlgorithms(keyId: Long): Set { return KeyAccessor.SubKey(this, SubkeyIdentifier(keys, keyId)) .preferredCompressionAlgorithms } val isUsableForThirdPartyCertification: Boolean = isKeyValidlyBound(keyId) && getKeyFlagsOf(keyId).contains(KeyFlag.CERTIFY_OTHER) private fun getKeyAccessor(userId: CharSequence?, keyId: Long): KeyAccessor { if (getPublicKey(keyId) == null) { throw NoSuchElementException( "No subkey with key-id ${keyId.openPgpKeyId()} found on this key.") } if (userId != null && !userIds.contains(userId)) { throw NoSuchElementException("No user-id '$userId' found on this key.") } return if (userId != null) { KeyAccessor.ViaUserId(this, SubkeyIdentifier(keys, keyId), userId) } else { KeyAccessor.ViaKeyId(this, SubkeyIdentifier(keys, keyId)) } } companion object { /** Evaluate the key for the given signature. */ @JvmStatic fun evaluateForSignature(keys: PGPKeyRing, signature: PGPSignature) = KeyRingInfo(keys, signature.creationTime!!) private val PATTERN_EMAIL_FROM_USERID = "<([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)>".toPattern() private val PATTERN_EMAIL_EXPLICIT = "^([a-zA-Z0-9_!#$%&'*+/=?`{|}~^.-]+@[a-zA-Z0-9.-]+)$".toPattern() @JvmStatic private val LOGGER = LoggerFactory.getLogger(KeyRingInfo::class.java) } private class Signatures(val keys: PGPKeyRing, val referenceDate: Date, val policy: Policy) { val primaryKeyRevocation: PGPSignature? = SignaturePicker.pickCurrentRevocationSelfSignature(keys, policy, referenceDate) val primaryKeySelfSignature: PGPSignature? = SignaturePicker.pickLatestDirectKeySignature(keys, policy, referenceDate) val userIdRevocations = mutableMapOf() val userIdCertifications = mutableMapOf() val subkeyRevocations = mutableMapOf() val subkeyBindings = mutableMapOf() init { KeyRingUtils.getUserIdsIgnoringInvalidUTF8(keys.publicKey).forEach { userId -> SignaturePicker.pickCurrentUserIdRevocationSignature( keys, userId, policy, referenceDate) ?.let { userIdRevocations[userId] = it } SignaturePicker.pickLatestUserIdCertificationSignature( keys, userId, policy, referenceDate) ?.let { userIdCertifications[userId] = it } } keys.publicKeys.asSequence().drop(1).forEach { subkey -> SignaturePicker.pickCurrentSubkeyBindingRevocationSignature( keys, subkey, policy, referenceDate) ?.let { subkeyRevocations[subkey.keyID] = it } SignaturePicker.pickLatestSubkeyBindingSignature( keys, subkey, policy, referenceDate) ?.let { subkeyBindings[subkey.keyID] = it } } } } }