mirror of
https://github.com/pgpainless/pgpainless.git
synced 2024-11-23 04:42:06 +01:00
Begin introducing new Decryption API
This commit is contained in:
parent
8fffa3079a
commit
70c4dcd1d2
5 changed files with 293 additions and 63 deletions
|
@ -0,0 +1,188 @@
|
|||
package org.pgpainless.decryption_verification;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import javax.annotation.Nonnull;
|
||||
import javax.annotation.Nullable;
|
||||
|
||||
import org.bouncycastle.openpgp.PGPException;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
|
||||
import org.bouncycastle.openpgp.PGPSignature;
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||
import org.pgpainless.util.Passphrase;
|
||||
|
||||
/**
|
||||
* Options for decryption and signature verification.
|
||||
*/
|
||||
public class ConsumerOptions {
|
||||
|
||||
private Date verifyNotBefore;
|
||||
private Date verifyNotAfter;
|
||||
|
||||
// Set of verification keys
|
||||
private Set<PGPPublicKeyRing> certificates = new HashSet<>();
|
||||
private Set<PGPSignature> detachedSignatures = new HashSet<>();
|
||||
private MissingPublicKeyCallback missingCertificateCallback = null;
|
||||
|
||||
// Session key for decryption without passphrase/key
|
||||
private byte[] sessionKey = null;
|
||||
|
||||
private final Map<PGPSecretKeyRing, SecretKeyRingProtector> decryptionKeys = new HashMap<>();
|
||||
private final Set<Passphrase> decryptionPassphrases = new HashSet<>();
|
||||
|
||||
|
||||
/**
|
||||
* Consider signatures made before the given timestamp invalid.
|
||||
*
|
||||
* @param timestamp timestamp
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions verifyNotBefore(Date timestamp) {
|
||||
this.verifyNotBefore = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consider signatures made after the given timestamp invalid.
|
||||
*
|
||||
* @param timestamp timestamp
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions verifyNotAfter(Date timestamp) {
|
||||
this.verifyNotAfter = timestamp;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a certificate (public key ring) for signature verification.
|
||||
*
|
||||
* @param verificationCert certificate for signature verification
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addVerificationCert(PGPPublicKeyRing verificationCert) {
|
||||
this.certificates.add(verificationCert);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a set of certificates (public key rings) for signature verification.
|
||||
*
|
||||
* @param verificationCerts certificates for signature verification
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addVerificationCerts(PGPPublicKeyRingCollection verificationCerts) {
|
||||
for (PGPPublicKeyRing certificate : verificationCerts) {
|
||||
addVerificationCert(certificate);
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a detached signature for the signature verification process.
|
||||
*
|
||||
* @param detachedSignature detached signature
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addVerificationOfDetachedSignature(PGPSignature detachedSignature) {
|
||||
detachedSignatures.add(detachedSignature);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a callback that's used when a certificate (public key) is missing for signature verification.
|
||||
*
|
||||
* @param callback callback
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions setMissingCertificateCallback(MissingPublicKeyCallback callback) {
|
||||
this.missingCertificateCallback = callback;
|
||||
return this;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Attempt decryption using a session key.
|
||||
*
|
||||
* Note: PGPainless does not yet support decryption with session keys.
|
||||
* TODO: Implement
|
||||
*
|
||||
* @see <a href="https://datatracker.ietf.org/doc/html/rfc4880#section-2.1">RFC4880 on Session Keys</a>
|
||||
*
|
||||
* @param sessionKey session key
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions setSessionKey(@Nonnull byte[] sessionKey) {
|
||||
this.sessionKey = sessionKey;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for message decryption.
|
||||
* The key is expected to be unencrypted.
|
||||
*
|
||||
* @param key unencrypted key
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key) {
|
||||
return addDecryptionKey(key, SecretKeyRingProtector.unprotectedKeys());
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a key for message decryption. If the key is encrypted, the {@link SecretKeyRingProtector} is used to decrypt it
|
||||
* when needed.
|
||||
*
|
||||
* @param key key
|
||||
* @param keyRingProtector protector for the secret key
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addDecryptionKey(@Nonnull PGPSecretKeyRing key, @Nonnull SecretKeyRingProtector keyRingProtector) {
|
||||
decryptionKeys.put(key, keyRingProtector);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a passphrase for message decryption.
|
||||
*
|
||||
* @param passphrase passphrase
|
||||
* @return options
|
||||
*/
|
||||
public ConsumerOptions addDecryptionPassphrase(@Nonnull Passphrase passphrase) {
|
||||
decryptionPassphrases.add(passphrase);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Set<PGPSecretKeyRing> getDecryptionKeys() {
|
||||
return Collections.unmodifiableSet(decryptionKeys.keySet());
|
||||
}
|
||||
|
||||
public Set<Passphrase> getDecryptionPassphrases() {
|
||||
return Collections.unmodifiableSet(decryptionPassphrases);
|
||||
}
|
||||
|
||||
public Set<PGPPublicKeyRing> getCertificates() {
|
||||
return Collections.unmodifiableSet(certificates);
|
||||
}
|
||||
|
||||
public MissingPublicKeyCallback getMissingCertificateCallback() {
|
||||
return missingCertificateCallback;
|
||||
}
|
||||
|
||||
public SecretKeyRingProtector getSecretKeyProtector(PGPSecretKeyRing decryptionKeyRing) {
|
||||
return decryptionKeys.get(decryptionKeyRing);
|
||||
}
|
||||
|
||||
public Set<PGPSignature> getDetachedSignatures() {
|
||||
return Collections.unmodifiableSet(detachedSignatures);
|
||||
}
|
||||
}
|
|
@ -63,6 +63,15 @@ public class DecryptionBuilder implements DecryptionBuilderInterface {
|
|||
|
||||
class DecryptWithImpl implements DecryptWith {
|
||||
|
||||
@Override
|
||||
public DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException {
|
||||
if (consumerOptions == null) {
|
||||
throw new IllegalArgumentException("Consumer options cannot be null.");
|
||||
}
|
||||
|
||||
return DecryptionStreamFactory.create(inputStream, consumerOptions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Verify decryptWith(@Nonnull SecretKeyRingProtector decryptor, @Nonnull PGPSecretKeyRingCollection secretKeyRings) {
|
||||
DecryptionBuilder.this.decryptionKeys = secretKeyRings;
|
||||
|
@ -219,8 +228,27 @@ public class DecryptionBuilder implements DecryptionBuilderInterface {
|
|||
|
||||
@Override
|
||||
public DecryptionStream build() throws IOException, PGPException {
|
||||
return DecryptionStreamFactory.create(inputStream, decryptionKeys, decryptionKeyDecryptor,
|
||||
decryptionPassphrase, detachedSignatures, verificationKeys, missingPublicKeyCallback);
|
||||
ConsumerOptions options = new ConsumerOptions();
|
||||
|
||||
for (PGPSecretKeyRing decryptionKey : (decryptionKeys != null ? decryptionKeys : Collections.<PGPSecretKeyRing>emptyList())) {
|
||||
options.addDecryptionKey(decryptionKey, decryptionKeyDecryptor);
|
||||
}
|
||||
|
||||
for (PGPPublicKeyRing certificate : (verificationKeys != null ? verificationKeys : Collections.<PGPPublicKeyRing>emptyList())) {
|
||||
options.addVerificationCert(certificate);
|
||||
}
|
||||
|
||||
for (PGPSignature detachedSignature : (detachedSignatures != null ? detachedSignatures : Collections.<PGPSignature>emptyList())) {
|
||||
options.addVerificationOfDetachedSignature(detachedSignature);
|
||||
}
|
||||
|
||||
options.setMissingCertificateCallback(missingPublicKeyCallback);
|
||||
|
||||
if (decryptionPassphrase != null) {
|
||||
options.addDecryptionPassphrase(decryptionPassphrase);
|
||||
}
|
||||
|
||||
return DecryptionStreamFactory.create(inputStream, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -46,6 +46,8 @@ public interface DecryptionBuilderInterface {
|
|||
|
||||
interface DecryptWith {
|
||||
|
||||
DecryptionStream withOptions(ConsumerOptions consumerOptions) throws PGPException, IOException;
|
||||
|
||||
/**
|
||||
* Decrypt the encrypted data using the secret keys found in the provided {@link PGPSecretKeyRingCollection}.
|
||||
* Here it is assumed that the secret keys are not password protected.
|
||||
|
|
|
@ -54,6 +54,7 @@ import org.bouncycastle.openpgp.operator.PGPContentVerifierBuilderProvider;
|
|||
import org.bouncycastle.openpgp.operator.PublicKeyDataDecryptorFactory;
|
||||
import org.pgpainless.PGPainless;
|
||||
import org.pgpainless.algorithm.CompressionAlgorithm;
|
||||
import org.pgpainless.algorithm.EncryptionPurpose;
|
||||
import org.pgpainless.algorithm.StreamEncoding;
|
||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm;
|
||||
import org.pgpainless.exception.MessageNotIntegrityProtectedException;
|
||||
|
@ -62,6 +63,7 @@ import org.pgpainless.exception.UnacceptableAlgorithmException;
|
|||
import org.pgpainless.implementation.ImplementationFactory;
|
||||
import org.pgpainless.key.OpenPgpV4Fingerprint;
|
||||
import org.pgpainless.key.SubkeyIdentifier;
|
||||
import org.pgpainless.key.info.KeyRingInfo;
|
||||
import org.pgpainless.key.protection.SecretKeyRingProtector;
|
||||
import org.pgpainless.key.protection.UnlockSecretKey;
|
||||
import org.pgpainless.signature.DetachedSignature;
|
||||
|
@ -75,11 +77,7 @@ public final class DecryptionStreamFactory {
|
|||
private static final Level LEVEL = Level.FINE;
|
||||
private static final int MAX_RECURSION_DEPTH = 16;
|
||||
|
||||
private final PGPSecretKeyRingCollection decryptionKeys;
|
||||
private final SecretKeyRingProtector decryptionKeyDecryptor;
|
||||
private final Passphrase decryptionPassphrase;
|
||||
private final Set<PGPPublicKeyRing> verificationKeys = new HashSet<>();
|
||||
private final MissingPublicKeyCallback missingPublicKeyCallback;
|
||||
private final ConsumerOptions options;
|
||||
|
||||
private final OpenPgpMetadata.Builder resultBuilder = OpenPgpMetadata.getBuilder();
|
||||
private static final PGPContentVerifierBuilderProvider verifierBuilderProvider = ImplementationFactory.getInstance().getPGPContentVerifierBuilderProvider();
|
||||
|
@ -87,33 +85,16 @@ public final class DecryptionStreamFactory {
|
|||
private final Map<OpenPgpV4Fingerprint, OnePassSignature> verifiableOnePassSignatures = new HashMap<>();
|
||||
private final List<IntegrityProtectedInputStream> integrityProtectedStreams = new ArrayList<>();
|
||||
|
||||
private DecryptionStreamFactory(@Nullable PGPSecretKeyRingCollection decryptionKeys,
|
||||
@Nullable SecretKeyRingProtector decryptor,
|
||||
@Nullable Passphrase decryptionPassphrase,
|
||||
@Nullable Set<PGPPublicKeyRing> verificationKeys,
|
||||
@Nullable MissingPublicKeyCallback missingPublicKeyCallback) {
|
||||
this.decryptionKeys = decryptionKeys;
|
||||
this.decryptionKeyDecryptor = decryptor;
|
||||
this.decryptionPassphrase = decryptionPassphrase;
|
||||
this.verificationKeys.addAll(verificationKeys != null ? verificationKeys : Collections.emptyList());
|
||||
this.missingPublicKeyCallback = missingPublicKeyCallback;
|
||||
public DecryptionStreamFactory(ConsumerOptions options) {
|
||||
this.options = options;
|
||||
}
|
||||
|
||||
public static DecryptionStream create(@Nonnull InputStream inputStream,
|
||||
@Nullable PGPSecretKeyRingCollection decryptionKeys,
|
||||
@Nullable SecretKeyRingProtector decryptor,
|
||||
@Nullable Passphrase decryptionPassphrase,
|
||||
@Nullable List<PGPSignature> detachedSignatures,
|
||||
@Nullable Set<PGPPublicKeyRing> verificationKeys,
|
||||
@Nullable MissingPublicKeyCallback missingPublicKeyCallback)
|
||||
throws IOException, PGPException {
|
||||
InputStream pgpInputStream;
|
||||
DecryptionStreamFactory factory = new DecryptionStreamFactory(decryptionKeys, decryptor,
|
||||
decryptionPassphrase, verificationKeys, missingPublicKeyCallback);
|
||||
@Nonnull ConsumerOptions options) throws PGPException, IOException {
|
||||
InputStream pgpInputStream = inputStream;
|
||||
DecryptionStreamFactory factory = new DecryptionStreamFactory(options);
|
||||
|
||||
if (detachedSignatures != null) {
|
||||
pgpInputStream = inputStream;
|
||||
for (PGPSignature signature : detachedSignatures) {
|
||||
for (PGPSignature signature : options.getDetachedSignatures()) {
|
||||
PGPPublicKeyRing signingKeyRing = factory.findSignatureVerificationKeyRing(signature.getKeyID());
|
||||
if (signingKeyRing == null) {
|
||||
continue;
|
||||
|
@ -123,11 +104,11 @@ public final class DecryptionStreamFactory {
|
|||
factory.resultBuilder.addDetachedSignature(
|
||||
new DetachedSignature(signature, signingKeyRing, new SubkeyIdentifier(signingKeyRing, signature.getKeyID())));
|
||||
}
|
||||
} else {
|
||||
|
||||
PGPObjectFactory objectFactory = new PGPObjectFactory(
|
||||
PGPUtil.getDecoderStream(inputStream), keyFingerprintCalculator);
|
||||
pgpInputStream = factory.processPGPPackets(objectFactory, 1);
|
||||
}
|
||||
|
||||
return new DecryptionStream(pgpInputStream, factory.resultBuilder, factory.integrityProtectedStreams);
|
||||
}
|
||||
|
||||
|
@ -210,50 +191,68 @@ public final class DecryptionStreamFactory {
|
|||
while (encryptedDataIterator.hasNext()) {
|
||||
PGPEncryptedData encryptedData = encryptedDataIterator.next();
|
||||
|
||||
// TODO: Can we just skip non-integrity-protected packages?
|
||||
if (!encryptedData.isIntegrityProtected()) {
|
||||
throw new MessageNotIntegrityProtectedException();
|
||||
}
|
||||
|
||||
// Data is passphrase encrypted
|
||||
if (encryptedData instanceof PGPPBEEncryptedData) {
|
||||
PGPPBEEncryptedData pbeEncryptedData = (PGPPBEEncryptedData) encryptedData;
|
||||
if (decryptionPassphrase != null) {
|
||||
for (Passphrase passphrase : options.getDecryptionPassphrases()) {
|
||||
PBEDataDecryptorFactory passphraseDecryptor = ImplementationFactory.getInstance()
|
||||
.getPBEDataDecryptorFactory(decryptionPassphrase);
|
||||
.getPBEDataDecryptorFactory(passphrase);
|
||||
try {
|
||||
InputStream decryptedDataStream = pbeEncryptedData.getDataStream(passphraseDecryptor);
|
||||
|
||||
SymmetricKeyAlgorithm symmetricKeyAlgorithm = SymmetricKeyAlgorithm.fromId(
|
||||
pbeEncryptedData.getSymmetricAlgorithm(passphraseDecryptor));
|
||||
throwIfAlgorithmIsRejected(symmetricKeyAlgorithm);
|
||||
resultBuilder.setSymmetricKeyAlgorithm(symmetricKeyAlgorithm);
|
||||
|
||||
try {
|
||||
return pbeEncryptedData.getDataStream(passphraseDecryptor);
|
||||
return decryptedDataStream;
|
||||
} catch (PGPException e) {
|
||||
LOGGER.log(LEVEL, "Probable passphrase mismatch, skip PBE encrypted data block", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// data is public key encrypted
|
||||
else if (encryptedData instanceof PGPPublicKeyEncryptedData) {
|
||||
if (options.getDecryptionKeys().isEmpty()) {
|
||||
|
||||
}
|
||||
PGPPublicKeyEncryptedData publicKeyEncryptedData = (PGPPublicKeyEncryptedData) encryptedData;
|
||||
long keyId = publicKeyEncryptedData.getKeyID();
|
||||
if (decryptionKeys != null) {
|
||||
if (!options.getDecryptionKeys().isEmpty()) {
|
||||
// Known key id
|
||||
if (keyId != 0) {
|
||||
LOGGER.log(LEVEL, "PGPEncryptedData is encrypted for key " + Long.toHexString(keyId));
|
||||
resultBuilder.addRecipientKeyId(keyId);
|
||||
PGPSecretKey secretKey = decryptionKeys.getSecretKey(keyId);
|
||||
if (secretKey != null) {
|
||||
PGPSecretKeyRing decryptionKeyRing = findDecryptionKeyRing(keyId);
|
||||
if (decryptionKeyRing != null) {
|
||||
PGPSecretKey secretKey = decryptionKeyRing.getSecretKey(keyId);
|
||||
LOGGER.log(LEVEL, "Found respective secret key " + Long.toHexString(keyId));
|
||||
// Watch out! This assignment is possibly done multiple times.
|
||||
encryptedSessionKey = publicKeyEncryptedData;
|
||||
decryptionKey = UnlockSecretKey.unlockSecretKey(secretKey, decryptionKeyDecryptor);
|
||||
decryptionKey = UnlockSecretKey.unlockSecretKey(secretKey, options.getSecretKeyProtector(decryptionKeyRing));
|
||||
resultBuilder.setDecryptionFingerprint(new OpenPgpV4Fingerprint(secretKey));
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
// Hidden recipient
|
||||
else {
|
||||
LOGGER.log(LEVEL, "Hidden recipient detected. Try to decrypt with all available secret keys.");
|
||||
outerloop: for (PGPSecretKeyRing ring : decryptionKeys) {
|
||||
for (PGPSecretKey key : ring) {
|
||||
PGPPrivateKey privateKey = key.extractPrivateKey(decryptionKeyDecryptor.getDecryptor(key.getKeyID()));
|
||||
outerloop: for (PGPSecretKeyRing ring : options.getDecryptionKeys()) {
|
||||
KeyRingInfo info = new KeyRingInfo(ring);
|
||||
List<PGPPublicKey> encryptionSubkeys = info.getEncryptionSubkeys(EncryptionPurpose.STORAGE_AND_COMMUNICATIONS);
|
||||
for (PGPPublicKey pubkey : encryptionSubkeys) {
|
||||
PGPSecretKey key = ring.getSecretKey(pubkey.getKeyID());
|
||||
if (key == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
PGPPrivateKey privateKey = UnlockSecretKey.unlockSecretKey(key, options.getSecretKeyProtector(ring).getDecryptor(key.getKeyID()));
|
||||
PublicKeyDataDecryptorFactory decryptorFactory = ImplementationFactory.getInstance().getPublicKeyDataDecryptorFactory(privateKey);
|
||||
try {
|
||||
publicKeyEncryptedData.getSymmetricAlgorithm(decryptorFactory); // will only succeed if we have the right secret key
|
||||
|
@ -271,7 +270,11 @@ public final class DecryptionStreamFactory {
|
|||
}
|
||||
}
|
||||
}
|
||||
return decryptWith(encryptedSessionKey, decryptionKey);
|
||||
}
|
||||
|
||||
private InputStream decryptWith(PGPPublicKeyEncryptedData encryptedSessionKey, PGPPrivateKey decryptionKey)
|
||||
throws PGPException {
|
||||
if (decryptionKey == null) {
|
||||
throw new MissingDecryptionMethodException("Decryption failed - No suitable decryption key or passphrase found");
|
||||
}
|
||||
|
@ -339,9 +342,18 @@ public final class DecryptionStreamFactory {
|
|||
verifiableOnePassSignatures.put(fingerprint, onePassSignature);
|
||||
}
|
||||
|
||||
private PGPSecretKeyRing findDecryptionKeyRing(long keyId) {
|
||||
for (PGPSecretKeyRing key : options.getDecryptionKeys()) {
|
||||
if (key.getSecretKey(keyId) != null) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private PGPPublicKeyRing findSignatureVerificationKeyRing(long keyId) {
|
||||
PGPPublicKeyRing verificationKeyRing = null;
|
||||
for (PGPPublicKeyRing publicKeyRing : verificationKeys) {
|
||||
for (PGPPublicKeyRing publicKeyRing : options.getCertificates()) {
|
||||
PGPPublicKey verificationKey = publicKeyRing.getPublicKey(keyId);
|
||||
if (verificationKey != null) {
|
||||
LOGGER.log(LEVEL, "Found public key " + Long.toHexString(keyId) + " for signature verification");
|
||||
|
@ -350,8 +362,8 @@ public final class DecryptionStreamFactory {
|
|||
}
|
||||
}
|
||||
|
||||
if (verificationKeyRing == null && missingPublicKeyCallback != null) {
|
||||
verificationKeyRing = missingPublicKeyCallback.onMissingPublicKeyEncountered(keyId);
|
||||
if (verificationKeyRing == null && options.getMissingCertificateCallback() != null) {
|
||||
verificationKeyRing = options.getMissingCertificateCallback().onMissingPublicKeyEncountered(keyId);
|
||||
}
|
||||
|
||||
return verificationKeyRing;
|
||||
|
|
|
@ -109,7 +109,7 @@ public class SymmetricEncryptionTest {
|
|||
|
||||
@ParameterizedTest
|
||||
@MethodSource("org.pgpainless.util.TestUtil#provideImplementationFactories")
|
||||
public void testMissmatchPassphraseFails(ImplementationFactory implementationFactory) throws IOException, PGPException {
|
||||
public void testMismatchPassphraseFails(ImplementationFactory implementationFactory) throws IOException, PGPException {
|
||||
ImplementationFactory.setFactoryImplementation(implementationFactory);
|
||||
|
||||
byte[] bytes = new byte[5000];
|
||||
|
|
Loading…
Reference in a new issue