mirror of
https://github.com/pgpainless/pgpainless.git
synced 2025-09-09 18:29:39 +02:00
More code cleanup
This commit is contained in:
parent
a0624d8ac1
commit
2ae2389666
6 changed files with 51 additions and 74 deletions
|
@ -190,7 +190,7 @@ class PGPainless(
|
||||||
*
|
*
|
||||||
* @return builder
|
* @return builder
|
||||||
*/
|
*/
|
||||||
@JvmStatic fun encryptAndOrSign() = EncryptionBuilder()
|
@JvmStatic fun encryptAndOrSign() = EncryptionBuilder(getInstance())
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using
|
* Create a [DecryptionBuilder], which can be used to decrypt and/or verify data using
|
||||||
|
|
|
@ -5,65 +5,25 @@
|
||||||
package org.pgpainless.encryption_signing
|
package org.pgpainless.encryption_signing
|
||||||
|
|
||||||
import java.io.OutputStream
|
import java.io.OutputStream
|
||||||
import org.pgpainless.PGPainless.Companion.getPolicy
|
import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
|
||||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
|
||||||
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
|
||||||
import org.pgpainless.util.NullOutputStream
|
import org.pgpainless.util.NullOutputStream
|
||||||
import org.slf4j.Logger
|
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
class EncryptionBuilder : EncryptionBuilderInterface {
|
class EncryptionBuilder(private val api: PGPainless) : EncryptionBuilderInterface {
|
||||||
override fun onOutputStream(
|
override fun onOutputStream(
|
||||||
outputStream: OutputStream
|
outputStream: OutputStream
|
||||||
): EncryptionBuilderInterface.WithOptions {
|
): EncryptionBuilderInterface.WithOptions {
|
||||||
return WithOptionsImpl(outputStream)
|
return WithOptionsImpl(outputStream, api)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
|
override fun discardOutput(): EncryptionBuilderInterface.WithOptions {
|
||||||
return onOutputStream(NullOutputStream())
|
return onOutputStream(NullOutputStream())
|
||||||
}
|
}
|
||||||
|
|
||||||
class WithOptionsImpl(val outputStream: OutputStream) : EncryptionBuilderInterface.WithOptions {
|
class WithOptionsImpl(val outputStream: OutputStream, private val api: PGPainless) :
|
||||||
|
EncryptionBuilderInterface.WithOptions {
|
||||||
|
|
||||||
override fun withOptions(options: ProducerOptions): EncryptionStream {
|
override fun withOptions(options: ProducerOptions): EncryptionStream {
|
||||||
return EncryptionStream(outputStream, options)
|
return EncryptionStream(outputStream, options, api)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
@JvmStatic val LOGGER: Logger = LoggerFactory.getLogger(EncryptionBuilder::class.java)
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Negotiate the [SymmetricKeyAlgorithm] used for message encryption.
|
|
||||||
*
|
|
||||||
* @param encryptionOptions encryption options
|
|
||||||
* @return negotiated symmetric key algorithm
|
|
||||||
*/
|
|
||||||
@JvmStatic
|
|
||||||
fun negotiateSymmetricEncryptionAlgorithm(
|
|
||||||
encryptionOptions: EncryptionOptions
|
|
||||||
): SymmetricKeyAlgorithm {
|
|
||||||
val preferences =
|
|
||||||
encryptionOptions.keyViews.values
|
|
||||||
.map { it.preferredSymmetricKeyAlgorithms }
|
|
||||||
.toList()
|
|
||||||
val algorithm =
|
|
||||||
byPopularity()
|
|
||||||
.negotiate(
|
|
||||||
getPolicy().symmetricKeyEncryptionAlgorithmPolicy,
|
|
||||||
encryptionOptions.encryptionAlgorithmOverride,
|
|
||||||
preferences)
|
|
||||||
LOGGER.debug(
|
|
||||||
"Negotiation resulted in {} being the symmetric encryption algorithm of choice.",
|
|
||||||
algorithm)
|
|
||||||
return algorithm
|
|
||||||
}
|
|
||||||
|
|
||||||
@JvmStatic
|
|
||||||
fun negotiateCompressionAlgorithm(producerOptions: ProducerOptions): CompressionAlgorithm {
|
|
||||||
return producerOptions.compressionAlgorithmOverride
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,11 +10,12 @@ import org.bouncycastle.openpgp.api.OpenPGPCertificate
|
||||||
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
|
import org.bouncycastle.openpgp.api.OpenPGPCertificate.OpenPGPComponentKey
|
||||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
||||||
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator
|
import org.bouncycastle.openpgp.operator.PGPKeyEncryptionMethodGenerator
|
||||||
|
import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.PGPainless.Companion.inspectKeyRing
|
import org.pgpainless.PGPainless.Companion.inspectKeyRing
|
||||||
import org.pgpainless.algorithm.EncryptionPurpose
|
import org.pgpainless.algorithm.EncryptionPurpose
|
||||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||||
|
import org.pgpainless.algorithm.negotiation.SymmetricKeyAlgorithmNegotiator.Companion.byPopularity
|
||||||
import org.pgpainless.authentication.CertificateAuthority
|
import org.pgpainless.authentication.CertificateAuthority
|
||||||
import org.pgpainless.bouncycastle.extensions.toOpenPGPCertificate
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector
|
import org.pgpainless.encryption_signing.EncryptionOptions.EncryptionKeySelector
|
||||||
import org.pgpainless.exception.KeyException.*
|
import org.pgpainless.exception.KeyException.*
|
||||||
import org.pgpainless.key.SubkeyIdentifier
|
import org.pgpainless.key.SubkeyIdentifier
|
||||||
|
@ -22,7 +23,10 @@ import org.pgpainless.key.info.KeyAccessor
|
||||||
import org.pgpainless.key.info.KeyRingInfo
|
import org.pgpainless.key.info.KeyRingInfo
|
||||||
import org.pgpainless.util.Passphrase
|
import org.pgpainless.util.Passphrase
|
||||||
|
|
||||||
class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
class EncryptionOptions(
|
||||||
|
private val purpose: EncryptionPurpose,
|
||||||
|
private val api: PGPainless = PGPainless.getInstance()
|
||||||
|
) {
|
||||||
private val _encryptionMethods: MutableSet<PGPKeyEncryptionMethodGenerator> = mutableSetOf()
|
private val _encryptionMethods: MutableSet<PGPKeyEncryptionMethodGenerator> = mutableSetOf()
|
||||||
private val _encryptionKeys: MutableSet<OpenPGPComponentKey> = mutableSetOf()
|
private val _encryptionKeys: MutableSet<OpenPGPComponentKey> = mutableSetOf()
|
||||||
private val _encryptionKeyIdentifiers: MutableSet<SubkeyIdentifier> = mutableSetOf()
|
private val _encryptionKeyIdentifiers: MutableSet<SubkeyIdentifier> = mutableSetOf()
|
||||||
|
@ -87,7 +91,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
||||||
.lookupByUserId(userId, email, evaluationDate, targetAmount)
|
.lookupByUserId(userId, email, evaluationDate, targetAmount)
|
||||||
.filter { it.isAuthenticated() }
|
.filter { it.isAuthenticated() }
|
||||||
.forEach {
|
.forEach {
|
||||||
addRecipient(it.certificate.toOpenPGPCertificate()).also { foundAcceptable = true }
|
addRecipient(api.toCertificate(it.certificate)).also { foundAcceptable = true }
|
||||||
}
|
}
|
||||||
require(foundAcceptable) {
|
require(foundAcceptable) {
|
||||||
"Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount."
|
"Could not identify any trust-worthy certificates for '$userId' and target trust amount $targetAmount."
|
||||||
|
@ -225,7 +229,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
||||||
key: PGPPublicKeyRing,
|
key: PGPPublicKeyRing,
|
||||||
userId: CharSequence,
|
userId: CharSequence,
|
||||||
encryptionKeySelector: EncryptionKeySelector
|
encryptionKeySelector: EncryptionKeySelector
|
||||||
) = addRecipient(key.toOpenPGPCertificate(), userId, encryptionKeySelector)
|
) = addRecipient(api.toCertificate(key), userId, encryptionKeySelector)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt the message for the given recipients [OpenPGPCertificate], filtering encryption
|
* Encrypt the message for the given recipients [OpenPGPCertificate], filtering encryption
|
||||||
|
@ -251,7 +255,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
||||||
replaceWith =
|
replaceWith =
|
||||||
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
|
ReplaceWith("addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)"))
|
||||||
fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) =
|
fun addRecipient(key: PGPPublicKeyRing, encryptionKeySelector: EncryptionKeySelector) =
|
||||||
addRecipient(key.toOpenPGPCertificate(), encryptionKeySelector)
|
addRecipient(api.toCertificate(key), encryptionKeySelector)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Encrypt the message for the recipients [OpenPGPCertificate], keeping the recipient anonymous
|
* Encrypt the message for the recipients [OpenPGPCertificate], keeping the recipient anonymous
|
||||||
|
@ -282,7 +286,7 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
||||||
fun addHiddenRecipient(
|
fun addHiddenRecipient(
|
||||||
key: PGPPublicKeyRing,
|
key: PGPPublicKeyRing,
|
||||||
selector: EncryptionKeySelector = encryptionKeySelector
|
selector: EncryptionKeySelector = encryptionKeySelector
|
||||||
) = addHiddenRecipient(key.toOpenPGPCertificate(), selector)
|
) = addHiddenRecipient(api.toCertificate(key), selector)
|
||||||
|
|
||||||
private fun addAsRecipient(
|
private fun addAsRecipient(
|
||||||
cert: OpenPGPCertificate,
|
cert: OpenPGPCertificate,
|
||||||
|
@ -406,6 +410,17 @@ class EncryptionOptions(private val purpose: EncryptionPurpose) {
|
||||||
|
|
||||||
fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty()
|
fun hasEncryptionMethod() = _encryptionMethods.isNotEmpty()
|
||||||
|
|
||||||
|
internal fun negotiateSymmetricEncryptionAlgorithm(): SymmetricKeyAlgorithm {
|
||||||
|
val preferences = keyViews.values.map { it.preferredSymmetricKeyAlgorithms }.toList()
|
||||||
|
val algorithm =
|
||||||
|
byPopularity()
|
||||||
|
.negotiate(
|
||||||
|
api.algorithmPolicy.symmetricKeyEncryptionAlgorithmPolicy,
|
||||||
|
encryptionAlgorithmOverride,
|
||||||
|
preferences)
|
||||||
|
return algorithm
|
||||||
|
}
|
||||||
|
|
||||||
fun interface EncryptionKeySelector {
|
fun interface EncryptionKeySelector {
|
||||||
fun selectEncryptionSubkeys(
|
fun selectEncryptionSubkeys(
|
||||||
encryptionCapableKeys: List<OpenPGPComponentKey>
|
encryptionCapableKeys: List<OpenPGPComponentKey>
|
||||||
|
|
|
@ -13,13 +13,12 @@ import org.bouncycastle.openpgp.PGPCompressedDataGenerator
|
||||||
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator
|
import org.bouncycastle.openpgp.PGPEncryptedDataGenerator
|
||||||
import org.bouncycastle.openpgp.PGPException
|
import org.bouncycastle.openpgp.PGPException
|
||||||
import org.bouncycastle.openpgp.PGPLiteralDataGenerator
|
import org.bouncycastle.openpgp.PGPLiteralDataGenerator
|
||||||
import org.bouncycastle.openpgp.api.OpenPGPImplementation
|
import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||||
import org.pgpainless.algorithm.StreamEncoding
|
import org.pgpainless.algorithm.StreamEncoding
|
||||||
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
|
||||||
import org.pgpainless.key.SubkeyIdentifier
|
import org.pgpainless.key.SubkeyIdentifier
|
||||||
import org.pgpainless.util.ArmoredOutputStreamFactory
|
import org.pgpainless.util.ArmoredOutputStreamFactory
|
||||||
import org.slf4j.LoggerFactory
|
|
||||||
|
|
||||||
// 1 << 8 causes wrong partial body length encoding
|
// 1 << 8 causes wrong partial body length encoding
|
||||||
// 1 << 9 fixes this.
|
// 1 << 9 fixes this.
|
||||||
|
@ -38,6 +37,7 @@ const val BUFFER_SIZE = 1 shl 9
|
||||||
class EncryptionStream(
|
class EncryptionStream(
|
||||||
private var outermostStream: OutputStream,
|
private var outermostStream: OutputStream,
|
||||||
private val options: ProducerOptions,
|
private val options: ProducerOptions,
|
||||||
|
private val api: PGPainless
|
||||||
) : OutputStream() {
|
) : OutputStream() {
|
||||||
|
|
||||||
private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder()
|
private val resultBuilder: EncryptionResult.Builder = EncryptionResult.builder()
|
||||||
|
@ -63,12 +63,10 @@ class EncryptionStream(
|
||||||
|
|
||||||
private fun prepareArmor() {
|
private fun prepareArmor() {
|
||||||
if (!options.isAsciiArmor) {
|
if (!options.isAsciiArmor) {
|
||||||
LOGGER.debug("Output will be unarmored.")
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
outermostStream = BufferedOutputStream(outermostStream)
|
outermostStream = BufferedOutputStream(outermostStream)
|
||||||
LOGGER.debug("Wrap encryption output in ASCII armor.")
|
|
||||||
armorOutputStream =
|
armorOutputStream =
|
||||||
ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it }
|
ArmoredOutputStreamFactory.get(outermostStream, options).also { outermostStream = it }
|
||||||
}
|
}
|
||||||
|
@ -84,14 +82,13 @@ class EncryptionStream(
|
||||||
"If EncryptionOptions are provided, at least one encryption method MUST be provided as well."
|
"If EncryptionOptions are provided, at least one encryption method MUST be provided as well."
|
||||||
}
|
}
|
||||||
|
|
||||||
EncryptionBuilder.negotiateSymmetricEncryptionAlgorithm(options.encryptionOptions).let {
|
options.encryptionOptions.negotiateSymmetricEncryptionAlgorithm().let {
|
||||||
resultBuilder.setEncryptionAlgorithm(it)
|
resultBuilder.setEncryptionAlgorithm(it)
|
||||||
LOGGER.debug("Encrypt message using symmetric algorithm $it.")
|
|
||||||
val encryptedDataGenerator =
|
val encryptedDataGenerator =
|
||||||
PGPEncryptedDataGenerator(
|
PGPEncryptedDataGenerator(
|
||||||
OpenPGPImplementation.getInstance()
|
api.implementation.pgpDataEncryptorBuilder(it.algorithmId).apply {
|
||||||
.pgpDataEncryptorBuilder(it.algorithmId)
|
setWithIntegrityPacket(true)
|
||||||
.apply { setWithIntegrityPacket(true) })
|
})
|
||||||
options.encryptionOptions.encryptionMethods.forEach { m ->
|
options.encryptionOptions.encryptionMethods.forEach { m ->
|
||||||
encryptedDataGenerator.addMethod(m)
|
encryptedDataGenerator.addMethod(m)
|
||||||
}
|
}
|
||||||
|
@ -109,12 +106,11 @@ class EncryptionStream(
|
||||||
|
|
||||||
@Throws(IOException::class)
|
@Throws(IOException::class)
|
||||||
private fun prepareCompression() {
|
private fun prepareCompression() {
|
||||||
EncryptionBuilder.negotiateCompressionAlgorithm(options).let {
|
options.negotiateCompressionAlgorithm().let {
|
||||||
resultBuilder.setCompressionAlgorithm(it)
|
resultBuilder.setCompressionAlgorithm(it)
|
||||||
compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId)
|
compressedDataGenerator = PGPCompressedDataGenerator(it.algorithmId)
|
||||||
if (it == CompressionAlgorithm.UNCOMPRESSED) return
|
if (it == CompressionAlgorithm.UNCOMPRESSED) return
|
||||||
|
|
||||||
LOGGER.debug("Compress using $it.")
|
|
||||||
basicCompressionStream =
|
basicCompressionStream =
|
||||||
BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream ->
|
BCPGOutputStream(compressedDataGenerator!!.open(outermostStream)).also { stream ->
|
||||||
outermostStream = stream
|
outermostStream = stream
|
||||||
|
@ -267,8 +263,4 @@ class EncryptionStream(
|
||||||
|
|
||||||
val isClosed
|
val isClosed
|
||||||
get() = closed
|
get() = closed
|
||||||
|
|
||||||
companion object {
|
|
||||||
@JvmStatic private val LOGGER = LoggerFactory.getLogger(EncryptionStream::class.java)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,10 +10,10 @@ import org.pgpainless.PGPainless
|
||||||
import org.pgpainless.algorithm.CompressionAlgorithm
|
import org.pgpainless.algorithm.CompressionAlgorithm
|
||||||
import org.pgpainless.algorithm.StreamEncoding
|
import org.pgpainless.algorithm.StreamEncoding
|
||||||
|
|
||||||
class ProducerOptions
|
class ProducerOptions(
|
||||||
private constructor(
|
|
||||||
val encryptionOptions: EncryptionOptions?,
|
val encryptionOptions: EncryptionOptions?,
|
||||||
val signingOptions: SigningOptions?
|
val signingOptions: SigningOptions?,
|
||||||
|
val api: PGPainless = PGPainless.getInstance()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private var _fileName: String = ""
|
private var _fileName: String = ""
|
||||||
|
@ -25,7 +25,7 @@ private constructor(
|
||||||
var isDisableAsciiArmorCRC = false
|
var isDisableAsciiArmorCRC = false
|
||||||
|
|
||||||
private var _compressionAlgorithmOverride: CompressionAlgorithm =
|
private var _compressionAlgorithmOverride: CompressionAlgorithm =
|
||||||
PGPainless.getPolicy().compressionAlgorithmPolicy.defaultCompressionAlgorithm
|
api.algorithmPolicy.compressionAlgorithmPolicy.defaultCompressionAlgorithm
|
||||||
private var asciiArmor = true
|
private var asciiArmor = true
|
||||||
private var _comment: String? = null
|
private var _comment: String? = null
|
||||||
private var _version: String? = null
|
private var _version: String? = null
|
||||||
|
@ -104,6 +104,13 @@ private constructor(
|
||||||
*/
|
*/
|
||||||
fun hasVersion() = version != null
|
fun hasVersion() = version != null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configure the resulting OpenPGP message to make use of the Cleartext Signature Framework
|
||||||
|
* (CSF). A CSF message MUST be signed using detached signatures only and MUST NOT be encrypted.
|
||||||
|
*
|
||||||
|
* @see
|
||||||
|
* [RFC9580: OpenPGP - Cleartext Signature Framework](https://www.rfc-editor.org/rfc/rfc9580.html#name-cleartext-signature-framewo)
|
||||||
|
*/
|
||||||
fun setCleartextSigned() = apply {
|
fun setCleartextSigned() = apply {
|
||||||
require(signingOptions != null) {
|
require(signingOptions != null) {
|
||||||
"Signing Options cannot be null if cleartext signing is enabled."
|
"Signing Options cannot be null if cleartext signing is enabled."
|
||||||
|
@ -230,6 +237,10 @@ private constructor(
|
||||||
_hideArmorHeaders = hideArmorHeaders
|
_hideArmorHeaders = hideArmorHeaders
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal fun negotiateCompressionAlgorithm(): CompressionAlgorithm {
|
||||||
|
return compressionAlgorithmOverride
|
||||||
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Sign and encrypt some data.
|
* Sign and encrypt some data.
|
||||||
|
|
|
@ -17,7 +17,6 @@ import org.bouncycastle.openpgp.PGPSecretKeyRing;
|
||||||
import org.bouncycastle.util.io.Streams;
|
import org.bouncycastle.util.io.Streams;
|
||||||
import org.junit.jupiter.api.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.pgpainless.PGPainless;
|
import org.pgpainless.PGPainless;
|
||||||
import org.pgpainless.algorithm.EncryptionPurpose;
|
|
||||||
import org.pgpainless.encryption_signing.EncryptionOptions;
|
import org.pgpainless.encryption_signing.EncryptionOptions;
|
||||||
import org.pgpainless.encryption_signing.EncryptionResult;
|
import org.pgpainless.encryption_signing.EncryptionResult;
|
||||||
import org.pgpainless.encryption_signing.EncryptionStream;
|
import org.pgpainless.encryption_signing.EncryptionStream;
|
||||||
|
@ -51,7 +50,7 @@ public class TestTwoSubkeysEncryption {
|
||||||
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
EncryptionStream encryptionStream = PGPainless.encryptAndOrSign()
|
||||||
.onOutputStream(out)
|
.onOutputStream(out)
|
||||||
.withOptions(
|
.withOptions(
|
||||||
ProducerOptions.encrypt(new EncryptionOptions(EncryptionPurpose.ANY)
|
ProducerOptions.encrypt(EncryptionOptions.get()
|
||||||
.addRecipient(publicKeys, EncryptionOptions.encryptToAllCapableSubkeys())
|
.addRecipient(publicKeys, EncryptionOptions.encryptToAllCapableSubkeys())
|
||||||
)
|
)
|
||||||
.setAsciiArmor(false)
|
.setAsciiArmor(false)
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue