diff --git a/README.md b/README.md index 7c1f304..1672a26 100644 --- a/README.md +++ b/README.md @@ -19,6 +19,7 @@ Additionally it facilitates several well-known attacks against JWT implementatio **Unreleased** - Add ability to test for HMAC signatures using [weak secrets](https://github.com/wallarm/jwt-secrets). +- Add import capability for JWK data. - Remember last used key within Signing dialog. **2.4 2024-12-24** diff --git a/src/main/java/com/blackberry/jwteditor/model/keys/JWKSetParser.java b/src/main/java/com/blackberry/jwteditor/model/keys/JWKSetParser.java new file mode 100644 index 0000000..fb4caa5 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/model/keys/JWKSetParser.java @@ -0,0 +1,44 @@ +/* +Author : Dolph Flynn + +Copyright 2025 Dolph Flynn + +Licensed 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. +*/ + +package com.blackberry.jwteditor.model.keys; + +import com.blackberry.jwteditor.exceptions.UnsupportedKeyException; +import com.nimbusds.jose.jwk.JWKSet; + +import java.text.ParseException; +import java.util.List; +import java.util.Objects; + +public class JWKSetParser { + public List parse(String json) throws ParseException { + return JWKSet.parse(json) + .getKeys() + .stream() + .map(jwk -> { + try { + return JWKKeyFactory.from(jwk); + } catch (UnsupportedKeyException e) { + return null; + } + }) + .filter(Objects::nonNull) + .map(key -> (Key) key) + .toList(); + } +} diff --git a/src/main/java/com/blackberry/jwteditor/presenter/KeysPresenter.java b/src/main/java/com/blackberry/jwteditor/presenter/KeysPresenter.java index 2dd8c66..a5e672e 100644 --- a/src/main/java/com/blackberry/jwteditor/presenter/KeysPresenter.java +++ b/src/main/java/com/blackberry/jwteditor/presenter/KeysPresenter.java @@ -24,9 +24,11 @@ import com.blackberry.jwteditor.model.persistence.KeysModelPersistence; import com.blackberry.jwteditor.utils.PEMUtils; import com.blackberry.jwteditor.utils.Utils; +import com.blackberry.jwteditor.view.dialog.jwks.JWKSImportDialog; import com.blackberry.jwteditor.view.dialog.keys.KeyDialog; import com.blackberry.jwteditor.view.dialog.keys.KeysDialogFactory; import com.blackberry.jwteditor.view.keys.KeysView; +import com.blackberry.jwteditor.view.rsta.RstaFactory; import com.nimbusds.jose.jwk.JWK; import javax.swing.*; @@ -44,14 +46,17 @@ public class KeysPresenter { private final KeysModel model; private final KeysView view; private final KeysDialogFactory keysDialogFactory; + private final RstaFactory rstaFactory; public KeysPresenter(KeysView view, KeysModelPersistence keysModelPersistence, KeysModel keysModel, - KeysDialogFactory keysDialogFactory) { + KeysDialogFactory keysDialogFactory, + RstaFactory rstaFactory) { this.view = view; this.model = keysModel; this.keysDialogFactory = keysDialogFactory; + this.rstaFactory = rstaFactory; model.addKeyModelListener(new KeysModelListener() { @Override @@ -142,6 +147,13 @@ public void onButtonNewPasswordClick() { onButtonNewClicked(keysDialogFactory.passwordDialog()); } + public void onButtonImportJWKSet() { + JWKSImportDialog dialog = new JWKSImportDialog(view.getParent(), model, rstaFactory); + dialog.display(); + + dialog.getKeys().forEach(model::addKey); + } + /** * Can the key at a position in the model be copied as a JWK with private key * diff --git a/src/main/java/com/blackberry/jwteditor/view/dialog/jwks/JWKSImportDialog.form b/src/main/java/com/blackberry/jwteditor/view/dialog/jwks/JWKSImportDialog.form new file mode 100644 index 0000000..ce1a0d2 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/dialog/jwks/JWKSImportDialog.form @@ -0,0 +1,117 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/blackberry/jwteditor/view/dialog/jwks/JWKSImportDialog.java b/src/main/java/com/blackberry/jwteditor/view/dialog/jwks/JWKSImportDialog.java new file mode 100644 index 0000000..f687008 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/dialog/jwks/JWKSImportDialog.java @@ -0,0 +1,147 @@ +/* +Author : Dolph Flynn + +Copyright 2025 Dolph Flynn + +Licensed 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. +*/ + +package com.blackberry.jwteditor.view.dialog.jwks; + +import com.blackberry.jwteditor.model.keys.JWKSetParser; +import com.blackberry.jwteditor.model.keys.Key; +import com.blackberry.jwteditor.model.keys.KeysModel; +import com.blackberry.jwteditor.utils.Utils; +import com.blackberry.jwteditor.view.rsta.RstaFactory; +import com.blackberry.jwteditor.view.utils.DebouncingDocumentAdapter; +import org.fife.ui.rsyntaxtextarea.RSyntaxTextArea; + +import javax.swing.*; +import javax.swing.event.DocumentListener; +import java.awt.*; +import java.awt.event.WindowAdapter; +import java.awt.event.WindowEvent; +import java.text.ParseException; +import java.util.LinkedList; +import java.util.List; + +import static java.awt.Color.PINK; +import static java.awt.Dialog.ModalityType.APPLICATION_MODAL; +import static java.awt.event.KeyEvent.VK_ESCAPE; +import static javax.swing.JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT; +import static javax.swing.JOptionPane.*; + +public class JWKSImportDialog extends JDialog { + private final KeysModel keysModel; + private final RstaFactory rstaFactory; + private final List keys; + private final Color textAreaKeyInitialBackgroundColor; + private final Color textAreaKeyInitialCurrentLineHighlightColor; + + private JPanel contentPane; + private JButton buttonImport; + private JButton buttonCancel; + private RSyntaxTextArea textAreaKeysJson; + private JLabel labelError; + + public JWKSImportDialog(Window parent, KeysModel keysModel, RstaFactory rstaFactory) { + super(parent, "Import JWKs", APPLICATION_MODAL); + + this.keysModel = keysModel; + this.rstaFactory = rstaFactory; + this.keys = new LinkedList<>(); + + setDefaultCloseOperation(DO_NOTHING_ON_CLOSE); + addWindowListener(new WindowAdapter() { + public void windowClosing(WindowEvent e) { + onCancel(); + } + }); + + setContentPane(contentPane); + getRootPane().setDefaultButton(buttonImport); + + buttonImport.addActionListener(e -> onImport()); + buttonCancel.addActionListener(e -> onCancel()); + + contentPane.registerKeyboardAction( + e -> onCancel(), + KeyStroke.getKeyStroke(VK_ESCAPE, 0), + WHEN_ANCESTOR_OF_FOCUSED_COMPONENT + ); + + DocumentListener documentListener = new DebouncingDocumentAdapter(e -> parseJson()); + textAreaKeysJson.getDocument().addDocumentListener(documentListener); + + textAreaKeyInitialBackgroundColor = textAreaKeysJson.getBackground(); + textAreaKeyInitialCurrentLineHighlightColor = textAreaKeysJson.getCurrentLineHighlightColor(); + } + + private void parseJson() { + textAreaKeysJson.setBackground(textAreaKeyInitialBackgroundColor); + textAreaKeysJson.setCurrentLineHighlightColor(textAreaKeyInitialCurrentLineHighlightColor); + buttonImport.setEnabled(false); + labelError.setText(" "); + keys.clear(); + + if (!textAreaKeysJson.getText().isEmpty()) { + try { + List parsedKeys = new JWKSetParser().parse(textAreaKeysJson.getText()); + keys.addAll(parsedKeys); + buttonImport.setEnabled(true); + } catch (ParseException e) { + textAreaKeysJson.setBackground(PINK); + textAreaKeysJson.setCurrentLineHighlightColor(PINK); + labelError.setText(Utils.getResourceString("error_invalid_keys")); + } + } + } + + public List getKeys() { + return keys; + } + + public void display() { + pack(); + setLocationRelativeTo(getOwner()); + setVisible(true); + } + + void onImport() { + boolean keyIdClash = keys.stream() + .filter(key -> key.getID() != null) + .anyMatch(key -> keysModel.keyExists(key.getID())); + + // Handle overwrites if a key already exists with the same kid + if (keyIdClash) { + if (showConfirmDialog( + this, + Utils.getResourceString("keys_confirm_overwrite"), + Utils.getResourceString("keys_confirm_overwrite_title"), + OK_CANCEL_OPTION) != OK_OPTION) { + keys.clear(); + } + } + + dispose(); + } + + private void onCancel() { + keys.clear(); + dispose(); + } + + private void createUIComponents() { + textAreaKeysJson = rstaFactory.buildDefaultTextArea(); + } +} diff --git a/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.form b/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.form index 27197f0..4772bd6 100644 --- a/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.form +++ b/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.form @@ -8,7 +8,7 @@ - + @@ -16,10 +16,10 @@ - + - + @@ -69,6 +69,14 @@ + + + + + + + + diff --git a/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.java b/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.java index d464103..51d0d9b 100644 --- a/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.java +++ b/src/main/java/com/blackberry/jwteditor/view/keys/KeysView.java @@ -54,6 +54,7 @@ public class KeysView { private JPanel panel; private JButton buttonNewOKP; private JTable tableKeys; + private JButton buttonImportJWK; private JMenuItem menuItemDelete; private JMenuItem menuItemCopyJWK; @@ -81,7 +82,8 @@ public KeysView( keysModel, rstaFactory, parent - ) + ), + rstaFactory ); // Attach event handlers for button clicks @@ -90,6 +92,7 @@ public KeysView( buttonNewOKP.addActionListener(e -> presenter.onButtonNewOKPClick()); buttonNewRSA.addActionListener(e -> presenter.onButtonNewRSAClick()); buttonNewPassword.addActionListener(e -> presenter.onButtonNewPasswordClick()); + buttonImportJWK.addActionListener(e -> presenter.onButtonImportJWKSet()); } /** diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index cdb14a2..2bc77f5 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -86,6 +86,7 @@ encryption_label_key = Encryption Key encryption_label_kek = Key Encryption Algorithm encryption_label_cek = Content Encryption Algorithm error_invalid_key=Invalid Key +error_invalid_keys=Invalid Keys error_missing_kid=Key is missing Key ID error_invalid_key_type=Invalid Key Type error_key_generation=Unable To Generate Key @@ -125,4 +126,5 @@ empty_key_signing_dialog_title=Empty Key Signing Dialog empty_key_signing_algorithm=Algorithm psychic_signature_signing_dialog_title=Psychic Signature Signing Dialog psychic_signature_signing_algorithm=Algorithm -embed_collaborator_payload_location=Header Location \ No newline at end of file +embed_collaborator_payload_location=Header Location +import=Import \ No newline at end of file diff --git a/src/test/java/com/blackberry/jwteditor/model/keys/JWKSetParserTest.java b/src/test/java/com/blackberry/jwteditor/model/keys/JWKSetParserTest.java new file mode 100644 index 0000000..35cb5ae --- /dev/null +++ b/src/test/java/com/blackberry/jwteditor/model/keys/JWKSetParserTest.java @@ -0,0 +1,147 @@ +/* +Author : Dolph Flynn + +Copyright 2025 Dolph Flynn + +Licensed 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. +*/ + +package com.blackberry.jwteditor.model.keys; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import java.text.ParseException; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class JWKSetParserTest { + private final JWKSetParser parser = new JWKSetParser(); + + @ValueSource(strings = { + "'", + "{", + "[" + }) + @ParameterizedTest + void givenInvalidJson_whenParsed_thenParseExceptionThrown(String json) { + assertThrows(ParseException.class, () -> parser.parse(json)); + } + + @ValueSource(strings = { + "null", + "3", + "'jwt'", + "\"jwt\"", + "[]" + }) + @ParameterizedTest + void givenNonObjectJson_whenParsed_thenParseExceptionThrown(String json) { + assertThrows(ParseException.class, () -> parser.parse(json)); + } + + @ValueSource(strings = { + "{}", + "{ 'jwt':[] }", + }) + @ParameterizedTest + void givenJsonObjectWithoutKeys_whenParsed_thenParseExceptionThrown(String json) { + assertThrows(ParseException.class, () -> parser.parse(json)); + } + + @Test + void givenJsonObjectWithEmptyKeysList_whenParsed_thenEmptyListReturned() throws ParseException { + String json = """ + { "keys" : [] } + """; + + assertThat(parser.parse(json)).isEmpty(); + } + + @Test + void givenJsonObjectWithAsymmetricKeys_whenParsed_thenKeysReturned() throws ParseException { + String json = """ + {"keys": + [ + {"kty":"EC", + "crv":"P-256", + "x":"MKBCTNIcKUSDii11ySs3526iDZ8AiTo7Tu6KPAqv7D4", + "y":"4Etl6SRW2YiLUrN5vfvVHuhp7x8PxltmWWlbbM4IFyM", + "d":"870MB6gfuTJ4HtUnUvYMyJpr5eUZNP4Bk43bVdj3eAE", + "use":"enc", + "kid":"1"}, + + {"kty":"RSA", + "n":"0vx7agoebGcQSuuPiLJXZptN9nndrQmbXEps2aiAFbWhM78LhWx4jcbbfAAtVT86zwu1RK7aPFFxuhDR1L6tSoc_BJECPebWKRXjBZCiFV4n3oknjhMstn64tZ_2W-5JsGY4Hc5n9yBXArwl93lqt7_RN5w6Cf0h4QyQ5v-65YGjQR0_FDW2QvzqY368QQMicAtaSqzs8KJZgnYb9c7d0zgdAZHzu6qMQvRL5hajrn1n91CbOpbISD08qNLyrdkt-bFTWhAI4vMQFh6WeZu0fM4lFd2NcRwr3XPksINHaQ-G_xBniIqbw0Ls1jF44-csFCur-kEgU8awapJzKnqDKgw", + "e":"AQAB", + "d":"X4cTteJY_gn4FYPsXB8rdXix5vwsg1FLN5E3EaG6RJoVH-HLLKD9M7dx5oo7GURknchnrRweUkC7hT5fJLM0WbFAKNLWY2vv7B6NqXSzUvxT0_YSfqijwp3RTzlBaCxWp4doFk5N2o8Gy_nHNKroADIkJ46pRUohsXywbReAdYaMwFs9tv8d_cPVY3i07a3t8MN6TNwm0dSawm9v47UiCl3Sk5ZiG7xojPLu4sbg1U2jx4IBTNBznbJSzFHK66jT8bgkuqsk0GjskDJk19Z4qwjwbsnn4j2WBii3RL-Us2lGVkY8fkFzme1z0HbIkfz0Y6mqnOYtqc0X4jfcKoAC8Q", + "p":"83i-7IvMGXoMXCskv73TKr8637FiO7Z27zv8oj6pbWUQyLPQBQxtPVnwD20R-60eTDmD2ujnMt5PoqMrm8RfmNhVWDtjjMmCMjOpSXicFHj7XOuVIYQyqVWlWEh6dN36GVZYk93N8Bc9vY41xy8B9RzzOGVQzXvNEvn7O0nVbfs", + "q":"3dfOR9cuYq-0S-mkFLzgItgMEfFzB2q3hWehMuG0oCuqnb3vobLyumqjVZQO1dIrdwgTnCdpYzBcOfW5r370AFXjiWft_NGEiovonizhKpo9VVS78TzFgxkIdrecRezsZ-1kYd_s1qDbxtkDEgfAITAG9LUnADun4vIcb6yelxk", + "dp":"G4sPXkc6Ya9y8oJW9_ILj4xuppu0lzi_H7VTkS8xj5SdX3coE0oimYwxIi2emTAue0UOa5dpgFGyBJ4c8tQ2VF402XRugKDTP8akYhFo5tAA77Qe_NmtuYZc3C3m3I24G2GvR5sSDxUyAN2zq8Lfn9EUms6rY3Ob8YeiKkTiBj0", + "dq":"s9lAH9fggBsoFR8Oac2R_E2gw282rT2kGOAhvIllETE1efrA6huUUvMfBcMpn8lqeW6vzznYY5SSQF7pMdC_agI3nG8Ibp1BUb0JUiraRNqUfLhcQb_d9GF4Dh7e74WbRsobRonujTYN1xCaP6TO61jvWrX-L18txXw494Q_cgk", + "qi":"GyM_p6JrXySiz1toFgKbWV-JdI3jQ4ypu9rbMWx3rQJBfmt0FoYzgUIZEVFEcOqwemRN81zoDAaa-Bk0KWNGDjJHZDdDmFhW3AN7lI-puxk_mHZGJ11rxyR8O55XLSe3SPmRfKwZI6yU24ZxvQKFYItdldUKGzO6Ia6zTKhAVRU", + "alg":"RS256", + "kid":"2011-04-29"} + ] + } + """; + + List keys = parser.parse(json); + + assertThat(keys).hasSize(2); + + Key firstKey = keys.getFirst(); + assertThat(firstKey).isInstanceOf(ECJWKKey.class); + ECJWKKey ecKey = (ECJWKKey) firstKey; + assertThat(ecKey.getID()).isEqualTo("1"); + + Key secondKey = keys.getLast(); + assertThat(secondKey).isInstanceOf(RSAJWKKey.class); + RSAJWKKey rsaKey = (RSAJWKKey) secondKey; + assertThat(rsaKey.getID()).isEqualTo("2011-04-29"); + } + + @Test + void givenJsonObjectWithSymmetricKey_whenParsed_thenKeyReturned() throws ParseException { + String json = """ + {"keys": + [ + {"kty":"oct", + "alg":"A128KW", + "k":"GawgguFyGrWKav7AX4VKUg"}, + + {"kty":"oct", + "k":"AyM1SysPpbyDfgZld3umj1qzKObwVMkoqQ-EstJQLr_T-1qS0gZH75aKtMN3Yj0iPS4hcgUuTwjAzZr1Z9CAow", + "kid":"HMAC key used in JWS spec Appendix A.1 example"} + ] + } + """; + + List keys = parser.parse(json); + + assertThat(keys).hasSize(2); + + Key firstKey = keys.getFirst(); + assertThat(firstKey).isInstanceOf(OctetSequenceKeyJWKKey.class); + OctetSequenceKeyJWKKey firstOctetSequenceKey = (OctetSequenceKeyJWKKey) firstKey; + assertThat(firstOctetSequenceKey.getID()).isNull(); + + Key secondKey = keys.getLast(); + assertThat(secondKey).isInstanceOf(OctetSequenceKeyJWKKey.class); + OctetSequenceKeyJWKKey secondOctetSequenceKey = (OctetSequenceKeyJWKKey) secondKey; + assertThat(secondOctetSequenceKey.getID()).isEqualTo("HMAC key used in JWS spec Appendix A.1 example"); + } +} \ No newline at end of file