mirror of
https://github.com/minio/minio.git
synced 2024-12-24 22:25:54 -05:00
remove the unused code for decrypting io.Writer
(#8277)
This commit removes unused code for decrypting `io.Writer` since the actual implementation only decrypts `io.Reader`
This commit is contained in:
parent
a9d724120f
commit
b823d6d7bd
@ -32,7 +32,6 @@ import (
|
|||||||
"github.com/minio/minio-go/v6/pkg/encrypt"
|
"github.com/minio/minio-go/v6/pkg/encrypt"
|
||||||
"github.com/minio/minio/cmd/crypto"
|
"github.com/minio/minio/cmd/crypto"
|
||||||
"github.com/minio/minio/cmd/logger"
|
"github.com/minio/minio/cmd/logger"
|
||||||
"github.com/minio/minio/pkg/ioutil"
|
|
||||||
sha256 "github.com/minio/sha256-simd"
|
sha256 "github.com/minio/sha256-simd"
|
||||||
"github.com/minio/sio"
|
"github.com/minio/sio"
|
||||||
)
|
)
|
||||||
@ -64,17 +63,6 @@ const (
|
|||||||
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
// SSESealAlgorithmDareSha256 specifies DARE as authenticated en/decryption scheme and SHA256 as cryptographic
|
|
||||||
// hash function. The key derivation of DARE-SHA256 is not optimal and does not include the object path.
|
|
||||||
// It is considered legacy and should not be used anymore.
|
|
||||||
SSESealAlgorithmDareSha256 = "DARE-SHA256"
|
|
||||||
|
|
||||||
// SSESealAlgorithmDareV2HmacSha256 specifies DAREv2 as authenticated en/decryption scheme and SHA256 as cryptographic
|
|
||||||
// hash function for the HMAC PRF.
|
|
||||||
SSESealAlgorithmDareV2HmacSha256 = "DAREv2-HMAC-SHA256"
|
|
||||||
)
|
|
||||||
|
|
||||||
// hasServerSideEncryptionHeader returns true if the given HTTP header
|
// hasServerSideEncryptionHeader returns true if the given HTTP header
|
||||||
// contains server-side-encryption.
|
// contains server-side-encryption.
|
||||||
func hasServerSideEncryptionHeader(header http.Header) bool {
|
func hasServerSideEncryptionHeader(header http.Header) bool {
|
||||||
@ -597,236 +585,6 @@ func (d *DecryptBlocksReader) Read(p []byte) (int, error) {
|
|||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecryptBlocksWriter - decrypts multipart parts, while implementing
|
|
||||||
// a io.Writer compatible interface.
|
|
||||||
type DecryptBlocksWriter struct {
|
|
||||||
// Original writer where the plain data will be written
|
|
||||||
writer io.Writer
|
|
||||||
// Current decrypter for the current encrypted data block
|
|
||||||
decrypter io.WriteCloser
|
|
||||||
// Start sequence number
|
|
||||||
startSeqNum uint32
|
|
||||||
// Current part index
|
|
||||||
partIndex int
|
|
||||||
// Parts information
|
|
||||||
parts []ObjectPartInfo
|
|
||||||
req *http.Request
|
|
||||||
bucket, object string
|
|
||||||
metadata map[string]string
|
|
||||||
|
|
||||||
partEncRelOffset int64
|
|
||||||
|
|
||||||
copySource bool
|
|
||||||
// Customer Key
|
|
||||||
customerKeyHeader string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *DecryptBlocksWriter) buildDecrypter(partID int) error {
|
|
||||||
m := make(map[string]string)
|
|
||||||
for k, v := range w.metadata {
|
|
||||||
m[k] = v
|
|
||||||
}
|
|
||||||
// Initialize the first decrypter, new decrypters will be initialized in Write() operation as needed.
|
|
||||||
var key []byte
|
|
||||||
var err error
|
|
||||||
if w.copySource {
|
|
||||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
|
||||||
w.req.Header.Set(crypto.SSECopyKey, w.customerKeyHeader)
|
|
||||||
key, err = ParseSSECopyCustomerRequest(w.req.Header, w.metadata)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if crypto.SSEC.IsEncrypted(w.metadata) {
|
|
||||||
w.req.Header.Set(crypto.SSECKey, w.customerKeyHeader)
|
|
||||||
key, err = ParseSSECustomerRequest(w.req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
objectEncryptionKey, err := decryptObjectInfo(key, w.bucket, w.object, m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
var partIDbin [4]byte
|
|
||||||
binary.LittleEndian.PutUint32(partIDbin[:], uint32(partID)) // marshal part ID
|
|
||||||
|
|
||||||
mac := hmac.New(sha256.New, objectEncryptionKey) // derive part encryption key from part ID and object key
|
|
||||||
mac.Write(partIDbin[:])
|
|
||||||
partEncryptionKey := mac.Sum(nil)
|
|
||||||
|
|
||||||
// make sure to provide a NopCloser such that a Close
|
|
||||||
// on sio.decryptWriter doesn't close the underlying writer's
|
|
||||||
// close which perhaps can close the stream prematurely.
|
|
||||||
decrypter, err := newDecryptWriterWithObjectKey(ioutil.NopCloser(w.writer), partEncryptionKey, w.startSeqNum, m)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if w.decrypter != nil {
|
|
||||||
// Pro-actively close the writer such that any pending buffers
|
|
||||||
// are flushed already before we allocate a new decrypter.
|
|
||||||
err = w.decrypter.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.decrypter = decrypter
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *DecryptBlocksWriter) Write(p []byte) (int, error) {
|
|
||||||
var err error
|
|
||||||
var n1 int
|
|
||||||
if int64(len(p)) < w.parts[w.partIndex].Size-w.partEncRelOffset {
|
|
||||||
n1, err = w.decrypter.Write(p)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
w.partEncRelOffset += int64(n1)
|
|
||||||
} else {
|
|
||||||
n1, err = w.decrypter.Write(p[:w.parts[w.partIndex].Size-w.partEncRelOffset])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should now proceed to next part, reset all values appropriately.
|
|
||||||
w.partEncRelOffset = 0
|
|
||||||
w.startSeqNum = 0
|
|
||||||
|
|
||||||
w.partIndex++
|
|
||||||
|
|
||||||
err = w.buildDecrypter(w.partIndex + 1)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
n1, err = w.decrypter.Write(p[n1:])
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
w.partEncRelOffset += int64(n1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return len(p), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close closes the LimitWriter. It behaves like io.Closer.
|
|
||||||
func (w *DecryptBlocksWriter) Close() error {
|
|
||||||
if w.decrypter != nil {
|
|
||||||
err := w.decrypter.Close()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if closer, ok := w.writer.(io.Closer); ok {
|
|
||||||
return closer.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptAllBlocksCopyRequest - setup a struct which can decrypt many concatenated encrypted data
|
|
||||||
// parts information helps to know the boundaries of each encrypted data block, this function decrypts
|
|
||||||
// all parts starting from part-1.
|
|
||||||
func DecryptAllBlocksCopyRequest(client io.Writer, r *http.Request, bucket, object string, objInfo ObjectInfo) (io.WriteCloser, int64, error) {
|
|
||||||
w, _, size, err := DecryptBlocksRequest(client, r, bucket, object, 0, objInfo.Size, objInfo, true)
|
|
||||||
return w, size, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// DecryptBlocksRequest - setup a struct which can decrypt many concatenated encrypted data
|
|
||||||
// parts information helps to know the boundaries of each encrypted data block.
|
|
||||||
func DecryptBlocksRequest(client io.Writer, r *http.Request, bucket, object string, startOffset, length int64, objInfo ObjectInfo, copySource bool) (io.WriteCloser, int64, int64, error) {
|
|
||||||
var seqNumber uint32
|
|
||||||
var encStartOffset, encLength int64
|
|
||||||
|
|
||||||
if !isEncryptedMultipart(objInfo) {
|
|
||||||
seqNumber, encStartOffset, encLength = getEncryptedSinglePartOffsetLength(startOffset, length, objInfo)
|
|
||||||
|
|
||||||
var writer io.WriteCloser
|
|
||||||
var err error
|
|
||||||
if copySource {
|
|
||||||
writer, err = DecryptCopyRequest(client, r, bucket, object, objInfo.UserDefined)
|
|
||||||
} else {
|
|
||||||
writer, err = DecryptRequestWithSequenceNumber(client, r, bucket, object, seqNumber, objInfo.UserDefined)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
return writer, encStartOffset, encLength, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
_, encStartOffset, encLength = getEncryptedMultipartsOffsetLength(startOffset, length, objInfo)
|
|
||||||
|
|
||||||
var partStartIndex int
|
|
||||||
var partStartOffset = startOffset
|
|
||||||
// Skip parts until final offset maps to a particular part offset.
|
|
||||||
for i, part := range objInfo.Parts {
|
|
||||||
decryptedSize, err := sio.DecryptedSize(uint64(part.Size))
|
|
||||||
if err != nil {
|
|
||||||
return nil, -1, -1, errObjectTampered
|
|
||||||
}
|
|
||||||
|
|
||||||
partStartIndex = i
|
|
||||||
|
|
||||||
// Offset is smaller than size we have reached the
|
|
||||||
// proper part offset, break out we start from
|
|
||||||
// this part index.
|
|
||||||
if partStartOffset < int64(decryptedSize) {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Continue to look for next part.
|
|
||||||
partStartOffset -= int64(decryptedSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
startSeqNum := partStartOffset / SSEDAREPackageBlockSize
|
|
||||||
partEncRelOffset := int64(startSeqNum) * (SSEDAREPackageBlockSize + SSEDAREPackageMetaSize)
|
|
||||||
|
|
||||||
w := &DecryptBlocksWriter{
|
|
||||||
writer: client,
|
|
||||||
startSeqNum: uint32(startSeqNum),
|
|
||||||
partEncRelOffset: partEncRelOffset,
|
|
||||||
parts: objInfo.Parts,
|
|
||||||
partIndex: partStartIndex,
|
|
||||||
req: r,
|
|
||||||
bucket: bucket,
|
|
||||||
object: object,
|
|
||||||
customerKeyHeader: r.Header.Get(crypto.SSECKey),
|
|
||||||
copySource: copySource,
|
|
||||||
}
|
|
||||||
|
|
||||||
w.metadata = map[string]string{}
|
|
||||||
// Copy encryption metadata for internal use.
|
|
||||||
for k, v := range objInfo.UserDefined {
|
|
||||||
w.metadata[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
// Purge all the encryption headers.
|
|
||||||
delete(objInfo.UserDefined, crypto.SSEIV)
|
|
||||||
delete(objInfo.UserDefined, crypto.SSESealAlgorithm)
|
|
||||||
delete(objInfo.UserDefined, crypto.SSECSealedKey)
|
|
||||||
delete(objInfo.UserDefined, crypto.SSEMultipart)
|
|
||||||
|
|
||||||
if crypto.S3.IsEncrypted(objInfo.UserDefined) {
|
|
||||||
delete(objInfo.UserDefined, crypto.S3SealedKey)
|
|
||||||
delete(objInfo.UserDefined, crypto.S3KMSKeyID)
|
|
||||||
delete(objInfo.UserDefined, crypto.S3KMSSealedKey)
|
|
||||||
}
|
|
||||||
if w.copySource {
|
|
||||||
w.customerKeyHeader = r.Header.Get(crypto.SSECopyKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.buildDecrypter(w.parts[w.partIndex].Number); err != nil {
|
|
||||||
return nil, 0, 0, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, encStartOffset, encLength, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
// getEncryptedMultipartsOffsetLength - fetch sequence number, encrypted start offset and encrypted length.
|
||||||
func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (uint32, int64, int64) {
|
func getEncryptedMultipartsOffsetLength(offset, length int64, obj ObjectInfo) (uint32, int64, int64) {
|
||||||
// Calculate encrypted offset of a multipart object
|
// Calculate encrypted offset of a multipart object
|
||||||
|
@ -173,7 +173,7 @@ var decryptRequestTests = []struct {
|
|||||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||||
crypto.SSEIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
crypto.SSEIV: "7nQqotA8xgrPx6QK7Ap3GCfjKitqJSrGP7xzgErSJlw=",
|
||||||
crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
crypto.SSECSealedKey: "EAAfAAAAAAD7v1hQq3PFRUHsItalxmrJqrOq6FwnbXNarxOOpb8jTWONPPKyM3Gfjkjyj6NCf+aB/VpHCLCTBA==",
|
||||||
},
|
},
|
||||||
@ -188,7 +188,7 @@ var decryptRequestTests = []struct {
|
|||||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
crypto.SSESealAlgorithm: crypto.SealAlgorithm,
|
||||||
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||||
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||||
},
|
},
|
||||||
@ -218,7 +218,7 @@ var decryptRequestTests = []struct {
|
|||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||||
crypto.SSEIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
crypto.SSEIV: "RrJsEsyPb1UuFNezv1bl9hxuYsgUVC/MUctE2k=",
|
||||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hcs4rR9z/CUuPqu5N4=",
|
||||||
},
|
},
|
||||||
@ -233,7 +233,7 @@ var decryptRequestTests = []struct {
|
|||||||
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
crypto.SSECKeyMD5: "bY4wkxQejw9mUJfo72k53A==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||||
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
crypto.SSEIV: "XAm0dRrJsEsyPb1UuFNezv1bl9ehxuYsgUVC/MUctE2k=",
|
||||||
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
crypto.SSECSealedKey: "SY5E9AvI2tI7/nUrUAssIGE32Hds4rR9z/CUuPqu5N4=",
|
||||||
},
|
},
|
||||||
@ -248,7 +248,7 @@ var decryptRequestTests = []struct {
|
|||||||
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
crypto.SSECKeyMD5: "7PpPLAK26ONlVUGOWlusfg==",
|
||||||
},
|
},
|
||||||
metadata: map[string]string{
|
metadata: map[string]string{
|
||||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareV2HmacSha256,
|
crypto.SSESealAlgorithm: crypto.SealAlgorithm,
|
||||||
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
crypto.SSEIV: "qEqmsONcorqlcZXJxaw32H04eyXyXwUgjHzlhkaIYrU=",
|
||||||
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
crypto.SSECSealedKey: "IAAfAIM14ugTGcM/dIrn4iQMrkl1sjKyeBQ8FBEvRebYj8vWvxG+0cJRpC6NXRU1wJN50JaUOATjO7kz0wZ2mA==",
|
||||||
},
|
},
|
||||||
@ -298,12 +298,12 @@ var decryptObjectInfoTests = []struct {
|
|||||||
expErr: nil,
|
expErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 100, UserDefined: map[string]string{crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: nil,
|
expErr: nil,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 0, UserDefined: map[string]string{crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: nil,
|
expErr: nil,
|
||||||
},
|
},
|
||||||
@ -318,7 +318,7 @@ var decryptObjectInfoTests = []struct {
|
|||||||
expErr: errInvalidEncryptionParameters,
|
expErr: errInvalidEncryptionParameters,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256}},
|
info: ObjectInfo{Size: 31, UserDefined: map[string]string{crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm}},
|
||||||
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
headers: http.Header{crypto.SSECAlgorithm: []string{crypto.SSEAlgorithmAES256}},
|
||||||
expErr: errObjectTampered,
|
expErr: errObjectTampered,
|
||||||
},
|
},
|
||||||
@ -408,7 +408,7 @@ func TestGetDecryptedRange(t *testing.T) {
|
|||||||
}
|
}
|
||||||
udMap = func(isMulti bool) map[string]string {
|
udMap = func(isMulti bool) map[string]string {
|
||||||
m := map[string]string{
|
m := map[string]string{
|
||||||
crypto.SSESealAlgorithm: SSESealAlgorithmDareSha256,
|
crypto.SSESealAlgorithm: crypto.InsecureSealAlgorithm,
|
||||||
crypto.SSEMultipart: "1",
|
crypto.SSEMultipart: "1",
|
||||||
}
|
}
|
||||||
if !isMulti {
|
if !isMulti {
|
||||||
|
@ -185,7 +185,7 @@ var containsReservedMetadataTests = []struct {
|
|||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: http.Header{crypto.SSESealAlgorithm: []string{SSESealAlgorithmDareSha256}},
|
header: http.Header{crypto.SSESealAlgorithm: []string{crypto.InsecureSealAlgorithm}},
|
||||||
shouldFail: true,
|
shouldFail: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
Loading…
Reference in New Issue
Block a user