Feature Request: Support decryption of recipient-less messages
Closed this issue · 5 comments
Thank you for this library!
I'm adding this issue in the hopes that I can submit a PR to address it, but the workflow I would like to have is this:
- You encrypt a message using
gpg -R
on the command line i.e.
❯ cat test.txt
foobarfoobarfoobar
❯ gpg -e -a -R 'Connor James Smith <cjs.connor.smith@gmail.com>' -o test3.txt.pgp test.txt
File 'test3.txt.pgp' exists. Overwrite? (y/N) y
❯ cat test3.txt.pgp
-----BEGIN PGP MESSAGE-----
hQIMAwAAAAAAAAAAAQ/9HgFExBedlOndKW8bOQ9m4uiNqJnm7UNNCe1IQpJoytHN
FYO6IFXbHCnQVS5EcYJVQO4n9tglqWAT8hwOjAGKwb1XxltY3y62dMRSi+CyPeR+
6dMw/taAHQmw+2xv25KRWEH3xPjTsvI3MVT92PANRsHLRs2L2TOIFUoOBwe7Pgzj
NafR2/R1sIpbDvDnvpJHdwLZrUiyjvug8jxs7Oto8TRv1u8tzgNg8Z6WD9myqM+N
HQWxvMOPJ/iSn5pelD7722eHeL/3JSwtJF30Te+H7809wPw9i9Xaeo5p3SHfLXdu
egr5xGrr5aoRHfQwkhbhAYOrX63N/FUsCqCfr2e2XcUKSpxwXLz+J9L+94Es02KX
tL+FZXDSB/sBV5qmanfVs+XGB+rZQ7VjS+PcxLc0I3EuLQr2hgJEsRd5DYNjArH1
2072S9eF1Kmq/uQE231KUSqv1UUDm3oX8Lg53Pt1f/F07go2QOFdmHjONSf5VUcb
2uAOyHS05iLnj/Uciqu1QQyvJz8YF5FCWlpCXZSsDfXqx7esEZWvztkAkwQa3E/n
VRc/UkwT31axtrYXp1KAbrx0Hg61Cqdejn5K2hnNz4JUlcNCwYRSPPbwmbKcYB/k
v8xeUKgx4DS/RzJFZUH/SIclCi1MvJo6ILiFKoVz2GU570b6R4XNWE9bREm7GvDS
TAFzoBx/OP/AICMaKbyOT0rHZmgvLbZsqo496q7dSsC3Cn6HvLG5xqvxt4afon+I
PUrwTuf/XJN2/lFEk1yTpk9ebFDDSECWDts6jtc=
=IeYl
-----END PGP MESSAGE-----
- Inside a unit test, be able to decrypt with hard-coded private key
test("decrypt works") {
val input = "foobarfoobarfoobar"
val encryptedInput: String = ??? //omitted
val privateKeyString: String = ??? //omitted
implicit val logger: Logger[IO] = Slf4jLogger.getLogger
for {
resultString <- PGPKeyAlg[IO].readPrivateKey(privateKeyString).attempt.flatMap {
case Right(k) if k.getKeyID > 0 =>
(for {
crypto <- Stream.resource(CryptoAlg[IO])
output <- Stream
.emits(encryptedInput.getBytes())
.through(crypto.decrypt(k))
.through(utf8.decode)
.attempt
} yield output).compile.foldMonoid
case Right(_) => new Throwable("shouldn't happen!").asLeft[String].pure[IO]
case Left(e) => e.asLeft[String].pure[IO]
}
} yield {
resultString match {
case Right(s) => assertEquals(s, input)
case Left(e) => fail(s"should never happen: ${e.getMessage}")
}
}
}
What is happening when I try this right now is this:
should never happen: Cannot decrypt message with key 0 because it requires key <key omitted>
Thanks again.
I think I was able to reproduce this with the private key that's part of the test suite of this library.
$ export GNUPGHOME="$(mktemp -d)/.gnupg"
$ mkdir -m 0700 -p "${GNUPGHOME}"
$ pbpaste | gpg --import
gpg: invalid armor header: lQVYBGDozbkBDACt63OSGFh4pVHSpVcPZXot2ZcHepkPXSJOFE+PnLOAvcMK8O9s\n
gpg: /var/folders/zl/t99c6ptn4p78vg6l6f24xwjc0000gq/T/tmp.b1L1ozoY/.gnupg/trustdb.gpg: trustdb created
gpg: key 52AFF6B5A43D6EBB: public key "key 1 <key1@dwolla.com>" imported
gpg: key 52AFF6B5A43D6EBB: secret key imported
gpg: Total number processed: 1
gpg: imported: 1
gpg: secret keys read: 1
gpg: secret keys imported: 1
$ echo "Hello World" | gpg --encrypt --armor --hidden-recipient "key 1 <key1@dwolla.com>"
gpg: 54E4844CFE7C68E1: There is no assurance this key belongs to the named user
sub rsa3072/54E4844CFE7C68E1 2021-07-09 key 1 <key1@dwolla.com>
Primary key fingerprint: 7EEE F61D E397 A6B1 350B 6F93 52AF F6B5 A43D 6EBB
Subkey fingerprint: C4BD 9D01 85E9 B333 4D8D D55D 54E4 844C FE7C 68E1
It is NOT certain that the key belongs to the person named
in the user ID. If you *really* know what you are doing,
you may answer the next question with yes.
Use this key anyway? (y/N) y
-----BEGIN PGP MESSAGE-----
hQGMAwAAAAAAAAAAAQv/QQ8RnRGF6jaPTUpuBoPollIBvPIzqokTGzuTaVD4bKsg
GGt4ooPpcTkxn0MRLs3rNJfZjaULSkWtUxc4NsSbqmrl8g3smnwJWk/UIR097zlC
s30/o3WlmSodAGbEuP5Y+mbAErwGbCs1e7cn1LqQO3BrSZ3m7djif9fiWRdb3AZ4
YPX5dmmOZLZoNQO5zLNu3iolrTXyimQLcS7VoFQ+Nbj9hOS+vDzcg6Kycaky7U+M
arfyyaqWan8hVygDthMT+n3Au0l7lBzN99aZmC13OP2fhuBBXvrGF+njFS+RkEOs
LToMlpFVYWlEFSnYlIQjsxKBzMKThNudKM7r4Kc1yw88DQ9C/rWZxmMxTyLAA4C7
QZKdO+zYfzSCYq3bO+YdN8vUGZPS63YN8Pp6qWvIXOZ3oecxmidqjGsItLpxJ0KK
zJ0IWVsQj1Zc/2zSojw8edcMh86PFbQsC4aPpMK54KiU4YKXcUnaDeQ48BGv27Po
Qx9PqZi+1ROo+anCVWrn0kcB/+g0TzSpG+nwMI4gxNTTAuybzEscK2ifkA76Df45
cSypFoj4OIRtTZ8iSGhfgt0fCn1qUrEs7Vw+iNYSqpl9/ue3u1icCQ==
=kHjl
-----END PGP MESSAGE-----
Then I added a test to CryptoAlgSpec
:
test("CryptoAlg should decrypt a fixed value encrypted with our test key") {
val crypto = resource()
val message =
"""-----BEGIN PGP MESSAGE-----
|
|hQGMAwAAAAAAAAAAAQv/QQ8RnRGF6jaPTUpuBoPollIBvPIzqokTGzuTaVD4bKsg
|GGt4ooPpcTkxn0MRLs3rNJfZjaULSkWtUxc4NsSbqmrl8g3smnwJWk/UIR097zlC
|s30/o3WlmSodAGbEuP5Y+mbAErwGbCs1e7cn1LqQO3BrSZ3m7djif9fiWRdb3AZ4
|YPX5dmmOZLZoNQO5zLNu3iolrTXyimQLcS7VoFQ+Nbj9hOS+vDzcg6Kycaky7U+M
|arfyyaqWan8hVygDthMT+n3Au0l7lBzN99aZmC13OP2fhuBBXvrGF+njFS+RkEOs
|LToMlpFVYWlEFSnYlIQjsxKBzMKThNudKM7r4Kc1yw88DQ9C/rWZxmMxTyLAA4C7
|QZKdO+zYfzSCYq3bO+YdN8vUGZPS63YN8Pp6qWvIXOZ3oecxmidqjGsItLpxJ0KK
|zJ0IWVsQj1Zc/2zSojw8edcMh86PFbQsC4aPpMK54KiU4YKXcUnaDeQ48BGv27Po
|Qx9PqZi+1ROo+anCVWrn0kcB/+g0TzSpG+nwMI4gxNTTAuybzEscK2ifkA76Df45
|cSypFoj4OIRtTZ8iSGhfgt0fCn1qUrEs7Vw+iNYSqpl9/ue3u1icCQ==
|=kHjl
|-----END PGP MESSAGE-----
|""".stripMargin
for {
key <- PGPKeyAlg[IO].readSecretKeyCollection(TestKey())
text <- Stream
.emit(message)
.through(text.utf8.encode)
.through(crypto.decrypt(key))
.through(text.utf8.decode)
.compile
.lastOrError
} yield {
assertEquals(text, "Hello World")
}
}
which fails with
com.dwolla.security.crypto.KeyRingMissingKeyException: Cannot decrypt message with the passed keyring because it requires key 0, but the ring does not contain that key
If I change it to use PGPKeyAlg[IO].readPrivateKey(TestKey())
instead of readSecretKeyCollection
, it still fails, but with
com.dwolla.security.crypto.KeyMismatchException: Cannot decrypt message with key 0 because it requires key 5958252092039458491
Does that seem like a correct reproduction, @CJSmith-0141?
Yes, exactly. Reading through RFC 4880 It's not clear to me if this is compliant with OpenPGP or is a GPG extension.
EDIT: What I mean by saying that is if you tag this "will-not-do" I would totally understand.
@CJSmith-0141 could you try the code in the hidden-recipients
branch (what's in #77) and let me know if it works for you? If so, we should be able to merge it and cut a new release.
hello @bpholt sorry for the delay. I was able to build this branch, publishLocal and switch my project to use the snapshot version.
I have a unit test that is set up to fail in the case where this feature is implemented, and it failed. Which means the feature works!
test("FUTURE FEATURE: decrypt does not work with gpg encrypted message, anonymous recipient") {
val input = "foobarfoobarfoobar\n"
val encryptedInput = ??? //omitted for brevity
(for {
resultString <- PgpService.decryptString[IO](unitTestOnlyPrivateKeyString, encryptedInput)
} yield resultString).unsafeRunSync() match {
case Right(s) =>
fail(
s"Decryption with anonymous recipient worked, unexpectedly.\n" +
s"Result String: ${s}\n" +
s"input: ${input}"
)
case Left(e) =>
assert(
e.getMessage.contains(
"Cannot decrypt message with the passed keyring because it requires key 0"
)
)
}
}
and the result:
[info] - FUTURE FEATURE: decrypt does not work with gpg encrypted message, anonymous recipient *** FAILED ***
[info] Decryption with anonymous recipient worked, unexpectedly.
[info] Result String: foobarfoobarfoobar
[info]
[info] input: foobarfoobarfoobar (PgpServiceSpec.scala:560)
Thank you for implementing this!
Excellent! I'll get the PR merged and cut a release!