Skip to content

Commit 40087a6

Browse files
committed
feat(amazonq): hook up lsp payload encryption
Certain LSP messages require AES-256-GCM encryption packaged as JWE
1 parent 9256837 commit 40087a6

File tree

4 files changed

+92
-2
lines changed

4 files changed

+92
-2
lines changed

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/AmazonQLspService.kt

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import org.slf4j.event.Level
2828
import software.aws.toolkits.core.utils.getLogger
2929
import software.aws.toolkits.core.utils.warn
3030
import software.aws.toolkits.jetbrains.isDeveloperMode
31+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption.JwtEncryptionManager
3132
import java.io.IOException
3233
import java.io.OutputStreamWriter
3334
import java.io.PipedInputStream
@@ -98,6 +99,8 @@ class AmazonQLspService(private val project: Project, private val cs: CoroutineS
9899
}
99100

100101
private class AmazonQServerInstance(private val project: Project, private val cs: CoroutineScope) : Disposable {
102+
private val encryptionManager = JwtEncryptionManager()
103+
101104
private val launcher: Launcher<AmazonQLanguageServer>
102105

103106
private val languageServer: AmazonQLanguageServer
@@ -108,7 +111,11 @@ private class AmazonQServerInstance(private val project: Project, private val cs
108111
private val launcherHandler: KillableProcessHandler
109112

110113
init {
111-
val cmd = GeneralCommandLine("amazon-q-lsp")
114+
val cmd = GeneralCommandLine(
115+
"amazon-q-lsp",
116+
"--stdio",
117+
"--set-credentials-encryption-key",
118+
)
112119

113120
launcherHandler = KillableColoredProcessHandler.Silent(cmd)
114121
val inputWrapper = LSPProcessListener()
@@ -143,6 +150,9 @@ private class AmazonQServerInstance(private val project: Project, private val cs
143150
launcherFuture = launcher.startListening()
144151

145152
cs.launch {
153+
// encryption info must be sent within 5s or Flare process will exit
154+
encryptionManager.writeInitializationPayload(launcherHandler.process.outputStream)
155+
146156
val initializeResult = languageServer.initialize(
147157
InitializeParams().apply {
148158
// does this work on windows
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption
5+
6+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
7+
import com.nimbusds.jose.EncryptionMethod
8+
import com.nimbusds.jose.JWEAlgorithm
9+
import com.nimbusds.jose.JWEHeader
10+
import com.nimbusds.jose.JWEObject
11+
import com.nimbusds.jose.Payload
12+
import com.nimbusds.jose.crypto.DirectDecrypter
13+
import com.nimbusds.jose.crypto.DirectEncrypter
14+
import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.EncryptionInitializationRequest
15+
import java.io.OutputStream
16+
import java.security.SecureRandom
17+
import java.util.Base64
18+
import javax.crypto.SecretKey
19+
import javax.crypto.spec.SecretKeySpec
20+
21+
class JwtEncryptionManager(private val key: SecretKey) {
22+
constructor() : this(generateHmacKey())
23+
24+
private val mapper = jacksonObjectMapper()
25+
26+
fun writeInitializationPayload(os: OutputStream) {
27+
val payload = EncryptionInitializationRequest(
28+
EncryptionInitializationRequest.Version.V1_0,
29+
EncryptionInitializationRequest.Mode.JWT,
30+
Base64.getUrlEncoder().withoutPadding().encodeToString(key.encoded)
31+
)
32+
33+
// write directly to stream because utils are closing the underlying stream
34+
os.write("${mapper.writeValueAsString(payload)}\n".toByteArray())
35+
}
36+
37+
fun encrypt(data: Any): String {
38+
val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM)
39+
val payload = Payload(mapper.writeValueAsBytes(data))
40+
val jweObject = JWEObject(header, payload)
41+
jweObject.encrypt(DirectEncrypter(key))
42+
43+
return jweObject.serialize()
44+
}
45+
46+
fun decrypt(jwt: String): String {
47+
val jweObject = JWEObject.parse(jwt)
48+
jweObject.decrypt(DirectDecrypter(key))
49+
50+
return jweObject.payload.toString()
51+
}
52+
53+
private companion object {
54+
private fun generateHmacKey(): SecretKey {
55+
val keyBytes = ByteArray(32)
56+
SecureRandom().nextBytes(keyBytes)
57+
return SecretKeySpec(keyBytes, "HmacSHA256")
58+
}
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package software.aws.toolkits.jetbrains.services.amazonq.lsp.model
5+
6+
import com.fasterxml.jackson.annotation.JsonValue
7+
8+
data class EncryptionInitializationRequest(
9+
val version: Version,
10+
val mode: Mode,
11+
val key: String,
12+
) {
13+
enum class Version(@JsonValue val value: String) {
14+
V1_0("1.0"),
15+
}
16+
17+
enum class Mode(@JsonValue val value: String) {
18+
JWT("JWT"),
19+
}
20+
}

plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/aws/credentials/UpdateCredentialsPayload.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ package software.aws.toolkits.jetbrains.services.amazonq.lsp.model.aws.credentia
55

66
data class UpdateCredentialsPayload(
77
val data: String,
8-
val encrypted: String,
8+
val encrypted: Boolean,
99
)
1010

1111
data class UpdateCredentialsPayloadData(

0 commit comments

Comments
 (0)