• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pgpainless / pgpainless / #1074

20 Jan 2026 11:18PM UTC coverage: 85.293% (+0.09%) from 85.206%
#1074

push

github

vanitasvitae
Bump sop-java to 15.0.0

6774 of 7942 relevant lines covered (85.29%)

0.85 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.0
/pgpainless-sop/src/main/kotlin/org/pgpainless/sop/EncryptImpl.kt
1
// SPDX-FileCopyrightText: 2024 Paul Schaub <vanitasvitae@fsfe.org>
2
//
3
// SPDX-License-Identifier: Apache-2.0
4

5
package org.pgpainless.sop
6

7
import java.io.IOException
8
import java.io.InputStream
9
import java.io.OutputStream
10
import java.util.Date
11
import org.bouncycastle.openpgp.PGPException
12
import org.bouncycastle.openpgp.api.MessageEncryptionMechanism
13
import org.bouncycastle.openpgp.api.OpenPGPKey
14
import org.bouncycastle.util.io.Streams
15
import org.pgpainless.PGPainless
16
import org.pgpainless.algorithm.AEADAlgorithm
17
import org.pgpainless.algorithm.DocumentSignatureType
18
import org.pgpainless.algorithm.KeyFlag
19
import org.pgpainless.algorithm.StreamEncoding
20
import org.pgpainless.algorithm.SymmetricKeyAlgorithm
21
import org.pgpainless.encryption_signing.EncryptionOptions
22
import org.pgpainless.encryption_signing.ProducerOptions
23
import org.pgpainless.encryption_signing.SigningOptions
24
import org.pgpainless.exception.KeyException.UnacceptableEncryptionKeyException
25
import org.pgpainless.exception.KeyException.UnacceptableSigningKeyException
26
import org.pgpainless.exception.WrongPassphraseException
27
import org.pgpainless.util.Passphrase
28
import sop.EncryptionResult
29
import sop.Profile
30
import sop.ReadyWithResult
31
import sop.SessionKey
32
import sop.enums.EncryptAs
33
import sop.enums.EncryptFor
34
import sop.exception.SOPGPException
35
import sop.operation.Encrypt
36

37
/** Implementation of the `encrypt` operation using PGPainless. */
38
class EncryptImpl(private val api: PGPainless) : Encrypt {
1✔
39

40
    companion object {
41
        @JvmField val RFC4880_PROFILE = Profile("rfc4880", "Follow the packet format of rfc4880")
1✔
42
        @JvmField val RFC9580_PROFILE = Profile("rfc9580", "Follow the packet format of rfc9580")
1✔
43

44
        @JvmField
45
        val SUPPORTED_PROFILES =
46
            listOf(
1✔
47
                RFC4880_PROFILE.withAliases("default", "compatibility"),
1✔
48
                RFC9580_PROFILE.withAliases("security", "performance"))
1✔
49
    }
50

51
    private val encryptionOptions = EncryptionOptions.get(api)
1✔
52
    private var signingOptions: SigningOptions? = null
53
    private val signingKeys = mutableListOf<OpenPGPKey>()
1✔
54
    private val protector = MatchMakingSecretKeyRingProtector()
1✔
55

56
    private var profile = RFC4880_PROFILE.name
1✔
57
    private var mode = EncryptAs.binary
1✔
58
    private var purpose: EncryptFor = EncryptFor.any
1✔
59
    private var armor = true
1✔
60

61
    override fun encryptFor(purpose: EncryptFor): Encrypt = apply { this.purpose = purpose }
1✔
62

63
    override fun mode(mode: EncryptAs): Encrypt = apply { this.mode = mode }
1✔
64

65
    override fun noArmor(): Encrypt = apply { this.armor = false }
1✔
66

67
    override fun plaintext(plaintext: InputStream): ReadyWithResult<EncryptionResult> {
68
        if (!encryptionOptions.hasEncryptionMethod()) {
1✔
69
            throw SOPGPException.MissingArg("Missing encryption method.")
1✔
70
        }
71

72
        if (encryptionOptions.usesOnlyPasswordBasedEncryption() &&
1✔
73
            profile == RFC9580_PROFILE.name) {
1✔
74
            encryptionOptions.overrideEncryptionMechanism(
×
75
                MessageEncryptionMechanism.aead(
×
76
                    SymmetricKeyAlgorithm.AES_128.algorithmId, AEADAlgorithm.OCB.algorithmId))
×
77
        }
78

79
        val options =
1✔
80
            if (signingOptions != null) {
1✔
81
                    ProducerOptions.signAndEncrypt(encryptionOptions, signingOptions!!)
1✔
82
                } else {
83
                    ProducerOptions.encrypt(encryptionOptions)
1✔
84
                }
85
                .setAsciiArmor(armor)
1✔
86
                .setEncoding(modeToStreamEncoding(mode))
1✔
87

88
        signingKeys.forEach {
1✔
89
            try {
1✔
90
                signingOptions!!.addInlineSignature(protector, it, modeToSignatureType(mode))
1✔
91
            } catch (e: UnacceptableSigningKeyException) {
×
92
                throw SOPGPException.KeyCannotSign("Key ${it.keyIdentifier} cannot sign", e)
×
93
            } catch (e: WrongPassphraseException) {
1✔
94
                throw SOPGPException.KeyIsProtected("Cannot unlock key ${it.keyIdentifier}", e)
1✔
95
            } catch (e: PGPException) {
×
96
                throw SOPGPException.BadData(e)
×
97
            }
98
        }
1✔
99

100
        try {
1✔
101
            return object : ReadyWithResult<EncryptionResult>() {
1✔
102
                override fun writeTo(outputStream: OutputStream): EncryptionResult {
103
                    val encryptionStream =
1✔
104
                        api.generateMessage().onOutputStream(outputStream).withOptions(options)
1✔
105
                    Streams.pipeAll(plaintext, encryptionStream)
1✔
106
                    encryptionStream.close()
1✔
107
                    return EncryptionResult(
1✔
108
                        encryptionStream.result.sessionKey?.let {
1✔
109
                            SessionKey(it.algorithm.algorithmId.toByte(), it.key)
1✔
110
                        })
111
                }
112
            }
113
        } catch (e: PGPException) {
×
114
            throw IOException(e)
×
115
        }
116
    }
117

118
    override fun profile(profileName: String): Encrypt = apply {
1✔
119
        profile =
1✔
120
            SUPPORTED_PROFILES.find { it.name == profileName || it.aliases.contains(profileName) }
1✔
121
                ?.name
1✔
122
                ?: throw SOPGPException.UnsupportedProfile("encrypt", profileName)
1✔
123
    }
1✔
124

125
    override fun signWith(key: InputStream): Encrypt = apply {
1✔
126
        if (signingOptions == null) {
1✔
127
            signingOptions = SigningOptions.get(api)
1✔
128
        }
129

130
        val signingKey =
1✔
131
            KeyReader(api).readSecretKeys(key, true).singleOrNull()
1✔
132
                ?: throw SOPGPException.BadData(
×
133
                    AssertionError(
×
134
                        "Exactly one secret key at a time expected. Got zero or multiple instead."))
×
135

136
        val info = api.inspect(signingKey)
1✔
137
        if (info.signingSubkeys.isEmpty()) {
1✔
138
            throw SOPGPException.KeyCannotSign("Key ${info.keyIdentifier} cannot sign.")
1✔
139
        }
140

141
        protector.addSecretKey(signingKey)
1✔
142
        signingKeys.add(signingKey)
1✔
143
    }
1✔
144

145
    override fun withCert(cert: InputStream): Encrypt = apply {
1✔
146
        try {
1✔
147
            KeyReader(api).readPublicKeys(cert, true).forEach {
1✔
148
                encryptionOptions.addRecipient(it, keySelectorForPurpose(purpose))
1✔
149
            }
1✔
150
        } catch (e: UnacceptableEncryptionKeyException) {
1✔
151
            throw SOPGPException.CertCannotEncrypt(e.message ?: "Cert cannot encrypt", e)
1✔
152
        } catch (e: IOException) {
×
153
            throw SOPGPException.BadData(e)
×
154
        }
155
    }
1✔
156

157
    override fun withKeyPassword(password: ByteArray): Encrypt = apply {
1✔
158
        PasswordHelper.addPassphrasePlusRemoveWhitespace(password, protector)
1✔
159
    }
1✔
160

161
    override fun withPassword(password: String): Encrypt = apply {
1✔
162
        encryptionOptions.addMessagePassphrase(
1✔
163
            Passphrase.fromPassword(password).withTrimmedWhitespace())
1✔
164
    }
1✔
165

166
    private fun modeToStreamEncoding(mode: EncryptAs): StreamEncoding {
167
        return when (mode) {
1✔
168
            EncryptAs.binary -> StreamEncoding.BINARY
1✔
169
            EncryptAs.text -> StreamEncoding.UTF8
1✔
170
        }
171
    }
172

173
    private fun modeToSignatureType(mode: EncryptAs): DocumentSignatureType {
174
        return when (mode) {
1✔
175
            EncryptAs.binary -> DocumentSignatureType.BINARY_DOCUMENT
1✔
176
            EncryptAs.text -> DocumentSignatureType.CANONICAL_TEXT_DOCUMENT
1✔
177
        }
178
    }
179

180
    private fun keySelectorForPurpose(
181
        purpose: EncryptFor
182
    ): EncryptionOptions.EncryptionKeySelector =
183
        EncryptionOptions.EncryptionKeySelector { encryptionCapableKeys ->
1✔
184
            encryptionCapableKeys.filter {
1✔
185
                when (purpose) {
1✔
186
                    EncryptFor.storage -> it.hasKeyFlags(Date(), KeyFlag.ENCRYPT_STORAGE.flag)
1✔
187
                    EncryptFor.communications -> it.hasKeyFlags(Date(), KeyFlag.ENCRYPT_COMMS.flag)
1✔
188
                    else -> true
1✔
189
                }
190
            }
191
        }
1✔
192
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc