Skip to content

Commit

Permalink
Merge pull request #555 from dinukaamarasinghe817/pgp
Browse files Browse the repository at this point in the history
Add support for PGP Encryption/Decryption
  • Loading branch information
Bhashinee authored May 13, 2024
2 parents 016076b + f24bd9a commit 2e30356
Show file tree
Hide file tree
Showing 17 changed files with 830 additions and 4 deletions.
12 changes: 9 additions & 3 deletions ballerina/Ballerina.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[package]
org = "ballerina"
name = "crypto"
version = "2.7.0"
version = "2.7.1"
authors = ["Ballerina"]
keywords = ["security", "hash", "hmac", "sign", "encrypt", "decrypt", "private key", "public key"]
repository = "https://github.com/ballerina-platform/module-ballerina-crypto"
Expand All @@ -15,8 +15,8 @@ graalvmCompatible = true
[[platform.java17.dependency]]
groupId = "io.ballerina.stdlib"
artifactId = "crypto-native"
version = "2.7.0"
path = "../native/build/libs/crypto-native-2.7.0.jar"
version = "2.7.1"
path = "../native/build/libs/crypto-native-2.7.1-SNAPSHOT.jar"

[[platform.java17.dependency]]
groupId = "org.bouncycastle"
Expand All @@ -35,3 +35,9 @@ groupId = "org.bouncycastle"
artifactId = "bcutil-jdk18on"
version = "1.78"
path = "./lib/bcutil-jdk18on-1.78.jar"

[[platform.java17.dependency]]
groupId = "org.bouncycastle"
artifactId = "bcpg-jdk18on"
version = "1.78"
path = "./lib/bcpg-jdk18on-1.78.jar"
2 changes: 1 addition & 1 deletion ballerina/Dependencies.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ distribution-version = "2201.9.0"
[[package]]
org = "ballerina"
name = "crypto"
version = "2.7.0"
version = "2.7.1"
dependencies = [
{org = "ballerina", name = "jballerina.java"},
{org = "ballerina", name = "lang.array"},
Expand Down
3 changes: 3 additions & 0 deletions ballerina/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,9 @@ dependencies {
externalJars(group: 'org.bouncycastle', name: 'bcutil-jdk18on', version: "${bouncycastleVersion}") {
transitive = false
}
externalJars(group: 'org.bouncycastle', name: 'bcpg-jdk18on', version: "${bouncycastleVersion}") {
transitive = false
}
}

task updateTomlFiles {
Expand Down
35 changes: 35 additions & 0 deletions ballerina/encrypt_decrypt.bal
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,38 @@ public isolated function decryptAesGcm(byte[] input, byte[] key, byte[] iv, AesP
name: "decryptAesGcm",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt"
} external;

# Returns the PGP-encrypted value for the given data.
# ```ballerina
# byte[] message = "Hello Ballerina!".toBytes();
# byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc");
# ```
#
# + plainText - The content to be encrypted
# + publicKeyPath - Path to the public key
# + options - PGP encryption options
# + return - Encrypted data or else a `crypto:Error` if the key is invalid
public isolated function encryptPgp(byte[] plainText, string publicKeyPath, *Options options)
returns byte[]|Error = @java:Method {
name: "encryptPgp",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Encrypt"
} external;

# Returns the PGP-decrypted value of the given PGP-encrypted data.
# ```ballerina
# byte[] message = "Hello Ballerina!".toBytes();
# byte[] cipherText = check crypto:encryptPgp(message, "public_key.asc");
#
# byte[] passphrase = check io:fileReadBytes("pass_phrase.txt");
# byte[] decryptedMessage = check crypto:decryptPgp(cipherText, "private_key.asc", passphrase);
# ```
#
# + cipherText - The encrypted content to be decrypted
# + privateKeyPath - Path to the private key
# + passphrase - passphrase of the private key
# + return - Decrypted data or else a `crypto:Error` if the key or passphrase is invalid
public isolated function decryptPgp(byte[] cipherText, string privateKeyPath, byte[] passphrase)
returns byte[]|Error = @java:Method {
name: "decryptPgp",
'class: "io.ballerina.stdlib.crypto.nativeimpl.Decrypt"
} external;
74 changes: 74 additions & 0 deletions ballerina/pgp_utils.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

# Represents the PGP encryption options.
#
# + compressionAlgorithm - Specifies the compression algorithm used for PGP encryption
# + symmetricKeyAlgorithm - Specifies the symmetric key algorithm used for encryption
# + armor - Indicates whether ASCII armor is enabled for the encrypted output
# + withIntegrityCheck - Indicates whether integrity check is included in the encryption
public type Options record {|
CompressionAlgorithmTags compressionAlgorithm = ZIP;
SymmetricKeyAlgorithmTags symmetricKeyAlgorithm = AES_256;
boolean armor = true;
boolean withIntegrityCheck = true;
|};

# Represents the compression algorithms available in PGP.
#
# + UNCOMPRESSED - No compression
# + ZIP - Uses (RFC 1951) compression
# + ZLIB - Uses (RFC 1950) compression
# + BZIP2 - Uses Burrows–Wheeler algorithm
public enum CompressionAlgorithmTags {
UNCOMPRESSED = "0",
ZIP = "1",
ZLIB = "2",
BZIP2= "3"
}

# Represent the symmetric key algorithms available in PGP.
#
# + NULL - No encryption
# + IDEA - IDEA symmetric key algorithm
# + TRIPLE_DES - Triple DES symmetric key algorithm
# + CAST5 - CAST5 symmetric key algorithm
# + BLOWFISH - Blowfish symmetric key algorithm
# + SAFER - SAFER symmetric key algorithm
# + DES - DES symmetric key algorithm
# + AES_128 - AES 128-bit symmetric key algorithm
# + AES_192 - AES 192-bit symmetric key algorithm
# + AES_256 - AES 256-bit symmetric key algorithm
# + TWOFISH - Twofish symmetric key algorithm
# + CAMELLIA_128 - Camellia 128-bit symmetric key algorithm
# + CAMELLIA_192 - Camellia 192-bit symmetric key algorithm
# + CAMMELIA_256 - Camellia 256-bit symmetric key algorithm
public enum SymmetricKeyAlgorithmTags {
NULL = "0",
IDEA = "1",
TRIPLE_DES = "2",
CAST5 = "3",
BLOWFISH = "4",
SAFER = "5",
DES = "6",
AES_128 = "7",
AES_192 = "8",
AES_256 = "9",
TWOFISH = "10",
CAMELLIA_128 = "11",
CAMELLIA_192 = "12",
CAMELLIA_256 = "13"
}
62 changes: 62 additions & 0 deletions ballerina/tests/encrypt_decrypt_pgp_test.bal
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
// Copyright (c) 2024 WSO2 LLC. (https://www.wso2.com).
//
// WSO2 LLC. licenses this file to you under the Apache License,
// Version 2.0 (the "License"); you may not use this file except
// in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import ballerina/test;

@test:Config {}
isolated function testEncryptAndDecryptWithPgp() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH);
byte[] plainText = check decryptPgp(cipherText, PGP_PRIVATE_KEY_PATH, passphrase);
test:assertEquals(plainText.toBase16(), message.toBase16());
}

@test:Config {}
isolated function testEncryptAndDecryptWithPgpWithOptions() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "qCr3bv@5mj5n4eY".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH, symmetricKeyAlgorithm = AES_128, armor = false);
byte[] plainText = check decryptPgp(cipherText, PGP_PRIVATE_KEY_PATH, passphrase);
test:assertEquals(plainText.toBase16(), message.toBase16());
}

@test:Config {}
isolated function testNegativeEncryptAndDecryptWithPgpInvalidPrivateKey() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH);
byte[]|Error plainText = decryptPgp(cipherText, PGP_INVALID_PRIVATE_KEY_PATH, passphrase);
if plainText is Error {
test:assertEquals(plainText.message(), "Error occurred while PGP decrypt: Could Not Extract private key");
} else {
test:assertFail("Should return a crypto Error");
}
}

@test:Config {}
isolated function testNegativeEncryptAndDecryptWithPgpInvalidPassphrase() returns error? {
byte[] message = "Ballerina crypto test ".toBytes();
byte[] passphrase = "p7S5@T2MRFD9TQb".toBytes();
byte[] cipherText = check encryptPgp(message, PGP_PUBLIC_KEY_PATH);
byte[]|Error plainText = decryptPgp(cipherText, PGP_PRIVATE_KEY_PATH, passphrase);
if plainText is Error {
test:assertEquals(plainText.message(),
"Error occurred while PGP decrypt: checksum mismatch at in checksum of 20 bytes");
} else {
test:assertFail("Should return a crypto Error");
}
}
99 changes: 99 additions & 0 deletions ballerina/tests/resources/invalid_private_key.asc
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
-----BEGIN PGP PRIVATE KEY BLOCK-----
Version: Keybase OpenPGP v2.0.76
Comment: https://keybase.io/crypto

xcMGBGY8Rx0BCAC+lfjc0bvxHCaZY6txTJOvMygfDVYtOLx19KCP/+B5Qjl0AuJ9
Ky0JaJJdGpe4IZvKgB0Sr+elLBRRLIvapmuDD6feSbUHl+ckeaCY26j6qWmAXT0I
9PI248rRCYzW3kyIa0c/d0pmwlVVICQ8DXxUaLBI9614q+v2lHRjKruGWAsdKIQ/
jssmTZI4b2pCqSlBe2PFtrKgLKNzSPXu8UFq7Ck8qoTkcaSBvKgDmf1su1PvEM+R
iNHqUmKE/w6FOVfkRkSYWs9er5pO4k5k0/LtSa3K8Abgwg4WOX7PwzPrC+RvNg7X
xuivbogqR6/i+CsYBmmhw3AGC2pXu2K8m/N1ABEBAAH+CQMI95pVHMldPkZgvH1v
fb14Il6kaWoHf6IbQMkxgouO8/Wk+PAhDkwS30z3UOdSlorG36ufJZD2P03DAtuq
VQ+TM1+kAG7R4nu8TICFV67jE86ouNpv4S4qhseLTk1a9+gPzoTT7VV49+9d8VPR
Pnn8FkOn9gZ92v8sHD3PyVTd8rAji2j+cNisxpz2c2ujEEf8rPS0pSgWwp7moY2h
Uykel91QNcq1mjLgETxkzEQZXL8w9w8N6RvcQyUuhWh2hx3YqpDQZZnOI9g5BDEv
TP2fGuVt3DV6VhyTGM0JONUS+03HfCasEW/JddF2M1+r9jRnjdcn2MIYivhhTeU9
aq4gwjzQCQZCkiQCBhoGtVyU/59qZ7Os+YJShZj7vikN/cAgkBkk7n0Q1HoDIFHl
ulDVX5AwGw5XdvAWpwAnJF8MDfe8d5ZbB4PSAHsb3ym4PohpQ2D7Q147bxb6uD3m
jQNZa9ULqCvdL7o+rXWhIULGEsvi7V9YHWEcCwy5ivV0IHzL0RFZmp61P1CgbC22
T8kWLOx9LQLown1t0LEtAb2oAaGL7XXoF/WWcAWSpWz6QL+VFKNAJeSrjXqdtn9c
zyo9n3JcgvYBjvtNeoU/QsMYGubDEiicOQMtDULZzDiICIh71Y8SRR9Iiypd97XN
e/za+GwBO3vI3nhPb1xFPASkTnFg+ldse4ngNjcXBUSLwGvKiR/aWDBrshlhpSlX
1h44Aw2WlXLLQRAR1H7HZ3/9W20j3JlgKzVjtWsgMIPqPGUzd5Oni6e9CgjaYT4l
U+bHznfrn5c5KOiNlr7tKRNghKXKOBBpVxdvkdYwTDEg/hPDTB2XZJDKLdBXykI6
tnCKw+O0Oo90hLoxWLOfk0VUVOZR9Wz50hpwcgBIK/P7XRZ8gev46V5RQ0xNCO+O
5pOlfMN7lJ9czRpIYXJpdGhhIDxoYXJpdGhAZ21haWwuY29tPsLAegQTAQoAJAUC
ZjxHHQIbLwMLCQcDFQoIAh4BAheAAxYCAQIZAQUJAeEzgAAKCRBWIr+sQ758fL0s
CACBLE+fvqMFLuLBWh/YlHaEmMPvvPKOkamLfMVaEouB81n55umZhtCtqvxf3j2v
EdPAbzm6i/OLpPAnV3xa4zSii754eOF1iNiYCR7h/nRvBsFfyEjhoLaPSfa0eAMR
ZCXkxDi++PZzpXBT0jgUOwOx8vdw1gBY/P78cOsYTzCoy1AJUfRhcWKBF1vgyqDt
IQJMHOKBkAGqOH2knLM9m6H6zcO1tXpHbpT6WG4DzGLHkK7gm5x4QuH6AbXrDyWb
gxXV3dLMJcQ23wzA7uLoqiznWNGcFZweeXDGcjZef3gBg7d8KAPHOelBN8gUKBKk
22E153/ESnuIBxWMKx2xFmFHx8MGBGY8Rx0BCADHChrRPQw2HGD1dBy+oenn6t0q
BfNhwDvh3n9u8ZD3QYk/7FKe8kXrYd5KUxcBUWn39fBXmSVjOPQYQpBHHF3I4+IF
eIDtJLmLcKL5CfJ/HE4BzyGtKIr95XMoxugu98Qqj14Lwf3LegZKhFO8s3VzHan1
1DNTsuIckHHgUvXp7+tHrtqD37qwFFxE/Cwg89n2UNi/nfhxaZp4SziT2tZJTWqj
X/fVkSUNi/nHe3iUB2+DrYo+N/mneKrQS6GBPz+vBE9O9vpCX0RvHfLho18GjBdb
V6Di4NHe/v1Mwfqr+Z/tADwgHW94W86+Wb24gCdzVLrtAbIns+pfIPSpsaQjABEB
AAH+CQMIUFzV892stCRgra+8XA9BQHkUeOdGvx0pRF3a7sgI2lsbHOanPJFEVQwg
7/z/W5hrh4WWDpArJNFbl4USSzULLensb3fd3DH3Eb3Nqq/HmmO1Qd3RhOAInQrZ
/O8u6tEMZPmLXHbOmcqsov7epw2d4T7hzkspigjgQ7QjHHCb2pRbOkRcuIi0kvNU
sg1upMJH9gb0GTpTLFRmyyUB7HEAjibCiwffGzVVO4YbfWgXE4VVJDI85btK8S42
0p+/HzI508On59ay/3Hfm7MJuy3JW7cySTTGxW5KmubLINmOU3JjQPEFwUcuyZA2
yLOsulLA5Rios9wRGkk4DBMiNDEbXnXvNHzI7ralEsqazOA6i675QhM5SUZcaydc
nlhQ1JlXUOQXC34yFaCTtjDyJbmszGLeohZBCkcdyD3B9W4SpMM41TQ3lX0qJbaX
d6pVPFC8PhmntJ9zvr4k5TL2XuB7awRGcmcV4LRG3chpBiRQ1eMIYbMrNhzNzPwq
oeJJUOt3tLbH7ROTM9WfuWJvJ3JxKI9ypLf8u54QB5dNFMTziZN2cybysLmU6RVv
IbEjxEUiEFvJPD55OLLqOQhukXoC90zXPp0u4ZzZSnUELkMZcHlsGik04L7I1M8X
tm/iBsn82owimZ2Gyj1afOICdC/aoZSxsrQlI/wA+CkHlfmT0OQhRDNt8YQfTHo3
Y3lSx7EaqKFxeAdao157UuWqxwDj2Rpl/hjZPg6hSAuPLrmCoVOnmCbyeq3EIEoB
LM8oTVlYrHaIkUPjyHvIRyfgDZk5dlOrCIzorGDh/d5gJbA8d0Rnl0QbswR4dvNR
/AMjV4Rd86i0sWbqKgcGF1wY9dbUPishZR73wZtsCvxQJhTPUVuTaEHaFpmhiC/g
EOPudwwpbThSyus2Gx5o6juh45YU56o1cbL4wsGEBBgBCgAPBQJmPEcdBQkB4TOA
AhsuASkJEFYiv6xDvnx8wF0gBBkBCgAGBQJmPEcdAAoJEG+5B/Ykf0qM79AH/0I5
PFbu4IBOoKR7c7XOGADyLo1DYKsNwLuxJLP8/3vy/BI5AFjnetroqgbLc85QlZxA
CeJsKZChkact1fnnc+nmOzGrlJSbNr9CZqSxnHctRdBHRMjoswyigkHjMqhKPwxk
+jOPRZsNxvYvKnkt4eTEqGwWof3bMXG11/jYQuJpyH5wh8LSC6be5lSYE9JxFw+a
hyMC79EFbxHYYZXwMzg+YIucGvFr6leLA2EUOFcRoDWmNHJta1fGZ9NIwX8bu++N
Je8Yn8Nnr2ba6hMiJWiYI4W70FOpcfC2r1OQtz5Kq4LOJfu6RsVzCTm0Af7wT2dB
HjfaE1mlpsDizxHMgl1Qnwf+NN1aTe7U8JTOgQegeAu7QWbaSwSo9JcJM8G9sirS
DGdeNwZDNAc0T60tjHeM4FslJzBzt2YNCgv0dFT7oPGtXOB44Jy1CEmK0nBPekDg
O0ycW0m89TbOJAvqplezlAlEgMAXFTl3PzJeiJYsNUoMn2HzC0NrO83mLdY96li7
pe1toe2nTK8fpouHN5nIoFG2TFRD2ko1/aNTjHvci1MneHdusPzAIae7P85bVxl9
5zjcKa66unW3OtpHKuKbNbPXCp7pNS7qgG/NROFpRpLYqxNud+dJ9nf0AZ8JS2wK
nXCYRanAbLHv4KqTwp3thFlV7fEzKugwc5jCCnPsQXi79MfDBgRmPEcdAQgAtrht
/mu6Jf2DLtgl+4XWpN9/pR1I1fReQd0Pg0rLuYyiH63cKBnPABkWEv3kIm5Vyh5s
AXJhxr+tRrGD3nLspGlDdcWM0BFv7Ua4E8OtuQ1Fyar7ZCmqUJKeIuqMDgtmLX1R
aas4UIEVjitbV3LaEPXtw9MyBIvrv8/NQtY+IeMFIVorzQZ+2owBJYP0w2gGP993
SlUB8yUhkDLLL2o3zAdIwX+jY35jmywdMCw3TWgJVu7PUN+wQCGfAvkezQwxaUAt
dH3sU8NCaTfqH18khoaG8+kmnFYhtxnU78F4rFoGAct8cUlgQ48VlciMk4N9UtAf
LbFwadDGmDR/eNGvrwARAQAB/gkDCN+ZCa9wF4WzYNo6nVDpYBKKX6HpntJX0b2T
YmWTOdh9Uc6T07v/wcswSDWoTG8CWu4/YEKjAgp+1B6cbxBsdwMzCsCCYJswhoc2
yVN9SiceF9noSxUTHWeXKx+X64PDnqI4CKax/PnRO5xzaFhPv8aGN7HVQcwIrpmL
wbjsw2eZCnLlGEbUEGGxtR+P9a8pm8QmcraTYD7sojj8vN6WrWnbx1iui/CeMLd6
YeVie6taoYgxzUBX+rgQbnsIWfjn7BFZjyhnv9FxzGmA0HtX/BMTWDXamntUYB6B
bgiGQReyzjMsNaYVvODyhly/ywahiCMtHJkY+efftZLm7Zax/ZWKMDGREriN5ybo
yRmVg2zy9zoeY4dWI9uF5y4tRAbaDBZOjb2TwoC0syEeGEXs/v7err4G+9DlRkO2
XszC8gGoqdxjdqhCJTpRRYd/R/EXvbyXqodwE5HOARF3BtVlFY60xrfZqrQqH6hV
vVb7hBj0BInMmghQ9BCaYZjmJgUn/7Shu0vzTzb17CRKYDiSbZAJfeIDJGLd4Pv4
YGI4YuZasUP+4teh98O0EQnw68WuHQc+OuHh46t9jIPqUIFoSj+T+TJNWnTMRssn
C6e/KakOsYWJwAdAZewTxZ33TMzH6QJOQ6/tF3bpIaC9Kg8oSeYMaMFIVuw6Omg3
fOB83gHbvHUj8u9LUZGpLdzIWJQuqo1cIhAkJV6GCSmA+vkHwd8yv9x5sOoNH0c4
Z31jQZmvLnNcZGP+mpZ0R8j959N8h8iwpxYJCgFFBXWzU54q1XfMDHctG8IOK7ru
9IsoApivJiVbqQsWjLbxDemCruk+UMWkFQe46Rhriu+wy88oOdHbaXBEdZHnrvkX
I+rcT0HmxbqJ2AdSSVDaT6rK0G7NurOOvOwtDA5cmxq0o4JWO8vvQfUyDcLBhAQY
AQoADwUCZjxHHQUJAeEzgAIbLgEpCRBWIr+sQ758fMBdIAQZAQoABgUCZjxHHQAK
CRCE0dPY9D+N5eRqCACCPfCj77XuTBQjWwP7f3LBZjR1Lk5V8VEuY1pjUWF2OCGL
lIhz92Im44NmChk6vcBnuSNx/lbCyK3It8rsJjwc4MfJ/KpZd3UCLUjaMEDXIFkk
W4P0Rfyb+s2gejUWZT/bD3wwvWhmPY4EIRN9bcOzOGKCWgRhRzpxIYhLb2Ta3UsI
d7pakOKB87LM06QVxUpOuQGlC7k4Ce98zA1/5poxeoAKEY5CpPpSXZKfVqJJHx9O
yOmq+VJB+XyaOnjCOkMfR+r8Lo4JZ3XVS6sShsLkplXhz3c8Dt29k/jpZQA0pfSK
pZTAzqIBedKMYnaGlJhxJmN6MeMCQvHswH5k9JcvHA0H/it2hBnyxIPs4oIOUH91
21k9/pfEImFiVaJZ9/rv6t5wJ57Fi2NeWx4JFRzVaEU7B8wQkbgvVhyy/oO2GRLJ
a6DExmeJHT2IRfx66tpiX5jcjkEu7+5SRPQOS/ZrC/GSVvpGm2L6ggFfEaCHHiwf
WRWkEQYRzl2V18wDgIpavbNiO+fZnaf5kAXc8bdOLmyr4lPiagwkNNtSfyNZEoaq
2/BsCsGDlvgfiLxbmvj/VBtVPr1/6dltQ59T6rZxNks5aY+9J7bVLIN1+F9S2nLH
uecrcGD3KUmFv8dbPyMyv4vPvT32SR5BPp7wAFPCMJ8tHhcQDl5vkdCKpESFKp+B
yRE=
=QlIR
-----END PGP PRIVATE KEY BLOCK-----
Loading

0 comments on commit 2e30356

Please sign in to comment.