Added oid which is a fork of https://github.com/mastahyeti/cms/tree/master/oid; contains the needed OIDs for cryptographic message syntax.
This commit is contained in:
parent
b33189be9c
commit
9179582f90
129
oid/oid.go
Normal file
129
oid/oid.go
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
// Package oid contains OIDs that are used by other packages in this repository.
|
||||||
|
package oid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto"
|
||||||
|
"crypto/x509"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Content type OIDs
|
||||||
|
var (
|
||||||
|
Data = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 1}
|
||||||
|
SignedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 2}
|
||||||
|
EnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 7, 3}
|
||||||
|
AuthEnvelopedData = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 23}
|
||||||
|
TSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}
|
||||||
|
ContentTypeTSTInfo = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 1, 4}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Attribute OIDs
|
||||||
|
var (
|
||||||
|
AttributeContentType = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 3}
|
||||||
|
AttributeMessageDigest = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 4}
|
||||||
|
AttributeSigningTime = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 5}
|
||||||
|
AttributeTimeStampToken = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 2, 14}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Signature Algorithm OIDs
|
||||||
|
var (
|
||||||
|
SignatureAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
|
||||||
|
SignatureAlgorithmECDSA = asn1.ObjectIdentifier{1, 2, 840, 10045, 2, 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Public Key Encryption OIDs
|
||||||
|
var (
|
||||||
|
EncryptionAlgorithmRSA = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 1, 1}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Digest Algorithm OIDs
|
||||||
|
var (
|
||||||
|
DigestAlgorithmSHA1 = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 26}
|
||||||
|
DigestAlgorithmMD5 = asn1.ObjectIdentifier{1, 2, 840, 113549, 2, 5}
|
||||||
|
DigestAlgorithmSHA256 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 1}
|
||||||
|
DigestAlgorithmSHA384 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 2}
|
||||||
|
DigestAlgorithmSHA512 = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 2, 3}
|
||||||
|
)
|
||||||
|
|
||||||
|
// X.509 extensions
|
||||||
|
var (
|
||||||
|
SubjectKeyIdentifier = asn1.ObjectIdentifier{2, 5, 29, 14}
|
||||||
|
)
|
||||||
|
|
||||||
|
// DigestAlgorithmToHash maps digest OIDs to crypto.Hash values.
|
||||||
|
var DigestAlgorithmToHash = map[string]crypto.Hash{
|
||||||
|
DigestAlgorithmSHA1.String(): crypto.SHA1,
|
||||||
|
DigestAlgorithmMD5.String(): crypto.MD5,
|
||||||
|
DigestAlgorithmSHA256.String(): crypto.SHA256,
|
||||||
|
DigestAlgorithmSHA384.String(): crypto.SHA384,
|
||||||
|
DigestAlgorithmSHA512.String(): crypto.SHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
// HashToDigestAlgorithm maps crypto.Hash values to digest OIDs.
|
||||||
|
var HashToDigestAlgorithm = map[crypto.Hash]asn1.ObjectIdentifier{
|
||||||
|
crypto.SHA1: DigestAlgorithmSHA1,
|
||||||
|
crypto.MD5: DigestAlgorithmMD5,
|
||||||
|
crypto.SHA256: DigestAlgorithmSHA256,
|
||||||
|
crypto.SHA384: DigestAlgorithmSHA384,
|
||||||
|
crypto.SHA512: DigestAlgorithmSHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithmToDigestAlgorithm maps x509.SignatureAlgorithm to
|
||||||
|
// digestAlgorithm OIDs.
|
||||||
|
var SignatureAlgorithmToDigestAlgorithm = map[x509.SignatureAlgorithm]asn1.ObjectIdentifier{
|
||||||
|
x509.SHA1WithRSA: DigestAlgorithmSHA1,
|
||||||
|
x509.MD5WithRSA: DigestAlgorithmMD5,
|
||||||
|
x509.SHA256WithRSA: DigestAlgorithmSHA256,
|
||||||
|
x509.SHA384WithRSA: DigestAlgorithmSHA384,
|
||||||
|
x509.SHA512WithRSA: DigestAlgorithmSHA512,
|
||||||
|
x509.ECDSAWithSHA1: DigestAlgorithmSHA1,
|
||||||
|
x509.ECDSAWithSHA256: DigestAlgorithmSHA256,
|
||||||
|
x509.ECDSAWithSHA384: DigestAlgorithmSHA384,
|
||||||
|
x509.ECDSAWithSHA512: DigestAlgorithmSHA512,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithmToSignatureAlgorithm maps x509.SignatureAlgorithm to
|
||||||
|
// signatureAlgorithm OIDs.
|
||||||
|
var SignatureAlgorithmToSignatureAlgorithm = map[x509.SignatureAlgorithm]asn1.ObjectIdentifier{
|
||||||
|
x509.SHA1WithRSA: SignatureAlgorithmRSA,
|
||||||
|
x509.MD5WithRSA: SignatureAlgorithmRSA,
|
||||||
|
x509.SHA256WithRSA: SignatureAlgorithmRSA,
|
||||||
|
x509.SHA384WithRSA: SignatureAlgorithmRSA,
|
||||||
|
x509.SHA512WithRSA: SignatureAlgorithmRSA,
|
||||||
|
x509.ECDSAWithSHA1: SignatureAlgorithmECDSA,
|
||||||
|
x509.ECDSAWithSHA256: SignatureAlgorithmECDSA,
|
||||||
|
x509.ECDSAWithSHA384: SignatureAlgorithmECDSA,
|
||||||
|
x509.ECDSAWithSHA512: SignatureAlgorithmECDSA,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SignatureAlgorithms maps digest and signature OIDs to
|
||||||
|
// x509.SignatureAlgorithm values.
|
||||||
|
var SignatureAlgorithms = map[string]map[string]x509.SignatureAlgorithm{
|
||||||
|
SignatureAlgorithmRSA.String(): map[string]x509.SignatureAlgorithm{
|
||||||
|
DigestAlgorithmSHA1.String(): x509.SHA1WithRSA,
|
||||||
|
DigestAlgorithmMD5.String(): x509.MD5WithRSA,
|
||||||
|
DigestAlgorithmSHA256.String(): x509.SHA256WithRSA,
|
||||||
|
DigestAlgorithmSHA384.String(): x509.SHA384WithRSA,
|
||||||
|
DigestAlgorithmSHA512.String(): x509.SHA512WithRSA,
|
||||||
|
},
|
||||||
|
SignatureAlgorithmECDSA.String(): map[string]x509.SignatureAlgorithm{
|
||||||
|
DigestAlgorithmSHA1.String(): x509.ECDSAWithSHA1,
|
||||||
|
DigestAlgorithmSHA256.String(): x509.ECDSAWithSHA256,
|
||||||
|
DigestAlgorithmSHA384.String(): x509.ECDSAWithSHA384,
|
||||||
|
DigestAlgorithmSHA512.String(): x509.ECDSAWithSHA512,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyAlgorithmToSignatureAlgorithm maps certificate public key
|
||||||
|
// algorithms to CMS signature algorithms.
|
||||||
|
var PublicKeyAlgorithmToSignatureAlgorithm = map[x509.PublicKeyAlgorithm]pkix.AlgorithmIdentifier{
|
||||||
|
x509.RSA: pkix.AlgorithmIdentifier{Algorithm: SignatureAlgorithmRSA},
|
||||||
|
x509.ECDSA: pkix.AlgorithmIdentifier{Algorithm: SignatureAlgorithmECDSA},
|
||||||
|
}
|
||||||
|
|
||||||
|
// PublicKeyAlgorithmToEncrytionAlgorithm maps certificate public key
|
||||||
|
// algorithms to CMS encryption algorithms.
|
||||||
|
var PublicKeyAlgorithmToEncrytionAlgorithm = map[x509.PublicKeyAlgorithm]pkix.AlgorithmIdentifier{
|
||||||
|
x509.RSA: pkix.AlgorithmIdentifier{Algorithm: EncryptionAlgorithmRSA},
|
||||||
|
}
|
260
oid/symmetric_ciphers.go
Normal file
260
oid/symmetric_ciphers.go
Normal file
@ -0,0 +1,260 @@
|
|||||||
|
package oid
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/des"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/x509/pkix"
|
||||||
|
"encoding/asn1"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"golang.org/x/crypto/chacha20poly1305"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EncryptionAlgorithm does the handling of the encrypton and decryption for a given algorithm identifier.
|
||||||
|
type EncryptionAlgorithm struct {
|
||||||
|
EncryptionAlgorithmIdentifier asn1.ObjectIdentifier
|
||||||
|
ContentEncryptionAlgorithmIdentifier pkix.AlgorithmIdentifier
|
||||||
|
Key, IV, MAC []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encryption Algorithm OIDs
|
||||||
|
var (
|
||||||
|
EncryptionAlgorithmDESCBC = asn1.ObjectIdentifier{1, 3, 14, 3, 2, 7}
|
||||||
|
EncryptionAlgorithmDESEDE3CBC = asn1.ObjectIdentifier{1, 2, 840, 113549, 3, 7}
|
||||||
|
EncryptionAlgorithmAES128CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 2}
|
||||||
|
EncryptionAlgorithmAES256CBC = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 42}
|
||||||
|
//AEAD
|
||||||
|
EncryptionAlgorithmAES128GCM = asn1.ObjectIdentifier{2, 16, 840, 1, 101, 3, 4, 1, 6}
|
||||||
|
AEADChaCha20Poly1305 = asn1.ObjectIdentifier{1, 2, 840, 113549, 1, 9, 16, 3, 18}
|
||||||
|
)
|
||||||
|
|
||||||
|
var symmetricKeyLen = map[string]int{
|
||||||
|
EncryptionAlgorithmDESCBC.String(): 7,
|
||||||
|
EncryptionAlgorithmDESEDE3CBC.String(): 21,
|
||||||
|
EncryptionAlgorithmAES128CBC.String(): 16,
|
||||||
|
EncryptionAlgorithmAES256CBC.String(): 32,
|
||||||
|
//AEAD
|
||||||
|
EncryptionAlgorithmAES128GCM.String(): 16,
|
||||||
|
AEADChaCha20Poly1305.String(): 32,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encrypt encrypts the plaintext and returns the ciphertext.
|
||||||
|
func (e *EncryptionAlgorithm) Encrypt(plaintext []byte) (ciphertext []byte, err error) {
|
||||||
|
|
||||||
|
if e.Key == nil {
|
||||||
|
e.Key = make([]byte, symmetricKeyLen[e.EncryptionAlgorithmIdentifier.String()])
|
||||||
|
rand.Read(e.Key)
|
||||||
|
}
|
||||||
|
|
||||||
|
//Choose cipher
|
||||||
|
var blockCipher cipher.Block
|
||||||
|
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128CBC.String(), EncryptionAlgorithmAES256CBC.String(), EncryptionAlgorithmAES128GCM.String():
|
||||||
|
blockCipher, err = aes.NewCipher(e.Key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case AEADChaCha20Poly1305.String():
|
||||||
|
default:
|
||||||
|
err = errors.New("Content encrytion: cipher not supportet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Choose blockmode
|
||||||
|
var blockMode cipher.BlockMode
|
||||||
|
var aead cipher.AEAD
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128CBC.String(), EncryptionAlgorithmAES256CBC.String():
|
||||||
|
if e.IV == nil {
|
||||||
|
e.IV = make([]byte, len(e.Key))
|
||||||
|
rand.Read(e.IV)
|
||||||
|
}
|
||||||
|
|
||||||
|
blockMode = cipher.NewCBCEncrypter(blockCipher, e.IV)
|
||||||
|
e.ContentEncryptionAlgorithmIdentifier = pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: e.EncryptionAlgorithmIdentifier,
|
||||||
|
Parameters: asn1.RawValue{Tag: 4, Bytes: e.IV}}
|
||||||
|
case EncryptionAlgorithmAES128GCM.String():
|
||||||
|
aead, err = cipher.NewGCM(blockCipher)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case AEADChaCha20Poly1305.String():
|
||||||
|
aead, err = chacha20poly1305.New(e.Key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128CBC.String(), EncryptionAlgorithmAES256CBC.String():
|
||||||
|
var plain []byte
|
||||||
|
plain, err = pad(plaintext, blockCipher.BlockSize())
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext = make([]byte, len(plain))
|
||||||
|
|
||||||
|
blockMode.CryptBlocks(ciphertext, plain)
|
||||||
|
|
||||||
|
return
|
||||||
|
case EncryptionAlgorithmAES128GCM.String(), AEADChaCha20Poly1305.String():
|
||||||
|
nonce := make([]byte, nonceSize)
|
||||||
|
_, err = rand.Read(nonce)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ciphertext = aead.Seal(nil, nonce, plaintext, nil)
|
||||||
|
|
||||||
|
e.MAC = ciphertext[len(ciphertext)-aead.Overhead():]
|
||||||
|
ciphertext = ciphertext[:len(ciphertext)-aead.Overhead()]
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128GCM.String():
|
||||||
|
paramSeq := aesGCMParameters{
|
||||||
|
Nonce: nonce,
|
||||||
|
ICVLen: aead.Overhead(),
|
||||||
|
}
|
||||||
|
|
||||||
|
paramBytes, _ := asn1.Marshal(paramSeq)
|
||||||
|
|
||||||
|
e.ContentEncryptionAlgorithmIdentifier = pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: e.EncryptionAlgorithmIdentifier,
|
||||||
|
Parameters: asn1.RawValue{
|
||||||
|
Tag: asn1.TagSequence,
|
||||||
|
Bytes: paramBytes,
|
||||||
|
}}
|
||||||
|
case AEADChaCha20Poly1305.String():
|
||||||
|
e.ContentEncryptionAlgorithmIdentifier = pkix.AlgorithmIdentifier{
|
||||||
|
Algorithm: e.EncryptionAlgorithmIdentifier,
|
||||||
|
Parameters: asn1.RawValue{Tag: 4, Bytes: nonce}}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceSize = 12
|
||||||
|
|
||||||
|
type aesGCMParameters struct {
|
||||||
|
Nonce []byte `asn1:"tag:4"`
|
||||||
|
ICVLen int
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts the ciphertext and returns the plaintext.
|
||||||
|
func (e *EncryptionAlgorithm) Decrypt(ciphertext []byte) (plaintext []byte, err error) {
|
||||||
|
|
||||||
|
e.EncryptionAlgorithmIdentifier = e.ContentEncryptionAlgorithmIdentifier.Algorithm
|
||||||
|
|
||||||
|
//Choose cipher
|
||||||
|
var blockCipher cipher.Block
|
||||||
|
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128CBC.String(), EncryptionAlgorithmAES256CBC.String(), EncryptionAlgorithmAES128GCM.String():
|
||||||
|
blockCipher, err = aes.NewCipher(e.Key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case EncryptionAlgorithmDESCBC.String():
|
||||||
|
blockCipher, err = des.NewCipher(e.Key)
|
||||||
|
fmt.Println("Warning: message is encoded with DES. DES should NOT be used.")
|
||||||
|
case EncryptionAlgorithmDESEDE3CBC.String():
|
||||||
|
blockCipher, err = des.NewTripleDESCipher(e.Key)
|
||||||
|
fmt.Println("Warning: message is encoded with 3DES. 3DES should NOT be used.")
|
||||||
|
case AEADChaCha20Poly1305.String():
|
||||||
|
default:
|
||||||
|
err = errors.New("Content encrytion: cipher not supportet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
//Choose blockmode
|
||||||
|
var blockMode cipher.BlockMode
|
||||||
|
var aead cipher.AEAD
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128CBC.String(), EncryptionAlgorithmAES256CBC.String(), EncryptionAlgorithmDESCBC.String(), EncryptionAlgorithmDESEDE3CBC.String():
|
||||||
|
e.IV = e.ContentEncryptionAlgorithmIdentifier.Parameters.Bytes
|
||||||
|
|
||||||
|
blockMode = cipher.NewCBCDecrypter(blockCipher, e.IV)
|
||||||
|
case EncryptionAlgorithmAES128GCM.String():
|
||||||
|
aead, err = cipher.NewGCM(blockCipher)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case AEADChaCha20Poly1305.String():
|
||||||
|
aead, err = chacha20poly1305.New(e.Key)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128CBC.String(), EncryptionAlgorithmAES256CBC.String(), EncryptionAlgorithmDESCBC.String(), EncryptionAlgorithmDESEDE3CBC.String():
|
||||||
|
plaintext = make([]byte, len(ciphertext))
|
||||||
|
blockMode.CryptBlocks(plaintext, ciphertext)
|
||||||
|
|
||||||
|
return unpad(plaintext, blockMode.BlockSize())
|
||||||
|
case EncryptionAlgorithmAES128GCM.String(), AEADChaCha20Poly1305.String():
|
||||||
|
var cipher []byte
|
||||||
|
cipher = append(cipher, ciphertext...)
|
||||||
|
cipher = append(cipher, e.MAC...)
|
||||||
|
|
||||||
|
var nonce []byte
|
||||||
|
switch e.EncryptionAlgorithmIdentifier.String() {
|
||||||
|
case EncryptionAlgorithmAES128GCM.String():
|
||||||
|
params := aesGCMParameters{}
|
||||||
|
paramBytes := e.ContentEncryptionAlgorithmIdentifier.Parameters.Bytes
|
||||||
|
_, err = asn1.Unmarshal(paramBytes, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
nonce = params.Nonce
|
||||||
|
case AEADChaCha20Poly1305.String():
|
||||||
|
nonce = e.ContentEncryptionAlgorithmIdentifier.Parameters.Bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
plaintext, err = aead.Open(nil, nonce, cipher, nil)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func pad(data []byte, blocklen int) ([]byte, error) {
|
||||||
|
if blocklen < 1 {
|
||||||
|
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
|
||||||
|
}
|
||||||
|
padlen := blocklen - (len(data) % blocklen)
|
||||||
|
if padlen == 0 {
|
||||||
|
padlen = blocklen
|
||||||
|
}
|
||||||
|
pad := bytes.Repeat([]byte{byte(padlen)}, padlen)
|
||||||
|
return append(data, pad...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func unpad(data []byte, blocklen int) ([]byte, error) {
|
||||||
|
if blocklen < 1 {
|
||||||
|
return nil, fmt.Errorf("invalid blocklen %d", blocklen)
|
||||||
|
}
|
||||||
|
if len(data)%blocklen != 0 || len(data) == 0 {
|
||||||
|
return nil, fmt.Errorf("invalid data len %d", len(data))
|
||||||
|
}
|
||||||
|
|
||||||
|
// the last byte is the length of padding
|
||||||
|
padlen := int(data[len(data)-1])
|
||||||
|
|
||||||
|
// check padding integrity, all bytes should be the same
|
||||||
|
pad := data[len(data)-padlen:]
|
||||||
|
for _, padbyte := range pad {
|
||||||
|
if padbyte != byte(padlen) {
|
||||||
|
return nil, errors.New("invalid padding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return data[:len(data)-padlen], nil
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user