Add KeyIDs public API functionality (#76)
* Add public KeyIDs functions * Add signature keyIDs functions * Lint code
This commit is contained in:
parent
1f4d966115
commit
2f89b9fa0e
7 changed files with 212 additions and 9 deletions
45
CHANGELOG.md
45
CHANGELOG.md
|
|
@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
(key *Key) ArmorWithCustomHeaders(comment, version string) (string, error)
|
(key *Key) ArmorWithCustomHeaders(comment, version string) (string, error)
|
||||||
(key *Key) GetArmoredPublicKeyWithCustomHeaders(comment, version string) (string, error)
|
(key *Key) GetArmoredPublicKeyWithCustomHeaders(comment, version string) (string, error)
|
||||||
```
|
```
|
||||||
|
|
||||||
- Message armoring with custom headers
|
- Message armoring with custom headers
|
||||||
```go
|
```go
|
||||||
(msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (string, error)
|
(msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (string, error)
|
||||||
|
|
@ -18,7 +19,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
- Extraction of encryption key IDs from a PGP message, i.e. the IDs of the keys used in the encryption of the session key
|
- Extraction of encryption key IDs from a PGP message, i.e. the IDs of the keys used in the encryption of the session key
|
||||||
```go
|
```go
|
||||||
(msg *PGPMessage) getEncryptionKeyIDs() ([]uint64, bool)
|
(msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool)
|
||||||
|
(msg *PGPMessage) GetHexEncryptionKeyIDs() ([]uint64, bool)
|
||||||
|
```
|
||||||
|
|
||||||
|
- Extraction of signing key IDs from a PGP message, i.e. the IDs of the keys used in the signature of the message
|
||||||
|
(of all the readable, unencrypted signature packets)
|
||||||
|
```go
|
||||||
|
(msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool)
|
||||||
|
(msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool)
|
||||||
```
|
```
|
||||||
|
|
||||||
- Getter for the x/crypto Entity (internal components of an OpenPGP key) from Key struct
|
- Getter for the x/crypto Entity (internal components of an OpenPGP key) from Key struct
|
||||||
|
|
@ -37,16 +46,48 @@ DecryptBinaryMessageArmored(privateKey string, passphrase []byte, ciphertext str
|
||||||
(key *Key) ToPublic() (publicKey *Key, err error)
|
(key *Key) ToPublic() (publicKey *Key, err error)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
- Helpers to handle detached signatures
|
||||||
|
```go
|
||||||
|
EncryptSignArmoredDetached(
|
||||||
|
publicKey, privateKey string,
|
||||||
|
passphrase, plainData []byte,
|
||||||
|
) (ciphertext, signature string, err error)
|
||||||
|
|
||||||
|
DecryptVerifyArmoredDetached(
|
||||||
|
publicKey, privateKey string,
|
||||||
|
passphrase []byte,
|
||||||
|
ciphertext string,
|
||||||
|
armoredSignature string,
|
||||||
|
) (plainData []byte, err error)
|
||||||
|
```
|
||||||
|
|
||||||
|
- `EncryptSignArmoredDetachedMobileResult` Struct (with its helper) to allow detached signature + encryption in one pass
|
||||||
|
```go
|
||||||
|
type EncryptSignArmoredDetachedMobileResult struct {
|
||||||
|
Ciphertext, Signature string
|
||||||
|
}
|
||||||
|
|
||||||
|
EncryptSignArmoredDetachedMobile(
|
||||||
|
publicKey, privateKey string,
|
||||||
|
passphrase, plainData []byte,
|
||||||
|
) (wrappedTuple *EncryptSignArmoredDetachedMobileResult, err error)
|
||||||
|
```
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
- Improved key and message armoring testing
|
- Improved key and message armoring testing
|
||||||
- `EncryptSessionKey` now creates encrypted key packets for each valid encryption key in the provided keyring.
|
- `EncryptSessionKey` now creates encrypted key packets for each valid encryption key in the provided keyring.
|
||||||
Returns a byte slice with all the concatenated key packets.
|
Returns a byte slice with all the concatenated key packets.
|
||||||
- Use aes256 chiper for message encryption with password.
|
- Use aes256 cipher for password-encrypted messages.
|
||||||
|
- The helpers `EncryptSignMessageArmored`, `DecryptVerifyMessageArmored`, `DecryptVerifyAttachment`, and`DecryptBinaryMessageArmored`
|
||||||
|
now accept private keys as public keys and perform automatic casting if the keys are locked.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
- Public key armoring headers
|
- Public key armoring headers
|
||||||
- `EncryptSessionKey` throws an error when invalid encryption keys are provided
|
- `EncryptSessionKey` throws an error when invalid encryption keys are provided
|
||||||
- Session keys' size is now checked against the expected value to prevent panics
|
- Session keys' size is now checked against the expected value to prevent panics
|
||||||
|
- Hex Key IDs returned from `(key *Key) GetHexKeyID() string` are now correctly padded
|
||||||
|
- Avoid panics in `(msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool)` by breaking the packet.next cycle on specific packet types
|
||||||
|
- Prevent the server time from going backwards in `UpdateTime`
|
||||||
|
|
||||||
## [2.0.1] - 2020-05-01
|
## [2.0.1] - 2020-05-01
|
||||||
### Security
|
### Security
|
||||||
|
|
|
||||||
|
|
@ -332,7 +332,7 @@ func (key *Key) PrintFingerprints() {
|
||||||
|
|
||||||
// GetHexKeyID returns the key ID, hex encoded as a string.
|
// GetHexKeyID returns the key ID, hex encoded as a string.
|
||||||
func (key *Key) GetHexKeyID() string {
|
func (key *Key) GetHexKeyID() string {
|
||||||
return strconv.FormatUint(key.GetKeyID(), 16)
|
return keyIDToHex(key.GetKeyID())
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetKeyID returns the key ID, encoded as 8-byte int.
|
// GetKeyID returns the key ID, encoded as 8-byte int.
|
||||||
|
|
@ -465,3 +465,8 @@ func generateKey(
|
||||||
|
|
||||||
return &Key{newEntity}, nil
|
return &Key{newEntity}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyIDToHex casts a keyID to hex with the correct padding.
|
||||||
|
func keyIDToHex(keyID uint64) string {
|
||||||
|
return fmt.Sprintf("%016v", strconv.FormatUint(keyID, 16))
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -223,8 +223,8 @@ func (msg *PGPMessage) GetArmoredWithCustomHeaders(comment, version string) (str
|
||||||
return armor.ArmorWithTypeAndCustomHeaders(msg.Data, constants.PGPMessageHeader, version, comment)
|
return armor.ArmorWithTypeAndCustomHeaders(msg.Data, constants.PGPMessageHeader, version, comment)
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEncryptionKeyIds Returns the key IDs of the keys to which the session key is encrypted.
|
// GetEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
|
||||||
func (msg *PGPMessage) getEncryptionKeyIDs() ([]uint64, bool) {
|
func (msg *PGPMessage) GetEncryptionKeyIDs() ([]uint64, bool) {
|
||||||
packets := packet.NewReader(bytes.NewReader(msg.Data))
|
packets := packet.NewReader(bytes.NewReader(msg.Data))
|
||||||
var err error
|
var err error
|
||||||
var ids []uint64
|
var ids []uint64
|
||||||
|
|
@ -252,6 +252,21 @@ Loop:
|
||||||
return ids, false
|
return ids, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetHexEncryptionKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
|
||||||
|
func (msg *PGPMessage) GetHexEncryptionKeyIDs() ([]string, bool) {
|
||||||
|
return getHexKeyIDs(msg.GetEncryptionKeyIDs())
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to.
|
||||||
|
func (msg *PGPMessage) GetSignatureKeyIDs() ([]uint64, bool) {
|
||||||
|
return getSignatureKeyIDs(msg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
|
||||||
|
func (msg *PGPMessage) GetHexSignatureKeyIDs() ([]string, bool) {
|
||||||
|
return getHexKeyIDs(msg.GetSignatureKeyIDs())
|
||||||
|
}
|
||||||
|
|
||||||
// GetBinaryDataPacket returns the unarmored binary datapacket as a []byte.
|
// GetBinaryDataPacket returns the unarmored binary datapacket as a []byte.
|
||||||
func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte {
|
func (msg *PGPSplitMessage) GetBinaryDataPacket() []byte {
|
||||||
return msg.DataPacket
|
return msg.DataPacket
|
||||||
|
|
@ -386,6 +401,16 @@ func (msg *PGPSignature) GetArmored() (string, error) {
|
||||||
return armor.ArmorWithType(msg.Data, constants.PGPSignatureHeader)
|
return armor.ArmorWithType(msg.Data, constants.PGPSignatureHeader)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetSignatureKeyIDs Returns the key IDs of the keys to which the (readable) signature packets are encrypted to.
|
||||||
|
func (msg *PGPSignature) GetSignatureKeyIDs() ([]uint64, bool) {
|
||||||
|
return getSignatureKeyIDs(msg.Data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetHexSignatureKeyIDs Returns the key IDs of the keys to which the session key is encrypted.
|
||||||
|
func (msg *PGPSignature) GetHexSignatureKeyIDs() ([]string, bool) {
|
||||||
|
return getHexKeyIDs(msg.GetSignatureKeyIDs())
|
||||||
|
}
|
||||||
|
|
||||||
// GetBinary returns the unarmored signed data as a []byte.
|
// GetBinary returns the unarmored signed data as a []byte.
|
||||||
func (msg *ClearTextMessage) GetBinary() []byte {
|
func (msg *ClearTextMessage) GetBinary() []byte {
|
||||||
return msg.Data
|
return msg.Data
|
||||||
|
|
@ -425,3 +450,48 @@ func IsPGPMessage(data string) bool {
|
||||||
constants.PGPMessageHeader + "-----")
|
constants.PGPMessageHeader + "-----")
|
||||||
return re.MatchString(data)
|
return re.MatchString(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getSignatureKeyIDs(data []byte) ([]uint64, bool) {
|
||||||
|
packets := packet.NewReader(bytes.NewReader(data))
|
||||||
|
var err error
|
||||||
|
var ids []uint64
|
||||||
|
var onePassSignaturePacket *packet.OnePassSignature
|
||||||
|
var signaturePacket *packet.Signature
|
||||||
|
|
||||||
|
Loop:
|
||||||
|
for {
|
||||||
|
var p packet.Packet
|
||||||
|
if p, err = packets.Next(); err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch p := p.(type) {
|
||||||
|
case *packet.OnePassSignature:
|
||||||
|
onePassSignaturePacket = p
|
||||||
|
ids = append(ids, onePassSignaturePacket.KeyId)
|
||||||
|
case *packet.Signature:
|
||||||
|
signaturePacket = p
|
||||||
|
if signaturePacket.IssuerKeyId != nil {
|
||||||
|
ids = append(ids, *signaturePacket.IssuerKeyId)
|
||||||
|
}
|
||||||
|
case *packet.SymmetricallyEncrypted,
|
||||||
|
*packet.AEADEncrypted,
|
||||||
|
*packet.Compressed,
|
||||||
|
*packet.LiteralData:
|
||||||
|
break Loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(ids) > 0 {
|
||||||
|
return ids, true
|
||||||
|
}
|
||||||
|
return ids, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHexKeyIDs(keyIDs []uint64, ok bool) ([]string, bool) {
|
||||||
|
hexIDs := make([]string, len(keyIDs))
|
||||||
|
|
||||||
|
for i, id := range keyIDs {
|
||||||
|
hexIDs[i] = keyIDToHex(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return hexIDs, ok
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -228,7 +228,7 @@ func TestMultipleKeyMessageEncryption(t *testing.T) {
|
||||||
assert.Exactly(t, message.GetString(), decrypted.GetString())
|
assert.Exactly(t, message.GetString(), decrypted.GetString())
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMessagegetGetEncryptionKeyIDs(t *testing.T) {
|
func TestMessageGetEncryptionKeyIDs(t *testing.T) {
|
||||||
var message = NewPlainMessageFromString("plain text")
|
var message = NewPlainMessageFromString("plain text")
|
||||||
assert.Exactly(t, 3, len(keyRingTestMultiple.entities))
|
assert.Exactly(t, 3, len(keyRingTestMultiple.entities))
|
||||||
|
|
||||||
|
|
@ -236,7 +236,7 @@ func TestMessagegetGetEncryptionKeyIDs(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Expected no error when encrypting, got:", err)
|
t.Fatal("Expected no error when encrypting, got:", err)
|
||||||
}
|
}
|
||||||
ids, ok := ciphertext.getEncryptionKeyIDs()
|
ids, ok := ciphertext.GetEncryptionKeyIDs()
|
||||||
assert.Exactly(t, 3, len(ids))
|
assert.Exactly(t, 3, len(ids))
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
encKey, ok := keyRingTestMultiple.entities[0].EncryptionKey(time.Now())
|
encKey, ok := keyRingTestMultiple.entities[0].EncryptionKey(time.Now())
|
||||||
|
|
@ -244,6 +244,50 @@ func TestMessagegetGetEncryptionKeyIDs(t *testing.T) {
|
||||||
assert.Exactly(t, encKey.PublicKey.KeyId, ids[0])
|
assert.Exactly(t, encKey.PublicKey.KeyId, ids[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestMessageGetHexGetEncryptionKeyIDs(t *testing.T) {
|
||||||
|
ciphertext, err := NewPGPMessageFromArmored(readTestFile("message_multipleKeyID", false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Expected no error when reading message, got:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, ok := ciphertext.GetHexEncryptionKeyIDs()
|
||||||
|
assert.Exactly(t, 2, len(ids))
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
assert.Exactly(t, "76ad736fa7e0e83c", ids[0])
|
||||||
|
assert.Exactly(t, "0f65b7ae456a9ceb", ids[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageGetSignatureKeyIDs(t *testing.T) {
|
||||||
|
var message = NewPlainMessageFromString("plain text")
|
||||||
|
|
||||||
|
signature, err := keyRingTestPrivate.SignDetached(message)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Expected no error when encrypting, got:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, ok := signature.GetSignatureKeyIDs()
|
||||||
|
assert.Exactly(t, 1, len(ids))
|
||||||
|
assert.True(t, ok)
|
||||||
|
signingKey, ok := keyRingTestPrivate.entities[0].SigningKey(time.Now())
|
||||||
|
assert.True(t, ok)
|
||||||
|
assert.Exactly(t, signingKey.PublicKey.KeyId, ids[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageGetHexSignatureKeyIDs(t *testing.T) {
|
||||||
|
ciphertext, err := NewPGPMessageFromArmored(readTestFile("message_plainSignature", false))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal("Expected no error when reading message, got:", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ids, ok := ciphertext.GetHexSignatureKeyIDs()
|
||||||
|
assert.Exactly(t, 2, len(ids))
|
||||||
|
assert.True(t, ok)
|
||||||
|
|
||||||
|
assert.Exactly(t, "3eb6259edf21df24", ids[0])
|
||||||
|
assert.Exactly(t, "d05b722681936ad0", ids[1])
|
||||||
|
}
|
||||||
|
|
||||||
func TestMessageGetArmoredWithCustomHeaders(t *testing.T) {
|
func TestMessageGetArmoredWithCustomHeaders(t *testing.T) {
|
||||||
var message = NewPlainMessageFromString("plain text")
|
var message = NewPlainMessageFromString("plain text")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ func TestDataPacketEncryption(t *testing.T) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal("Unable to unarmor pgp message, got:", err)
|
t.Fatal("Unable to unarmor pgp message, got:", err)
|
||||||
}
|
}
|
||||||
ids, ok := pgpMessage.getEncryptionKeyIDs()
|
ids, ok := pgpMessage.GetEncryptionKeyIDs()
|
||||||
assert.True(t, ok)
|
assert.True(t, ok)
|
||||||
assert.Exactly(t, 3, len(ids))
|
assert.Exactly(t, 3, len(ids))
|
||||||
|
|
||||||
|
|
|
||||||
20
crypto/testdata/message_multipleKeyID
vendored
Normal file
20
crypto/testdata/message_multipleKeyID
vendored
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
Version: OpenPGP.js v3.1.3
|
||||||
|
Comment: https://openpgpjs.org
|
||||||
|
|
||||||
|
wcBMA3atc2+n4Og8AQf+PiFMi7BkL/Ppe6kILs0E/ik0jR4sAXaNA0kpJYYE
|
||||||
|
FhWBZ8RWOFLeriyABaY2gQD86kQ7TZSgJcOoBYWLEIRdnHP0ss4niG1WuEPB
|
||||||
|
gyKXdyyS99URdYtxCV0ErxPHObdO61vxwsE7Fx9nC3Lk8mnktzfqWQelA9Ej
|
||||||
|
TLj9pO3M+uo2H5baZ8ylErcTRmgyQlSy+gcjCDeLs94kDpbdJlZX1x+79Zub
|
||||||
|
2iWnOSmdmW2ZhbBq7w8az5qhPUfMMNIRcfieOAQwkKz5dBoCO81mwHcd+LWY
|
||||||
|
EiiNDNVrD5uYV4t0PqHp9o5537JV//tlG1UEWvNhQ35tqXgsmND7MQkb2ct3
|
||||||
|
6cHATAMPZbeuRWqc6wEH/2wKL4h9QOOxBAJKHf1MJlVbMN8qgwtKr7q/YcdH
|
||||||
|
gooTF5asSMq91qKCcW38NuFXkdzf5sGZsOA0yLTQnjbu+42RzBfty10qTZg0
|
||||||
|
v1zmgKErACFpsztYNOdsJh3aGJ4WjytpZWKL+PqHnX4/HR+zbEMEGfDjhTBZ
|
||||||
|
eL8TLa3F3OK533F2oNAO5ITYsdnipVmiy89FL5yt/9ZpvFRRuj8lOwJYTe4O
|
||||||
|
pQ+ZbenRZ0sXyEIV6xZguqvFICOELoy4LM3kHpQfY4Gi1JPT/buWxnDFugEW
|
||||||
|
u5JQ6qix0Y1KunuWSogEjfaJ8BgLSBs/U7RxxjLYETFB15VxyEaQJx9wx3Id
|
||||||
|
uU7SRQG+UkFbDn9ghZoF7ROPTXAUnHlqODGxdgnPhJJQaPSkNOMALkBI6I4Q
|
||||||
|
lqsO2LprVTVeCGo+Qd3WrE5MqGunvDli5VK37A==
|
||||||
|
=QeVN
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
23
crypto/testdata/message_plainSignature
vendored
Normal file
23
crypto/testdata/message_plainSignature
vendored
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
-----BEGIN PGP MESSAGE-----
|
||||||
|
Comment: https://gopenpgp.org
|
||||||
|
Version: GopenPGP 2.0.1
|
||||||
|
|
||||||
|
wcBMAw9lt65FapzrAQf/bAoviH1A47EEAkod/UwmVVsw3yqDC0qvur9hx0eCihMX
|
||||||
|
lqxIyr3WooJxbfw24VeR3N/mwZmw4DTItNCeNu77jZHMF+3LXSpNmDS/XOaAoSsA
|
||||||
|
IWmzO1g052wmHdoYnhaPK2llYov4+oedfj8dH7NsQwQZ8OOFMFl4vxMtrcXc4rnf
|
||||||
|
cXag0A7khNix2eKlWaLLz0UvnK3/1mm8VFG6PyU7AlhN7g6lD5lt6dFnSxfIQhXr
|
||||||
|
FmC6q8UgI4QujLgszeQelB9jgaLUk9P9u5bGcMW6ARa7klDqqLHRjUq6e5ZKiASN
|
||||||
|
9onwGAtIGz9TtHHGMtgRMUHXlXHIRpAnH3DHch25TsLAXAQAAQoAEAUCXNlzAwkQ
|
||||||
|
PrYlnt8h3yQAAMuZCACipXp2GmSo26JgRJADNan01cBu6nVbzpNHNqKqUNLDnCvZ
|
||||||
|
L4HjXeUQ/o+vl8GSpy51kvcXmNsD36d4agnzDf7OjiIdcLns/ARSUESQyrprf+oF
|
||||||
|
+OYTeRXufxCoiG35Kn82g4ML2ifj52c+E/mS7ZTupQgSZrXPcS7XNAEuAuOnjC8O
|
||||||
|
5TpzlA3hKwirVzRVmyn2wlTVWQWWMNoci8esnLH/eZVt/3DFCBwVG3D+avsnKXN3
|
||||||
|
CI6kAFtiRA9drDHXT56AR4lZGotTltG2g5D3exAzczuHpxA4Qshp/03dDADBAm4T
|
||||||
|
3IJ6p/7rZ3wgmeaRfcg9sAxEJ4y1pME1ma8jBrB8wpwEAAEKABAFAlzZcwMJENBb
|
||||||
|
ciaBk2rQAADeRAP+LCDF4zEXVQYhQXBnvVGEK1Ar3R/0lj3GSWczb3+QYbcmwAZR
|
||||||
|
n+ll3xlrgCqls4BfVBvXQ/hyABF3HPlkFRNodHLonq+fuvjCgEnsdJG18/yzfeP6
|
||||||
|
Ox0w2vHRE3ad78dhJvyuWqL7Wd8L2EG9MsCdzx5MQnfWShzQE4EcSnbCtprSRQG+
|
||||||
|
UkFbDn9ghZoF7ROPTXAUnHlqODGxdgnPhJJQaPSkNOMALkBI6I4QlqsO2LprVTVe
|
||||||
|
CGo+Qd3WrE5MqGunvDli5VK37A==
|
||||||
|
=cPmR
|
||||||
|
-----END PGP MESSAGE-----
|
||||||
Loading…
Add table
Add a link
Reference in a new issue