544 lines
22 KiB
Java
544 lines
22 KiB
Java
// SPDX-FileCopyrightText: 2020 Paul Schaub <vanitasvitae@fsfe.org>
|
|
//
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package org.pgpainless.key.modification.secretkeyring;
|
|
|
|
import java.io.IOException;
|
|
import java.security.InvalidAlgorithmParameterException;
|
|
import java.security.NoSuchAlgorithmException;
|
|
import java.util.Date;
|
|
import javax.annotation.Nonnull;
|
|
import javax.annotation.Nullable;
|
|
|
|
import org.bouncycastle.openpgp.PGPException;
|
|
import org.bouncycastle.openpgp.PGPKeyPair;
|
|
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
|
import org.bouncycastle.openpgp.PGPSignature;
|
|
import org.pgpainless.algorithm.KeyFlag;
|
|
import org.pgpainless.key.OpenPgpFingerprint;
|
|
import org.pgpainless.key.generation.KeySpec;
|
|
import org.pgpainless.key.protection.KeyRingProtectionSettings;
|
|
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
|
import org.pgpainless.key.util.RevocationAttributes;
|
|
import org.pgpainless.signature.subpackets.RevocationSignatureSubpackets;
|
|
import org.pgpainless.signature.subpackets.SelfSignatureSubpackets;
|
|
import org.pgpainless.util.Passphrase;
|
|
import org.pgpainless.util.selection.userid.SelectUserId;
|
|
|
|
public interface SecretKeyRingEditorInterface {
|
|
|
|
/**
|
|
* Add a user-id to the key ring.
|
|
*
|
|
* @param userId user-id
|
|
* @param secretKeyRingProtector protector to unlock the secret key
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface addUserId(
|
|
@Nonnull CharSequence userId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Add a user-id to the key ring.
|
|
*
|
|
* @param userId user-id
|
|
* @param signatureSubpacketCallback callback that can be used to modify signature subpackets of the
|
|
* certification signature.
|
|
* @param protector protector to unlock the primary secret key
|
|
* @return the builder
|
|
* @throws PGPException
|
|
*/
|
|
SecretKeyRingEditorInterface addUserId(
|
|
@Nonnull CharSequence userId,
|
|
@Nullable SelfSignatureSubpackets.Callback signatureSubpacketCallback,
|
|
@Nonnull SecretKeyRingProtector protector)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Add a user-id to the key ring and mark it as primary.
|
|
* If the user-id is already present, a new certification signature will be created.
|
|
*
|
|
* @param userId user id
|
|
* @param protector protector to unlock the secret key
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface addPrimaryUserId(
|
|
@Nonnull CharSequence userId,
|
|
@Nonnull SecretKeyRingProtector protector)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Convenience method to revoke selected user-ids using soft revocation signatures.
|
|
* The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}, so that the user-id
|
|
* can be re-certified at a later point.
|
|
*
|
|
* @param userIdSelector selector to select user-ids
|
|
* @param protector protector to unlock the primary key
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface removeUserId(SelectUserId userIdSelector,
|
|
SecretKeyRingProtector protector)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Convenience method to revoke a single user-id using a soft revocation signature.
|
|
* The revocation will use {@link RevocationAttributes.Reason#USER_ID_NO_LONGER_VALID}. so that the user-id
|
|
* can be re-certified at a later point.
|
|
*
|
|
* @param userId user-id to revoke
|
|
* @param protector protector to unlock the primary key
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface removeUserId(CharSequence userId,
|
|
SecretKeyRingProtector protector)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Add a subkey to the key ring.
|
|
* The subkey will be generated from the provided {@link KeySpec}.
|
|
*
|
|
* @param keySpec key specification
|
|
* @param subKeyPassphrase passphrase to encrypt the sub key
|
|
* @param secretKeyRingProtector protector to unlock the secret key of the key ring
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface addSubKey(
|
|
@Nonnull KeySpec keySpec,
|
|
@Nonnull Passphrase subKeyPassphrase,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws InvalidAlgorithmParameterException, NoSuchAlgorithmException, PGPException, IOException;
|
|
|
|
/**
|
|
* Add a subkey to the key ring.
|
|
* The subkey will be generated from the provided {@link KeySpec}.
|
|
*
|
|
* @param keySpec key spec of the subkey
|
|
* @param subkeyPassphrase passphrase to encrypt the subkey
|
|
* @param subpacketsCallback callback to modify the subpackets of the subkey binding signature
|
|
* @param secretKeyRingProtector protector to unlock the primary key
|
|
* @return builder
|
|
*/
|
|
SecretKeyRingEditorInterface addSubKey(
|
|
@Nonnull KeySpec keySpec,
|
|
@Nonnull Passphrase subkeyPassphrase,
|
|
@Nullable SelfSignatureSubpackets.Callback subpacketsCallback,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException, InvalidAlgorithmParameterException, NoSuchAlgorithmException, IOException;
|
|
|
|
/**
|
|
* Add a subkey to the key ring.
|
|
*
|
|
* @param subkey subkey key pair
|
|
* @param bindingSignatureCallback callback to modify the subpackets of the subkey binding signature
|
|
* @param subkeyProtector protector to unlock and encrypt the subkey
|
|
* @param primaryKeyProtector protector to unlock the primary key
|
|
* @param keyFlag first key flag for the subkey
|
|
* @param additionalKeyFlags optional additional key flags
|
|
* @return builder
|
|
*/
|
|
SecretKeyRingEditorInterface addSubKey(
|
|
@Nonnull PGPKeyPair subkey,
|
|
@Nullable SelfSignatureSubpackets.Callback bindingSignatureCallback,
|
|
@Nonnull SecretKeyRingProtector subkeyProtector,
|
|
@Nonnull SecretKeyRingProtector primaryKeyProtector,
|
|
@Nonnull KeyFlag keyFlag,
|
|
KeyFlag... additionalKeyFlags)
|
|
throws PGPException, IOException, NoSuchAlgorithmException;
|
|
|
|
/**
|
|
* Revoke the key ring.
|
|
* The revocation will be a hard revocation, rendering the whole key invalid for any past or future signatures.
|
|
*
|
|
* @param secretKeyRingProtector protector of the primary key
|
|
* @return the builder
|
|
*/
|
|
default SecretKeyRingEditorInterface revoke(
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException {
|
|
return revoke(secretKeyRingProtector, (RevocationAttributes) null);
|
|
}
|
|
|
|
/**
|
|
* Revoke the key ring using the provided revocation attributes.
|
|
* The attributes define, whether the revocation was a hard revocation or not.
|
|
*
|
|
* @param secretKeyRingProtector protector of the primary key
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface revoke(
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationAttributes revocationAttributes)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Revoke the key ring.
|
|
* You can use the {@link RevocationSignatureSubpackets.Callback} to modify the revocation signatures
|
|
* subpackets, eg. in order to define whether this is a hard or soft revocation.
|
|
*
|
|
* @param secretKeyRingProtector protector to unlock the primary secret key
|
|
* @param subpacketsCallback callback to modify the revocations subpackets
|
|
* @return builder
|
|
*/
|
|
SecretKeyRingEditorInterface revoke(
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationSignatureSubpackets.Callback subpacketsCallback) throws PGPException;
|
|
|
|
/**
|
|
* Revoke the subkey binding signature of a subkey.
|
|
* The subkey with the provided fingerprint will be revoked.
|
|
* If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown.
|
|
*
|
|
* Note: This method will hard-revoke the provided subkey, meaning it cannot be re-certified at a later point.
|
|
* If you instead want to temporarily "deactivate" the subkey, provide a soft revocation reason,
|
|
* eg. by calling {@link #revokeSubKey(OpenPgpFingerprint, SecretKeyRingProtector, RevocationAttributes)}
|
|
* and provide a suitable {@link RevocationAttributes} object.
|
|
*
|
|
* @param fingerprint fingerprint of the subkey to be revoked
|
|
* @param secretKeyRingProtector protector to unlock the secret key ring
|
|
* @return the builder
|
|
*/
|
|
default SecretKeyRingEditorInterface revokeSubKey(
|
|
@Nonnull OpenPgpFingerprint fingerprint,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException {
|
|
return revokeSubKey(fingerprint, secretKeyRingProtector, null);
|
|
}
|
|
|
|
/**
|
|
* Revoke the subkey binding signature of a subkey.
|
|
* The subkey with the provided fingerprint will be revoked.
|
|
* If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown.
|
|
*
|
|
* @param fingerprint fingerprint of the subkey to be revoked
|
|
* @param secretKeyRingProtector protector to unlock the primary key
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return the builder
|
|
*/
|
|
default SecretKeyRingEditorInterface revokeSubKey(
|
|
OpenPgpFingerprint fingerprint,
|
|
SecretKeyRingProtector secretKeyRingProtector,
|
|
RevocationAttributes revocationAttributes)
|
|
throws PGPException {
|
|
return revokeSubKey(fingerprint.getKeyId(),
|
|
secretKeyRingProtector,
|
|
revocationAttributes);
|
|
}
|
|
|
|
/**
|
|
* Revoke the subkey binding signature of a subkey.
|
|
* The subkey with the provided key-id will be revoked.
|
|
* If no suitable subkey is found, a {@link java.util.NoSuchElementException} will be thrown.
|
|
*
|
|
* @param subKeyId id of the subkey
|
|
* @param secretKeyRingProtector protector to unlock the primary key
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface revokeSubKey(
|
|
long subKeyId,
|
|
SecretKeyRingProtector secretKeyRingProtector,
|
|
RevocationAttributes revocationAttributes)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Revoke the subkey binding signature of a subkey.
|
|
* The subkey with the provided key-id will be revoked.
|
|
* If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown.
|
|
*
|
|
* Note: This method will hard-revoke the subkey, meaning it cannot be re-bound at a later point.
|
|
* If you intend to re-bind the subkey in order to make it usable again at a later point in time,
|
|
* consider using {@link #revokeSubKey(long, SecretKeyRingProtector, RevocationAttributes)}
|
|
* and provide a soft revocation reason.
|
|
*
|
|
* @param subKeyId id of the subkey
|
|
* @param secretKeyRingProtector protector to unlock the secret key ring
|
|
* @return the builder
|
|
*/
|
|
default SecretKeyRingEditorInterface revokeSubKey(
|
|
long subKeyId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException {
|
|
|
|
return revokeSubKey(
|
|
subKeyId,
|
|
secretKeyRingProtector,
|
|
(RevocationSignatureSubpackets.Callback) null);
|
|
}
|
|
|
|
/**
|
|
* Revoke the subkey binding signature of a subkey.
|
|
* The subkey with the provided key-id will be revoked.
|
|
* If no suitable subkey is found, q {@link java.util.NoSuchElementException} will be thrown.
|
|
*
|
|
* The provided subpackets callback is used to modify the revocation signatures subpackets.
|
|
*
|
|
* @param keyID id of the subkey
|
|
* @param secretKeyRingProtector protector to unlock the secret key ring
|
|
* @param subpacketsCallback callback which can be used to modify the subpackets of the revocation
|
|
* signature
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface revokeSubKey(
|
|
long keyID,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationSignatureSubpackets.Callback subpacketsCallback)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Revoke the given userID.
|
|
* The revocation will be a hard revocation, rendering the user-id invalid for any past or future signatures.
|
|
* If you intend to re-certify the user-id at a later point in time, consider using
|
|
* {@link #revokeUserId(CharSequence, SecretKeyRingProtector, RevocationAttributes)} instead and provide
|
|
* a soft revocation reason.
|
|
*
|
|
* @param userId userId to revoke
|
|
* @param secretKeyRingProtector protector to unlock the primary key
|
|
* @return the builder
|
|
*/
|
|
default SecretKeyRingEditorInterface revokeUserId(
|
|
@Nonnull CharSequence userId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException {
|
|
return revokeUserId(userId, secretKeyRingProtector, (RevocationAttributes) null);
|
|
}
|
|
|
|
/**
|
|
* Revoke the given userID using the provided revocation attributes.
|
|
*
|
|
* @param userId userId to revoke
|
|
* @param secretKeyRingProtector protector to unlock the primary key
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface revokeUserId(
|
|
@Nonnull CharSequence userId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationAttributes revocationAttributes)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Revoke the provided user-id.
|
|
* Note: If you don't provide a {@link RevocationSignatureSubpackets.Callback} which
|
|
* sets a revocation reason ({@link RevocationAttributes}), the revocation might be considered hard.
|
|
* So if you intend to re-certify the user-id at a later point to make it valid again,
|
|
* make sure to set a soft revocation reason in the signatures hashed area using the subpacket callback.
|
|
*
|
|
* @param userId userid to be revoked
|
|
* @param secretKeyRingProtector protector to unlock the primary secret key
|
|
* @param subpacketCallback callback to modify the revocations subpackets
|
|
* @return builder
|
|
*/
|
|
SecretKeyRingEditorInterface revokeUserId(
|
|
@Nonnull CharSequence userId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationSignatureSubpackets.Callback subpacketCallback)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Revoke all user-ids that match the provided {@link SelectUserId} filter.
|
|
* The provided {@link RevocationAttributes} will be set as reason for revocation in each
|
|
* revocation signature.
|
|
*
|
|
* Note: If you intend to re-certify these user-ids at a later point, make sure to choose
|
|
* a soft revocation reason. See {@link RevocationAttributes.Reason} for more information.
|
|
*
|
|
* @param userIdSelector user-id selector
|
|
* @param secretKeyRingProtector protector to unlock the primary secret key
|
|
* @param revocationAttributes revocation attributes
|
|
* @return builder
|
|
* @throws PGPException
|
|
*/
|
|
SecretKeyRingEditorInterface revokeUserIds(
|
|
@Nonnull SelectUserId userIdSelector,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationAttributes revocationAttributes)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Revoke all user-ids that match the provided {@link SelectUserId} filter.
|
|
* The provided {@link RevocationSignatureSubpackets.Callback} will be used to modify the
|
|
* revocation signatures subpackets.
|
|
*
|
|
* Note: If you intend to re-certify these user-ids at a later point, make sure to set
|
|
* a soft revocation reason in the revocation signatures hashed subpacket area using the callback.
|
|
*
|
|
* See {@link RevocationAttributes.Reason} for more information.
|
|
*
|
|
* @param userIdSelector user-id selector
|
|
* @param secretKeyRingProtector protector to unlock the primary secret key
|
|
* @param subpacketsCallback callback to modify the revocations subpackets
|
|
* @return builder
|
|
* @throws PGPException
|
|
*/
|
|
SecretKeyRingEditorInterface revokeUserIds(
|
|
@Nonnull SelectUserId userIdSelector,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationSignatureSubpackets.Callback subpacketsCallback)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Set the expiration date for the primary key of the key ring.
|
|
* If the key is supposed to never expire, then an expiration date of null is expected.
|
|
*
|
|
* @param expiration new expiration date or null
|
|
* @param secretKeyRingProtector to unlock the secret key
|
|
* @return the builder
|
|
*/
|
|
SecretKeyRingEditorInterface setExpirationDate(
|
|
@Nullable Date expiration,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Create a detached revocation certificate, which can be used to revoke the whole key.
|
|
*
|
|
* @param secretKeyRingProtector protector to unlock the primary key.
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return revocation certificate
|
|
*/
|
|
PGPSignature createRevocationCertificate(
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationAttributes revocationAttributes)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Create a detached revocation certificate, which can be used to revoke the specified subkey.
|
|
*
|
|
* @param subkeyId id of the subkey to be revoked
|
|
* @param secretKeyRingProtector protector to unlock the primary key.
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return revocation certificate
|
|
*/
|
|
PGPSignature createRevocationCertificate(
|
|
long subkeyId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationAttributes revocationAttributes)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Create a detached revocation certificate, which can be used to revoke the specified subkey.
|
|
*
|
|
* @param subkeyId id of the subkey to be revoked
|
|
* @param secretKeyRingProtector protector to unlock the primary key.
|
|
* @param certificateSubpacketsCallback callback to modify the subpackets of the revocation certificate.
|
|
* @return revocation certificate
|
|
*/
|
|
PGPSignature createRevocationCertificate(
|
|
long subkeyId,
|
|
@Nonnull SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationSignatureSubpackets.Callback certificateSubpacketsCallback)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Create a detached revocation certificate, which can be used to revoke the specified subkey.
|
|
*
|
|
* @param subkeyFingerprint fingerprint of the subkey to be revoked
|
|
* @param secretKeyRingProtector protector to unlock the primary key.
|
|
* @param revocationAttributes reason for the revocation
|
|
* @return revocation certificate
|
|
*/
|
|
default PGPSignature createRevocationCertificate(
|
|
OpenPgpFingerprint subkeyFingerprint,
|
|
SecretKeyRingProtector secretKeyRingProtector,
|
|
@Nullable RevocationAttributes revocationAttributes)
|
|
throws PGPException {
|
|
|
|
return createRevocationCertificate(
|
|
subkeyFingerprint.getKeyId(),
|
|
secretKeyRingProtector,
|
|
revocationAttributes);
|
|
}
|
|
|
|
/**
|
|
* Change the passphrase of the whole key ring.
|
|
*
|
|
* @param oldPassphrase old passphrase or null, if the key was unprotected
|
|
* @return next builder step
|
|
*/
|
|
default WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(
|
|
@Nullable Passphrase oldPassphrase) {
|
|
return changePassphraseFromOldPassphrase(oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings());
|
|
}
|
|
|
|
/**
|
|
* Change the passphrase of the whole key ring.
|
|
*
|
|
* @param oldPassphrase old passphrase or null, if the key was unprotected
|
|
* @param oldProtectionSettings custom settings for the old passphrase
|
|
* @return next builder step
|
|
*/
|
|
WithKeyRingEncryptionSettings changePassphraseFromOldPassphrase(
|
|
@Nullable Passphrase oldPassphrase,
|
|
@Nonnull KeyRingProtectionSettings oldProtectionSettings);
|
|
|
|
/**
|
|
* Change the passphrase of a single subkey in the key ring.
|
|
*
|
|
* Note: While it is a valid use-case to have different passphrases per subKey,
|
|
* this is one of the reasons why OpenPGP sucks in practice.
|
|
*
|
|
* @param keyId id of the subkey
|
|
* @param oldPassphrase old passphrase
|
|
* @return next builder step
|
|
*/
|
|
default WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(
|
|
@Nonnull Long keyId,
|
|
@Nullable Passphrase oldPassphrase) {
|
|
return changeSubKeyPassphraseFromOldPassphrase(keyId, oldPassphrase, KeyRingProtectionSettings.secureDefaultSettings());
|
|
}
|
|
|
|
WithKeyRingEncryptionSettings changeSubKeyPassphraseFromOldPassphrase(
|
|
@Nonnull Long keyId,
|
|
@Nullable Passphrase oldPassphrase,
|
|
@Nonnull KeyRingProtectionSettings oldProtectionSettings);
|
|
|
|
interface WithKeyRingEncryptionSettings {
|
|
|
|
/**
|
|
* Set secure default settings for the symmetric passphrase encryption.
|
|
* Note that this obviously has no effect if you decide to set {@link WithPassphrase#toNoPassphrase()}.
|
|
*
|
|
* @return next builder step
|
|
*/
|
|
WithPassphrase withSecureDefaultSettings();
|
|
|
|
/**
|
|
* Set custom settings for the symmetric passphrase encryption.
|
|
*
|
|
* @param settings custom settings
|
|
* @return next builder step
|
|
*/
|
|
WithPassphrase withCustomSettings(KeyRingProtectionSettings settings);
|
|
|
|
}
|
|
|
|
interface WithPassphrase {
|
|
|
|
/**
|
|
* Set the passphrase.
|
|
*
|
|
* @param passphrase passphrase
|
|
* @return editor builder
|
|
*/
|
|
SecretKeyRingEditorInterface toNewPassphrase(Passphrase passphrase)
|
|
throws PGPException;
|
|
|
|
/**
|
|
* Leave the key unprotected.
|
|
*
|
|
* @return editor builder
|
|
*/
|
|
SecretKeyRingEditorInterface toNoPassphrase() throws PGPException;
|
|
}
|
|
|
|
/**
|
|
* Return the {@link PGPSecretKeyRing}.
|
|
* @return the key
|
|
*/
|
|
PGPSecretKeyRing done();
|
|
|
|
}
|