-
Notifications
You must be signed in to change notification settings - Fork 273
feat(amazonq): hook up lsp payload encryption #5370
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption | ||
|
|
||
| import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper | ||
| import com.nimbusds.jose.EncryptionMethod | ||
| import com.nimbusds.jose.JWEAlgorithm | ||
| import com.nimbusds.jose.JWEHeader | ||
| import com.nimbusds.jose.JWEObject | ||
| import com.nimbusds.jose.Payload | ||
| import com.nimbusds.jose.crypto.DirectDecrypter | ||
| import com.nimbusds.jose.crypto.DirectEncrypter | ||
| import software.aws.toolkits.jetbrains.services.amazonq.lsp.model.EncryptionInitializationRequest | ||
| import java.io.OutputStream | ||
| import java.security.SecureRandom | ||
| import java.util.Base64 | ||
| import javax.crypto.SecretKey | ||
| import javax.crypto.spec.SecretKeySpec | ||
|
|
||
| class JwtEncryptionManager(private val key: SecretKey) { | ||
| constructor() : this(generateHmacKey()) | ||
|
Check warning on line 22 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
|
|
||
| private val mapper = jacksonObjectMapper() | ||
|
Check warning on line 24 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
|
|
||
| fun writeInitializationPayload(os: OutputStream) { | ||
| val payload = EncryptionInitializationRequest( | ||
| EncryptionInitializationRequest.Version.V1_0, | ||
| EncryptionInitializationRequest.Mode.JWT, | ||
| Base64.getUrlEncoder().withoutPadding().encodeToString(key.encoded) | ||
|
Check warning on line 30 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| ) | ||
|
|
||
| // write directly to stream because utils are closing the underlying stream | ||
| os.write("${mapper.writeValueAsString(payload)}\n".toByteArray()) | ||
| } | ||
|
Check warning on line 35 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
|
|
||
| fun encrypt(data: Any): String { | ||
| val header = JWEHeader(JWEAlgorithm.DIR, EncryptionMethod.A256GCM) | ||
|
Check warning on line 38 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| val payload = if (data is String) { | ||
| Payload(data) | ||
|
Check warning on line 40 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| } else { | ||
| Payload(mapper.writeValueAsBytes(data)) | ||
|
Check warning on line 42 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| } | ||
|
|
||
| val jweObject = JWEObject(header, payload) | ||
| jweObject.encrypt(DirectEncrypter(key)) | ||
|
Check warning on line 46 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
|
|
||
| return jweObject.serialize() | ||
|
Check warning on line 48 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| } | ||
|
|
||
| fun decrypt(jwt: String): String { | ||
Check warningCode scanning / QDJVMC Unused symbol Warning
Function "decrypt" is never used
|
||
| val jweObject = JWEObject.parse(jwt) | ||
| jweObject.decrypt(DirectDecrypter(key)) | ||
|
Check warning on line 53 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
|
|
||
| return jweObject.payload.toString() | ||
|
Check warning on line 55 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| } | ||
|
|
||
| private companion object { | ||
| private fun generateHmacKey(): SecretKey { | ||
| val keyBytes = ByteArray(32) | ||
| SecureRandom().nextBytes(keyBytes) | ||
| return SecretKeySpec(keyBytes, "HmacSHA256") | ||
|
Check warning on line 62 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/encryption/JwtEncryptionManager.kt
|
||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp.model | ||
|
|
||
| import com.fasterxml.jackson.annotation.JsonValue | ||
|
|
||
| data class EncryptionInitializationRequest( | ||
| val version: Version, | ||
| val mode: Mode, | ||
| val key: String, | ||
|
Check warning on line 11 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EncryptionInitializationRequest.kt
|
||
| ) { | ||
| enum class Version(@JsonValue val value: String) { | ||
| V1_0("1.0"), | ||
| } | ||
|
Check warning on line 15 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EncryptionInitializationRequest.kt
|
||
|
|
||
| enum class Mode(@JsonValue val value: String) { | ||
| JWT("JWT"), | ||
| } | ||
|
Check warning on line 19 in plugins/amazonq/shared/jetbrains-community/src/software/aws/toolkits/jetbrains/services/amazonq/lsp/model/EncryptionInitializationRequest.kt
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| // Copyright 2025 Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| // SPDX-License-Identifier: Apache-2.0 | ||
|
|
||
| package software.aws.toolkits.jetbrains.services.amazonq.lsp.encryption | ||
|
|
||
| import org.assertj.core.api.Assertions.assertThat | ||
| import org.junit.jupiter.api.Test | ||
| import java.io.ByteArrayOutputStream | ||
| import javax.crypto.spec.SecretKeySpec | ||
| import kotlin.random.Random | ||
|
|
||
| class JwtEncryptionManagerTest { | ||
| @Test | ||
| fun `uses a different encryption key for each instance`() { | ||
| val blob = Random.Default.nextBytes(256) | ||
| assertThat(JwtEncryptionManager().encrypt(blob)) | ||
| .isNotEqualTo(JwtEncryptionManager().encrypt(blob)) | ||
| } | ||
|
|
||
| @Test | ||
| @OptIn(ExperimentalStdlibApi::class) | ||
| fun `encryption is stable with static key`() { | ||
| val blob = Random.Default.nextBytes(256) | ||
| val bytes = "DEADBEEF".repeat(8).hexToByteArray() // 32 bytes | ||
| val key = SecretKeySpec(bytes, "HmacSHA256") | ||
| assertThat(JwtEncryptionManager(key).encrypt(blob)) | ||
| .isNotEqualTo(JwtEncryptionManager(key).encrypt(blob)) | ||
| } | ||
|
|
||
| @Test | ||
| fun `encryption can be round-tripped`() { | ||
| val sut = JwtEncryptionManager() | ||
| val blob = "DEADBEEF".repeat(8) | ||
| assertThat(sut.decrypt(sut.encrypt(blob))).isEqualTo(blob) | ||
| } | ||
|
|
||
| @Test | ||
| @OptIn(ExperimentalStdlibApi::class) | ||
| fun writeInitializationPayload() { | ||
| val bytes = "DEADBEEF".repeat(8).hexToByteArray() // 32 bytes | ||
| val key = SecretKeySpec(bytes, "HmacSHA256") | ||
|
|
||
| val os = ByteArrayOutputStream() | ||
| JwtEncryptionManager(key).writeInitializationPayload(os) | ||
| assertThat(os.toString()) | ||
| // Flare requires encryption ends with new line | ||
| // https://github.com/aws/language-server-runtimes/blob/4d7f81295dc12b59ed2e1c0ebaedb85ccb86cf76/runtimes/README.md#encryption | ||
| .endsWith("\n") | ||
| // language=JSON | ||
| .isEqualTo( | ||
| """ | ||
| |{"version":"1.0","mode":"JWT","key":"3q2-796tvu_erb7v3q2-796tvu_erb7v3q2-796tvu8"} | ||
| | | ||
| """.trimMargin() | ||
| ) | ||
| } | ||
| } |
Check warning
Code scanning / QDJVMC
Unused symbol Warning