diff --git a/BappManifest.bmf b/BappManifest.bmf index 82db3bd..94e5263 100644 --- a/BappManifest.bmf +++ b/BappManifest.bmf @@ -2,12 +2,12 @@ Uuid: 26aaa5ded2f74beea19e2ed8345a93dd ExtensionType: 1 Name: JWT Editor RepoName: jwt-editor -ScreenVersion: 2.1.1 +ScreenVersion: 2.2 SerialVersion: 10 MinPlatformVersion: 8 ProOnly: False Author: Fraser Winterborn and Dolph Flynn. ShortDescription: Edit, sign, verify, encrypt and decrypt JSON Web Tokens (JWTs). -EntryPoint: build/libs/jwt-editor-2.1.1.jar +EntryPoint: build/libs/jwt-editor-2.2.jar BuildCommand: ./gradlew jar SupportedProducts: Pro, Community diff --git a/README.md b/README.md index 91b5a5a..beb5267 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,9 @@ Additionally it facilitates several well-known attacks against JWT implementatio ## Changelog +**2.2 2024-02-29** +- Allow resigning of JWS tokens during fuzzing (Thanks to [@BafDyce](https://github.com/BafDyce)). + **2.1.1 2024-01-22** - Use split panes to improve JWT editor with small screens or large font sizes (Thanks to [@eldstal](https://github.com/eldstal)). @@ -149,7 +152,7 @@ This option is automatically enabled if it is detected that the original JWT did *JWT Editor* can be built from source. * Ensure that Java JDK 17 or newer is installed * From root of project, run the command `./gradlew jar` -* This should place the JAR file `jwt-editor-2.1.1.jar` within the `build/libs` directory +* This should place the JAR file `jwt-editor-2.2.jar` within the `build/libs` directory * This can be loaded into Burp Suite by navigating to the `Extensions` tab, `Installed` sub-tab, clicking `Add` and loading the JAR file * This BApp is using the newer Montoya API so it's best to use the latest version of Burp Suite (try the earlier adopter channel if there are issues with the latest stable release) diff --git a/build.gradle b/build.gradle index 1a27f7a..ef8844d 100644 --- a/build.gradle +++ b/build.gradle @@ -3,7 +3,7 @@ plugins { } group = 'com.blackberry' -version = '2.1.1' +version = '2.2' description = 'jwt-editor' repositories { @@ -36,7 +36,7 @@ dependencies { 'com.nimbusds:nimbus-jose-jwt:9.21', 'org.exbin.deltahex:deltahex-swing:0.1.2', 'com.fifesoft:rsyntaxtextarea:3.3.4', - 'org.json:json:20231013', + 'org.json:json:20240205', 'org.apache.commons:commons-lang3:3.14.0' ) testImplementation( diff --git a/gradle.properties b/gradle.properties index f1cde57..9c33896 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,3 +1,3 @@ bouncycastle_version=1.77 -gui_designer_version=233.13135.104 +gui_designer_version=233.14475.38 extender_version=2023.5 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index ac7cb7a..f7c42a5 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,7 +1,7 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionSha256Sum=9d926787066a081739e8200858338b4a69e837c3a821a33aca9db09dd4a41026 -distributionUrl=https\://services.gradle.org/distributions/gradle-8.5-bin.zip +distributionSha256Sum=9631d53cf3e74bfa726893aee1f8994fee4e060c401335946dba2156f440f24c +distributionUrl=https\://services.gradle.org/distributions/gradle-8.6-bin.zip networkTimeout=10000 zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/src/main/java/burp/JWTEditorExtension.java b/src/main/java/burp/JWTEditorExtension.java index 2e5ce21..23f6d5b 100644 --- a/src/main/java/burp/JWTEditorExtension.java +++ b/src/main/java/burp/JWTEditorExtension.java @@ -105,7 +105,7 @@ public void initialize(MontoyaApi api) { ); Intruder intruder = api.intruder(); - intruder.registerPayloadProcessor(new JWSPayloadProcessor(burpConfig.intruderConfig())); + intruder.registerPayloadProcessor(new JWSPayloadProcessor(burpConfig.intruderConfig(), api.logging(), keysModel)); if (api.burpSuite().version().edition() != COMMUNITY_EDITION) { api.scanner().registerInsertionPointProvider(new JWSHeaderInsertionPointProvider(burpConfig.scannerConfig())); diff --git a/src/main/java/burp/config/BurpConfigPersistence.java b/src/main/java/burp/config/BurpConfigPersistence.java index 57b214d..c37dcea 100644 --- a/src/main/java/burp/config/BurpConfigPersistence.java +++ b/src/main/java/burp/config/BurpConfigPersistence.java @@ -24,6 +24,7 @@ import burp.proxy.HighlightColor; import burp.proxy.ProxyConfig; import burp.scanner.ScannerConfig; +import com.nimbusds.jose.JWSAlgorithm; import org.json.JSONException; import org.json.JSONObject; @@ -36,6 +37,7 @@ public class BurpConfigPersistence { private static final String INTRUDER_FUZZ_PARAMETER_NAME = "intruder_payload_processor_parameter_name"; private static final String INTRUDER_FUZZ_RESIGNING = "intruder_payload_processor_resign"; private static final String INTRUDER_FUZZ_SIGNING_KEY_ID = "intruder_payload_processor_signing_key_id"; + private static final String INTRUDER_FUZZ_SIGNING_ALGORITHM = "intruder_payload_processor_signing_algorithm"; private static final String SCANNER_INSERTION_POINT_PROVIDER_ENABLED_KEY = "scanner_insertion_point_provider_enabled"; private static final String SCANNER_INSERTION_PARAMETER_NAME = "scanner_insertion_point_provider_parameter_name"; @@ -77,6 +79,11 @@ public BurpConfig loadOrCreateNew() { intruderConfig.setSigningKeyId(keyId); } + if (parsedObject.has(INTRUDER_FUZZ_SIGNING_ALGORITHM) && parsedObject.get(INTRUDER_FUZZ_SIGNING_ALGORITHM) instanceof String algorithm) { + JWSAlgorithm jwsAlgorithm = JWSAlgorithm.parse(algorithm); + intruderConfig.setSigningAlgorithm(jwsAlgorithm); + } + if (parsedObject.has(INTRUDER_FUZZ_RESIGNING) && parsedObject.get(INTRUDER_FUZZ_RESIGNING) instanceof Boolean resign) { intruderConfig.setResign(resign); } @@ -107,6 +114,11 @@ public void save(BurpConfig model) { burpConfigJson.put(INTRUDER_FUZZ_PARAMETER_TYPE, model.intruderConfig().fuzzLocation()); burpConfigJson.put(INTRUDER_FUZZ_RESIGNING, model.intruderConfig().resign()); burpConfigJson.put(INTRUDER_FUZZ_SIGNING_KEY_ID, model.intruderConfig().signingKeyId()); + + JWSAlgorithm signingAlgorithm = model.intruderConfig().signingAlgorithm(); + String signingAlgorithmName = signingAlgorithm == null ? null : signingAlgorithm.getName(); + burpConfigJson.put(INTRUDER_FUZZ_SIGNING_ALGORITHM, signingAlgorithmName); + burpConfigJson.put(SCANNER_INSERTION_POINT_PROVIDER_ENABLED_KEY, model.scannerConfig().enableHeaderJWSInsertionPointLocation()); burpConfigJson.put(SCANNER_INSERTION_PARAMETER_NAME, model.scannerConfig().insertionPointLocationParameterName()); diff --git a/src/main/java/burp/intruder/IntruderConfig.java b/src/main/java/burp/intruder/IntruderConfig.java index 9f3c1e3..83c75ce 100644 --- a/src/main/java/burp/intruder/IntruderConfig.java +++ b/src/main/java/burp/intruder/IntruderConfig.java @@ -18,12 +18,16 @@ package burp.intruder; +import com.nimbusds.jose.JWSAlgorithm; + import static burp.intruder.FuzzLocation.PAYLOAD; +import static org.apache.commons.lang3.StringUtils.isNotEmpty; public class IntruderConfig { private String fuzzParameter; private FuzzLocation fuzzLocation; private String signingKeyId; + private JWSAlgorithm signingAlgorithm; private boolean resign; public IntruderConfig() { @@ -53,6 +57,7 @@ public String signingKeyId() { public void setSigningKeyId(String signingKeyId) { this.signingKeyId = signingKeyId; + this.resign = resign && canSign(); } public boolean resign() { @@ -60,6 +65,19 @@ public boolean resign() { } public void setResign(boolean resign) { - this.resign = resign; + this.resign = resign && canSign(); + } + + public JWSAlgorithm signingAlgorithm() { + return signingAlgorithm; + } + + public void setSigningAlgorithm(JWSAlgorithm signingAlgorithm) { + this.signingAlgorithm = signingAlgorithm; + this.resign = resign && canSign(); + } + + private boolean canSign() { + return isNotEmpty(signingKeyId) && signingAlgorithm != null; } } diff --git a/src/main/java/burp/intruder/JWSPayloadProcessor.java b/src/main/java/burp/intruder/JWSPayloadProcessor.java index 300f86c..2810732 100644 --- a/src/main/java/burp/intruder/JWSPayloadProcessor.java +++ b/src/main/java/burp/intruder/JWSPayloadProcessor.java @@ -4,9 +4,14 @@ import burp.api.montoya.intruder.PayloadData; import burp.api.montoya.intruder.PayloadProcessingResult; import burp.api.montoya.intruder.PayloadProcessor; +import burp.api.montoya.logging.Logging; +import com.blackberry.jwteditor.exceptions.SigningException; import com.blackberry.jwteditor.model.jose.JOSEObject; import com.blackberry.jwteditor.model.jose.JWS; import com.blackberry.jwteditor.model.jose.JWSFactory; +import com.blackberry.jwteditor.model.keys.Key; +import com.blackberry.jwteditor.model.keys.KeysModel; +import com.nimbusds.jose.JWSAlgorithm; import com.nimbusds.jose.util.Base64URL; import org.json.JSONObject; @@ -14,12 +19,17 @@ import static burp.intruder.FuzzLocation.PAYLOAD; import static com.blackberry.jwteditor.model.jose.JOSEObjectFinder.parseJOSEObject; +import static com.nimbusds.jose.HeaderParameterNames.ALGORITHM; public class JWSPayloadProcessor implements PayloadProcessor { + private final Logging logging; private final IntruderConfig intruderConfig; + private final KeysModel keysModel; - public JWSPayloadProcessor(IntruderConfig intruderConfig) { + public JWSPayloadProcessor(IntruderConfig intruderConfig, Logging logging, KeysModel keysModel) { + this.logging = logging; this.intruderConfig = intruderConfig; + this.keysModel = keysModel; } @Override @@ -43,7 +53,7 @@ public PayloadProcessingResult processPayload(PayloadData payloadData) { ? Base64URL.encode(targetJson.toString()) : jws.getEncodedPayload(); - JWS updatedJws = JWSFactory.jwsFromParts(updatedHeader, updatedPayload, jws.getEncodedSignature()); + JWS updatedJws = createJWS(updatedHeader, updatedPayload, jws.getEncodedSignature()); baseValue = ByteArray.byteArray(updatedJws.serialize()); } } @@ -51,8 +61,51 @@ public PayloadProcessingResult processPayload(PayloadData payloadData) { return PayloadProcessingResult.usePayload(baseValue); } + private Optional loadKey() { + if (!intruderConfig.resign()) { + return Optional.empty(); + } + + Key key = keysModel.getKey(intruderConfig.signingKeyId()); + + if (key == null) { + logging.logToError("Key with ID " + intruderConfig.signingKeyId() + " not found."); + } + + return Optional.ofNullable(key); + } + @Override public String displayName() { return "JWS payload processor"; } + + // Creates a JWS object from the given attributes. Signs the JWS if possible (i.e., available key selected in Intruder settings) + private JWS createJWS(Base64URL header, Base64URL payload, Base64URL originalSignature) { + return loadKey() + .flatMap(key -> { + try { + JWSAlgorithm algorithm = intruderConfig.signingAlgorithm(); + + String headerJson = header.decodeToString(); + JSONObject headerJsonObject = new JSONObject(headerJson); + + Object originalAlgorithm = headerJsonObject.get(ALGORITHM); + headerJsonObject.put(ALGORITHM, algorithm.getName()); + + // Only update when alg different to preserve key order + Base64URL updatedHeader = originalAlgorithm instanceof JWSAlgorithm alg && alg.equals(algorithm) + ? header + : Base64URL.encode(headerJsonObject.toString()); + + return Optional.of(JWSFactory.sign(key, algorithm, updatedHeader, payload)); + } catch (SigningException ex) { + logging.logToError("Failed to sign JWS: " + ex); + return Optional.empty(); + } + }) + .orElseGet( + () -> JWSFactory.jwsFromParts(header, payload, originalSignature) + ); + } } diff --git a/src/main/java/com/blackberry/jwteditor/model/keys/KeysModel.java b/src/main/java/com/blackberry/jwteditor/model/keys/KeysModel.java index ffca8b3..7480002 100644 --- a/src/main/java/com/blackberry/jwteditor/model/keys/KeysModel.java +++ b/src/main/java/com/blackberry/jwteditor/model/keys/KeysModel.java @@ -19,11 +19,11 @@ package com.blackberry.jwteditor.model.keys; import com.blackberry.jwteditor.exceptions.UnsupportedKeyException; -import com.blackberry.jwteditor.model.keys.KeysModelListener.InertKeyModelListener; import org.json.JSONArray; import org.json.JSONObject; import java.text.ParseException; +import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -37,12 +37,12 @@ public class KeysModel { private final Map keys; private final Object lock; - - private KeysModelListener modelListener; + + private final List modelListeners; public KeysModel() { this.keys = new LinkedHashMap<>(); - this.modelListener = new InertKeyModelListener(); + this.modelListeners = new ArrayList<>(); this.lock = new Object(); } @@ -53,7 +53,7 @@ public Iterable keys() { } public void addKeyModelListener(KeysModelListener modelListener) { - this.modelListener = modelListener; + this.modelListeners.add(modelListener); } /** @@ -131,8 +131,13 @@ public void addKey(Key key) { oldKey = keys.put(key.getID(), key); } - modelListener.notifyKeyDeleted(oldKey); - modelListener.notifyKeyInserted(key); + for (KeysModelListener modelListener : modelListeners) { + if (oldKey != null) { + modelListener.notifyKeyDeleted(oldKey); + } + + modelListener.notifyKeyInserted(key); + } } private int findIndexOfKeyWithId(String id) { @@ -163,7 +168,9 @@ public void deleteKey(String keyId) { } if (rowIndex >= 0) { - modelListener.notifyKeyDeleted(rowIndex); + for (KeysModelListener modelListener : this.modelListeners) { + modelListener.notifyKeyDeleted(rowIndex); + } } } diff --git a/src/main/java/com/blackberry/jwteditor/model/keys/KeysModelListener.java b/src/main/java/com/blackberry/jwteditor/model/keys/KeysModelListener.java index f6a9fc7..6ce2045 100644 --- a/src/main/java/com/blackberry/jwteditor/model/keys/KeysModelListener.java +++ b/src/main/java/com/blackberry/jwteditor/model/keys/KeysModelListener.java @@ -7,7 +7,7 @@ public interface KeysModelListener { void notifyKeyDeleted(Key key); - class InertKeyModelListener implements KeysModelListener { + class InertKeysModelListener implements KeysModelListener { @Override public void notifyKeyInserted(Key key) { } @@ -20,4 +20,27 @@ public void notifyKeyDeleted(int rowIndex) { public void notifyKeyDeleted(Key key) { } } + + class SimpleKeysModelListener implements KeysModelListener { + private final Runnable action; + + public SimpleKeysModelListener(Runnable action) { + this.action = action; + } + + @Override + public void notifyKeyInserted(Key key) { + action.run(); + } + + @Override + public void notifyKeyDeleted(int rowIndex) { + action.run(); + } + + @Override + public void notifyKeyDeleted(Key key) { + action.run(); + } + } } diff --git a/src/main/java/com/blackberry/jwteditor/view/SuiteView.java b/src/main/java/com/blackberry/jwteditor/view/SuiteView.java index 0316b50..a78f152 100644 --- a/src/main/java/com/blackberry/jwteditor/view/SuiteView.java +++ b/src/main/java/com/blackberry/jwteditor/view/SuiteView.java @@ -94,6 +94,6 @@ private void createUIComponents() { keysModel, rstaFactory ); - configView = new ConfigView(burpConfig, userInterface, isProVersion); + configView = new ConfigView(burpConfig, userInterface, isProVersion, keysModel); } } diff --git a/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.form b/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.form index 742708a..a36bee6 100644 --- a/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.form +++ b/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.form @@ -1,274 +1,51 @@
- - + + - + - - + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + - + - + - + - - - - - diff --git a/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.java b/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.java index 9180d2b..8da71f1 100644 --- a/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.java +++ b/src/main/java/com/blackberry/jwteditor/view/config/ConfigView.java @@ -20,100 +20,32 @@ import burp.api.montoya.ui.UserInterface; import burp.config.BurpConfig; -import burp.intruder.FuzzLocation; -import burp.intruder.IntruderConfig; -import burp.proxy.HighlightColor; -import burp.proxy.ProxyConfig; -import burp.scanner.ScannerConfig; -import com.blackberry.jwteditor.view.utils.DocumentAdapter; +import com.blackberry.jwteditor.model.keys.KeysModel; import javax.swing.*; -import java.awt.*; -import static java.awt.Font.BOLD; -/** - * Config panel - */ public class ConfigView { - private JPanel mainPanel; - private JCheckBox checkBoxHighlightJWT; - private JLabel labelHighlightColor; - private JComboBox comboBoxHighlightColor; - private JLabel labelHighlightJWT; - private JTextField intruderParameterName; - private JComboBox comboBoxPayloadPosition; - private JCheckBox checkBoxHeaderInsertionPoint; - private JTextField scannerParameterName; - private JPanel proxyPanel; - private JLabel proxyLabel; - private JLabel intruderLabel; - private JLabel scannerLabel; - private JPanel intruderPanel; - private JLabel spacerLabel; - - public ConfigView(BurpConfig burpConfig, UserInterface userInterface, boolean isProVersion) { - ProxyConfig proxyConfig = burpConfig.proxyConfig(); - - checkBoxHighlightJWT.setSelected(proxyConfig.highlightJWT()); - checkBoxHighlightJWT.addActionListener(e -> { - comboBoxHighlightColor.setEnabled(checkBoxHighlightJWT.isSelected()); - proxyConfig.setHighlightJWT(checkBoxHighlightJWT.isSelected()); - }); - - comboBoxHighlightColor.setModel(new DefaultComboBoxModel<>(HighlightColor.values())); - comboBoxHighlightColor.setSelectedItem(proxyConfig.highlightColor()); - comboBoxHighlightColor.setEnabled(proxyConfig.highlightJWT()); - comboBoxHighlightColor.addActionListener(e -> proxyConfig.setHighlightColor((HighlightColor) comboBoxHighlightColor.getSelectedItem())); - - IntruderConfig intruderConfig = burpConfig.intruderConfig(); - - intruderParameterName.setText(intruderConfig.fuzzParameter()); - intruderParameterName.getDocument().addDocumentListener( - new DocumentAdapter(e -> intruderConfig.setFuzzParameter(intruderParameterName.getText())) - ); - - comboBoxPayloadPosition.setModel(new DefaultComboBoxModel<>(FuzzLocation.values())); - comboBoxPayloadPosition.setSelectedItem(intruderConfig.fuzzLocation()); - comboBoxPayloadPosition.addActionListener(e -> intruderConfig.setFuzzLocation((FuzzLocation) comboBoxPayloadPosition.getSelectedItem())); - - ScannerConfig scannerConfig = burpConfig.scannerConfig(); + private final BurpConfig burpConfig; + private final UserInterface userInterface; + private final boolean isProVersion; + private final KeysModel keysModel; - checkBoxHeaderInsertionPoint.setEnabled(isProVersion); - checkBoxHeaderInsertionPoint.setSelected(scannerConfig.enableHeaderJWSInsertionPointLocation()); - checkBoxHeaderInsertionPoint.addActionListener(e -> { - scannerConfig.setEnableHeaderJWSInsertionPointLocation(checkBoxHeaderInsertionPoint.isSelected()); - scannerParameterName.setEnabled(checkBoxHeaderInsertionPoint.isSelected()); - }); - - scannerParameterName.setEnabled(scannerConfig.enableHeaderJWSInsertionPointLocation()); - scannerParameterName.setText(scannerConfig.insertionPointLocationParameterName()); - scannerParameterName.getDocument().addDocumentListener( - new DocumentAdapter(e -> scannerConfig.setInsertionPointLocationParameterName(scannerParameterName.getText())) - ); - - proxyLabel.setFont(proxyLabel.getFont().deriveFont(BOLD)); - intruderLabel.setFont(intruderLabel.getFont().deriveFont(BOLD)); - scannerLabel.setFont(scannerLabel.getFont().deriveFont(BOLD)); - userInterface.applyThemeToComponent(mainPanel); - - comboBoxHighlightColor.setRenderer(new HighlightComboRenderer()); + private JPanel mainPanel; + private ProxyConfigView proxyConfigView; + private ScannerConfigView scannerConfigView; + private IntruderConfigView intruderConfigView; + + public ConfigView(BurpConfig burpConfig, UserInterface userInterface, boolean isProVersion, KeysModel keysModel) { + this.burpConfig = burpConfig; + this.userInterface = userInterface; + this.isProVersion = isProVersion; + this.keysModel = keysModel; } - /** - * Custom list cell renderer to color rows of combo box drop down list. - */ - private static class HighlightComboRenderer implements ListCellRenderer { - private final ListCellRenderer renderer = new DefaultListCellRenderer(); - - @Override - public Component getListCellRendererComponent(JList list, HighlightColor value, int index, boolean isSelected, boolean cellHasFocus) { - JLabel label = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); - - Color background = isSelected ? list.getSelectionBackground() : value.color; - label.setBackground(background); - - return label; - } + private void createUIComponents() { + proxyConfigView = new ProxyConfigView(userInterface, burpConfig.proxyConfig()); + intruderConfigView = new IntruderConfigView(userInterface, new IntruderConfigModel(keysModel, burpConfig.intruderConfig())); + scannerConfigView = new ScannerConfigView(userInterface, burpConfig.scannerConfig(), isProVersion); } } diff --git a/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigModel.java b/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigModel.java new file mode 100644 index 0000000..a3dd2e1 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigModel.java @@ -0,0 +1,168 @@ +package com.blackberry.jwteditor.view.config; + +import burp.intruder.FuzzLocation; +import burp.intruder.IntruderConfig; +import com.blackberry.jwteditor.model.keys.Key; +import com.blackberry.jwteditor.model.keys.KeysModel; +import com.blackberry.jwteditor.model.keys.KeysModelListener.SimpleKeysModelListener; +import com.nimbusds.jose.JWSAlgorithm; + +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.ArrayList; +import java.util.List; + +import static java.util.Arrays.stream; + +class IntruderConfigModel { + static final String SIGNING_KEYS_UPDATED = "signingKeysUpdated"; + static final String SELECTED_KEY_UPDATED = "selectedKeyUpdated"; + static final String SIGNING_ALGORITHMS_UPDATED = "signingAlgorithmsUpdated"; + static final String SELECTED_ALGORITHM_UPDATED = "selectedAlgorithmUpdated"; + static final String RESIGN_UPDATED = "resignUpdated"; + + private static final JWSAlgorithm[] NO_ALGORITHMS = new JWSAlgorithm[0]; + + private final PropertyChangeSupport propertyChangeSupport; + private final KeysModel keysModel; + private final IntruderConfig intruderConfig; + private final List listeners; + + private String[] signingKeyIds; + private JWSAlgorithm[] selectedKeySigningAlgorithms; + + IntruderConfigModel(KeysModel keysModel, IntruderConfig intruderConfig) { + this.keysModel = keysModel; + this.intruderConfig = intruderConfig; + this.signingKeyIds = signingKeyIds(); + this.selectedKeySigningAlgorithms = signingAlgorithms(); + this.listeners = new ArrayList<>(); + this.propertyChangeSupport = new PropertyChangeSupport(this); + + keysModel.addKeyModelListener(new SimpleKeysModelListener(this::updateSigningKeyList)); + } + + String fuzzParameter() { + return intruderConfig.fuzzParameter(); + } + + void setFuzzParameter(String fuzzParameter) { + intruderConfig.setFuzzParameter(fuzzParameter); + } + + FuzzLocation fuzzLocation() { + return intruderConfig.fuzzLocation(); + } + + void setFuzzLocation(FuzzLocation fuzzLocation) { + intruderConfig.setFuzzLocation(fuzzLocation); + } + + FuzzLocation[] fuzzLocations() { + return FuzzLocation.values(); + } + + boolean hasSigningKeys() { + return !keysModel.getSigningKeys().isEmpty(); + } + + String[] signingKeyIds() { + return keysModel.getSigningKeys().stream().map(Key::getID).toArray(String[]::new); + } + + String signingKeyId() { + return intruderConfig.signingKeyId(); + } + + public void setSigningKeyId(String signingKeyId) { + String oldSigningKeyId = intruderConfig.signingKeyId(); + JWSAlgorithm[] oldAlgorithms = selectedKeySigningAlgorithms; + intruderConfig.setSigningKeyId(signingKeyId); + + boolean selectedKeyUnchanged = (oldSigningKeyId == null && signingKeyId==null) || (oldSigningKeyId != null && oldSigningKeyId.equals(signingKeyId)); + + if (!selectedKeyUnchanged) { + selectedKeySigningAlgorithms = signingAlgorithms(); + propertyChangeSupport.firePropertyChange(SIGNING_ALGORITHMS_UPDATED, oldAlgorithms, selectedKeySigningAlgorithms); + } + } + + JWSAlgorithm[] signingAlgorithms() { + if (intruderConfig.signingKeyId() == null) { + return NO_ALGORITHMS; + } + + return keysModel.getSigningKeys().stream() + .filter(k -> k.getID().equals(intruderConfig.signingKeyId())) + .findFirst() + .orElseThrow() + .getSigningAlgorithms(); + } + + JWSAlgorithm signingAlgorithm() { + return intruderConfig.signingAlgorithm(); + } + + void setSigningAlgorithm(JWSAlgorithm signingAlgorithm) { + intruderConfig.setSigningAlgorithm(signingAlgorithm); + } + + boolean resign() { + return intruderConfig.resign() && hasSigningKeys(); + } + + void setResign(boolean resign) { + intruderConfig.setResign(resign); + } + + void addPropertyChangeListener(PropertyChangeListener listener) { + listeners.add(listener); + propertyChangeSupport.addPropertyChangeListener(listener); + } + + void clearListeners() { + listeners.forEach(propertyChangeSupport::removePropertyChangeListener); + } + + private void updateSigningKeyList() { + String[] oldSigningKeyIds = signingKeyIds; + JWSAlgorithm[] oldSigningAlgorithms = selectedKeySigningAlgorithms; + JWSAlgorithm oldSigningAlgorithm = intruderConfig.signingAlgorithm(); + signingKeyIds = signingKeyIds(); + propertyChangeSupport.firePropertyChange(SIGNING_KEYS_UPDATED, oldSigningKeyIds, signingKeyIds); + + String selectedKeyId = intruderConfig.signingKeyId(); + boolean modelIsEmpty = signingKeyIds.length == 0; + boolean modelWasEmpty = oldSigningKeyIds.length == 0 && signingKeyIds.length > 0; + boolean selectedKeyDeleted = selectedKeyId != null && (signingKeyIds.length == 0 || stream(signingKeyIds).noneMatch(selectedKeyId::equals)); + boolean wasResigning = intruderConfig.resign(); + + if (modelIsEmpty) { + selectedKeySigningAlgorithms = NO_ALGORITHMS; + propertyChangeSupport.firePropertyChange(SIGNING_ALGORITHMS_UPDATED, oldSigningAlgorithms, NO_ALGORITHMS); + + String oldSigningKeyId = intruderConfig.signingKeyId(); + intruderConfig.setSigningKeyId(null); + propertyChangeSupport.firePropertyChange(SELECTED_KEY_UPDATED, oldSigningKeyId, null); + + intruderConfig.setSigningAlgorithm(null); + propertyChangeSupport.firePropertyChange(SELECTED_ALGORITHM_UPDATED, oldSigningAlgorithm, null); + + intruderConfig.setResign(false); + propertyChangeSupport.firePropertyChange(RESIGN_UPDATED, wasResigning, false); + } + else if (modelWasEmpty || selectedKeyDeleted) { + String firstKeyId = signingKeyIds[0]; + intruderConfig.setSigningKeyId(firstKeyId); + selectedKeySigningAlgorithms = signingAlgorithms(); + JWSAlgorithm firstSigningAlgorithm = selectedKeySigningAlgorithms[0]; + intruderConfig.setSigningAlgorithm(firstSigningAlgorithm); + intruderConfig.setResign(false); + + propertyChangeSupport.firePropertyChange(SELECTED_KEY_UPDATED, null, firstKeyId); + propertyChangeSupport.firePropertyChange(SIGNING_ALGORITHMS_UPDATED, oldSigningAlgorithms, selectedKeySigningAlgorithms); + propertyChangeSupport.firePropertyChange(SELECTED_ALGORITHM_UPDATED, oldSigningAlgorithm, firstSigningAlgorithm); + propertyChangeSupport.firePropertyChange(RESIGN_UPDATED, wasResigning, false); + } + } +} diff --git a/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigView.form b/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigView.form new file mode 100644 index 0000000..265fb65 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigView.form @@ -0,0 +1,143 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigView.java b/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigView.java new file mode 100644 index 0000000..e5e48df --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/IntruderConfigView.java @@ -0,0 +1,104 @@ +/* +Author : Dolph Flynn + +Copyright 2024 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.config; + +import burp.api.montoya.ui.UserInterface; +import burp.intruder.FuzzLocation; +import com.blackberry.jwteditor.view.utils.DocumentAdapter; +import com.nimbusds.jose.JWSAlgorithm; + +import javax.swing.*; + +import static com.blackberry.jwteditor.view.config.IntruderConfigModel.*; +import static java.awt.Font.BOLD; + + +class IntruderConfigView { + private JPanel mainPanel; + private JTextField intruderParameterName; + private JComboBox comboBoxPayloadPosition; + private JComboBox comboBoxIntruderSigningKeyId; + private JLabel intruderLabel; + private JLabel spacerLabel; + private JCheckBox resignIntruderJWS; + private JComboBox comboBoxIntruderSigningAlg; + + IntruderConfigView(UserInterface userInterface, IntruderConfigModel model) { + intruderParameterName.setText(model.fuzzParameter()); + intruderParameterName.getDocument().addDocumentListener( + new DocumentAdapter(e -> model.setFuzzParameter(intruderParameterName.getText())) + ); + + comboBoxPayloadPosition.setModel(new DefaultComboBoxModel<>(model.fuzzLocations())); + comboBoxPayloadPosition.setSelectedItem(model.fuzzLocation()); + comboBoxPayloadPosition.addActionListener(e -> model.setFuzzLocation((FuzzLocation) comboBoxPayloadPosition.getSelectedItem())); + + comboBoxIntruderSigningKeyId.setModel(new DefaultComboBoxModel<>(model.signingKeyIds())); + comboBoxIntruderSigningKeyId.setSelectedItem(model.signingKeyId()); + comboBoxIntruderSigningKeyId.setEnabled(model.hasSigningKeys()); + comboBoxIntruderSigningKeyId.addActionListener(e -> model.setSigningKeyId((String) comboBoxIntruderSigningKeyId.getSelectedItem())); + + comboBoxIntruderSigningAlg.setModel(new DefaultComboBoxModel<>(model.signingAlgorithms())); + comboBoxIntruderSigningAlg.setSelectedItem(model.signingAlgorithm()); + comboBoxIntruderSigningAlg.setEnabled(model.hasSigningKeys()); + comboBoxIntruderSigningAlg.addActionListener(e -> model.setSigningAlgorithm((JWSAlgorithm) comboBoxIntruderSigningAlg.getSelectedItem())); + + resignIntruderJWS.setEnabled(model.hasSigningKeys()); + resignIntruderJWS.setSelected(model.resign()); + resignIntruderJWS.addActionListener(e -> model.setResign(resignIntruderJWS.isSelected())); + + intruderLabel.setFont(intruderLabel.getFont().deriveFont(BOLD)); + userInterface.applyThemeToComponent(mainPanel); + + updateControls(model.hasSigningKeys()); + + model.addPropertyChangeListener(evt -> { + switch (evt.getPropertyName()) { + case SIGNING_KEYS_UPDATED: + comboBoxIntruderSigningKeyId.setModel(new DefaultComboBoxModel<>((String[]) evt.getNewValue())); + comboBoxIntruderSigningKeyId.setSelectedItem(model.signingKeyId()); + updateControls(model.hasSigningKeys()); + resignIntruderJWS.setSelected(model.resign()); + break; + + case SIGNING_ALGORITHMS_UPDATED: + comboBoxIntruderSigningAlg.setModel(new DefaultComboBoxModel((JWSAlgorithm[]) evt.getNewValue())); + break; + + case SELECTED_KEY_UPDATED: + comboBoxIntruderSigningKeyId.setSelectedItem(evt.getNewValue()); + break; + + case SELECTED_ALGORITHM_UPDATED: + comboBoxIntruderSigningAlg.setSelectedItem(evt.getNewValue()); + break; + + case RESIGN_UPDATED: + resignIntruderJWS.setSelected((Boolean) evt.getNewValue()); + break; + } + }); + } + + private void updateControls(boolean hasSigningKeys) { + resignIntruderJWS.setEnabled(hasSigningKeys); + comboBoxIntruderSigningKeyId.setEnabled(hasSigningKeys); + comboBoxIntruderSigningAlg.setEnabled(hasSigningKeys); + } +} diff --git a/src/main/java/com/blackberry/jwteditor/view/config/ProxyConfigView.form b/src/main/java/com/blackberry/jwteditor/view/config/ProxyConfigView.form new file mode 100644 index 0000000..84b5ca7 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/ProxyConfigView.form @@ -0,0 +1,66 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/blackberry/jwteditor/view/config/ProxyConfigView.java b/src/main/java/com/blackberry/jwteditor/view/config/ProxyConfigView.java new file mode 100644 index 0000000..e8a11c6 --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/ProxyConfigView.java @@ -0,0 +1,69 @@ +/* +Author : Dolph Flynn + +Copyright 2024 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.config; + +import burp.api.montoya.ui.UserInterface; +import burp.proxy.HighlightColor; +import burp.proxy.ProxyConfig; + +import javax.swing.*; +import java.awt.*; + +import static java.awt.Font.BOLD; + + +class ProxyConfigView { + private JPanel mainPanel; + private JCheckBox checkBoxHighlightJWT; + private JComboBox comboBoxHighlightColor; + private JLabel proxyLabel; + + ProxyConfigView(UserInterface userInterface, ProxyConfig proxyConfig) { + checkBoxHighlightJWT.setSelected(proxyConfig.highlightJWT()); + + checkBoxHighlightJWT.addActionListener(e -> { + comboBoxHighlightColor.setEnabled(checkBoxHighlightJWT.isSelected()); + proxyConfig.setHighlightJWT(checkBoxHighlightJWT.isSelected()); + }); + + comboBoxHighlightColor.setModel(new DefaultComboBoxModel<>(HighlightColor.values())); + comboBoxHighlightColor.setSelectedItem(proxyConfig.highlightColor()); + comboBoxHighlightColor.setEnabled(proxyConfig.highlightJWT()); + comboBoxHighlightColor.addActionListener(e -> proxyConfig.setHighlightColor((HighlightColor) comboBoxHighlightColor.getSelectedItem())); + + proxyLabel.setFont(proxyLabel.getFont().deriveFont(BOLD)); + userInterface.applyThemeToComponent(mainPanel); + + comboBoxHighlightColor.setRenderer(new HighlightComboRenderer()); + } + + private static class HighlightComboRenderer implements ListCellRenderer { + private final ListCellRenderer renderer = new DefaultListCellRenderer(); + + @Override + public Component getListCellRendererComponent(JList list, HighlightColor value, int index, boolean isSelected, boolean cellHasFocus) { + JLabel label = (JLabel) renderer.getListCellRendererComponent(list, value, index, isSelected, cellHasFocus); + + Color background = isSelected ? list.getSelectionBackground() : value.color; + label.setBackground(background); + + return label; + } + } +} diff --git a/src/main/java/com/blackberry/jwteditor/view/config/ScannerConfigView.form b/src/main/java/com/blackberry/jwteditor/view/config/ScannerConfigView.form new file mode 100644 index 0000000..a8ef9aa --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/ScannerConfigView.form @@ -0,0 +1,96 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/src/main/java/com/blackberry/jwteditor/view/config/ScannerConfigView.java b/src/main/java/com/blackberry/jwteditor/view/config/ScannerConfigView.java new file mode 100644 index 0000000..9e11d8d --- /dev/null +++ b/src/main/java/com/blackberry/jwteditor/view/config/ScannerConfigView.java @@ -0,0 +1,54 @@ +/* +Author : Dolph Flynn + +Copyright 2024 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.config; + +import burp.api.montoya.ui.UserInterface; +import burp.scanner.ScannerConfig; +import com.blackberry.jwteditor.view.utils.DocumentAdapter; + +import javax.swing.*; + +import static java.awt.Font.BOLD; + + +class ScannerConfigView { + private JPanel mainPanel; + private JCheckBox checkBoxHeaderInsertionPoint; + private JTextField scannerParameterName; + private JLabel scannerLabel; + + ScannerConfigView(UserInterface userInterface, ScannerConfig scannerConfig, boolean isProVersion) { + + checkBoxHeaderInsertionPoint.setEnabled(isProVersion); + checkBoxHeaderInsertionPoint.setSelected(scannerConfig.enableHeaderJWSInsertionPointLocation()); + checkBoxHeaderInsertionPoint.addActionListener(e -> { + scannerConfig.setEnableHeaderJWSInsertionPointLocation(checkBoxHeaderInsertionPoint.isSelected()); + scannerParameterName.setEnabled(checkBoxHeaderInsertionPoint.isSelected()); + }); + + scannerParameterName.setEnabled(scannerConfig.enableHeaderJWSInsertionPointLocation()); + scannerParameterName.setText(scannerConfig.insertionPointLocationParameterName()); + scannerParameterName.getDocument().addDocumentListener( + new DocumentAdapter(e -> scannerConfig.setInsertionPointLocationParameterName(scannerParameterName.getText())) + ); + + scannerLabel.setFont(scannerLabel.getFont().deriveFont(BOLD)); + userInterface.applyThemeToComponent(mainPanel); + } +} diff --git a/src/main/resources/strings.properties b/src/main/resources/strings.properties index 82a2dab..7ac3f4d 100644 --- a/src/main/resources/strings.properties +++ b/src/main/resources/strings.properties @@ -115,6 +115,7 @@ proxy_config_proxy_listener_enabled=Highlight JWTs within HTTP and WebSocket mes proxy_config_highlight_color=Highlight color: intruder_payload_processing_location=Payload Position: intruder_payload_processing_parameter_name=Parameter Name: +intruder_signing_key_id=Signing Key ID: empty_key_signing_dialog_title=Empty Key Signing Dialog empty_key_signing_algorithm=Algorithm psychic_signature_signing_dialog_title=Psychic Signature Signing Dialog diff --git a/src/test/java/burp/api/montoya/logging/StubLogging.java b/src/test/java/burp/api/montoya/logging/StubLogging.java new file mode 100644 index 0000000..e9af542 --- /dev/null +++ b/src/test/java/burp/api/montoya/logging/StubLogging.java @@ -0,0 +1,41 @@ +package burp.api.montoya.logging; + +import java.io.PrintStream; + +public class StubLogging implements Logging { + public static final Logging LOGGING = new StubLogging(); + + @Override + public PrintStream output() { + return null; + } + + @Override + public PrintStream error() { + return null; + } + + @Override + public void logToOutput(String message) { + } + + @Override + public void logToError(String message) { + } + + @Override + public void raiseDebugEvent(String message) { + } + + @Override + public void raiseInfoEvent(String message) { + } + + @Override + public void raiseErrorEvent(String message) { + } + + @Override + public void raiseCriticalEvent(String message) { + } +} diff --git a/src/test/java/burp/config/BurpConfigPersistenceTest.java b/src/test/java/burp/config/BurpConfigPersistenceTest.java index 22057bd..f4abf6b 100644 --- a/src/test/java/burp/config/BurpConfigPersistenceTest.java +++ b/src/test/java/burp/config/BurpConfigPersistenceTest.java @@ -21,6 +21,7 @@ import burp.api.montoya.persistence.Preferences; import burp.intruder.FuzzLocation; import burp.proxy.HighlightColor; +import com.nimbusds.jose.JWSAlgorithm; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.Arguments; @@ -34,6 +35,8 @@ import static burp.proxy.HighlightColor.CYAN; import static burp.proxy.HighlightColor.RED; import static burp.proxy.ProxyConfig.DEFAULT_HIGHLIGHT_COLOR; +import static com.nimbusds.jose.JWSAlgorithm.ES256; +import static com.nimbusds.jose.JWSAlgorithm.EdDSA; import static org.assertj.core.api.Assertions.assertThat; import static org.junit.jupiter.params.provider.Arguments.arguments; import static org.mockito.Mockito.*; @@ -230,6 +233,7 @@ private static Stream validIntruderConfigJson() { HEADER, "sub", false, + null, null ), arguments( @@ -237,28 +241,31 @@ private static Stream validIntruderConfigJson() { PAYLOAD, "role", false, + null, null ), arguments( - "{\"intruder_payload_processor_fuzz_location\":\"header\",\"intruder_payload_processor_parameter_name\":\"sub\",\"intruder_payload_processor_resign\":true}", + "{\"intruder_payload_processor_fuzz_location\":\"header\",\"intruder_payload_processor_parameter_name\":\"sub\",\"intruder_payload_processor_resign\":true, \"intruder_payload_processor_signing_key_id\": \"uuid\", \"intruder_payload_processor_signing_algorithm\": \"EdDSA\"}", HEADER, "sub", true, - null + "uuid", + EdDSA ), arguments( - "{\"intruder_payload_processor_fuzz_location\":\"header\",\"intruder_payload_processor_parameter_name\":\"sub\",\"intruder_payload_processor_signing_key_id\":\"131da5fb-8484-4717-b0d2-b79925978596\"}", + "{\"intruder_payload_processor_fuzz_location\":\"header\",\"intruder_payload_processor_parameter_name\":\"sub\",\"intruder_payload_processor_signing_key_id\":\"131da5fb-8484-4717-b0d2-b79925978596\", \"intruder_payload_processor_signing_algorithm\": \"ES256\"}", HEADER, "sub", false, - "131da5fb-8484-4717-b0d2-b79925978596" + "131da5fb-8484-4717-b0d2-b79925978596", + ES256 ) ); } @ParameterizedTest @MethodSource("validIntruderConfigJson") - void givenValidIntruderSavedConfig_whenLoadOrCreateCalled_thenAppropriateConfigReturned(String json, FuzzLocation expectedLocation, String expectedParameterName, boolean expectedResign, String expectedSigningKeyId) { + void givenValidIntruderSavedConfig_whenLoadOrCreateCalled_thenAppropriateConfigReturned(String json, FuzzLocation expectedLocation, String expectedParameterName, boolean expectedResign, String expectedSigningKeyId, JWSAlgorithm expectedSigningAlgorithm) { BurpConfigPersistence configPersistence = new BurpConfigPersistence(callbacks); when(callbacks.getString(BURP_SETTINGS_NAME)).thenReturn(json); @@ -270,6 +277,7 @@ void givenValidIntruderSavedConfig_whenLoadOrCreateCalled_thenAppropriateConfigR assertThat(burpConfig.intruderConfig().fuzzParameter()).isEqualTo(expectedParameterName); assertThat(burpConfig.intruderConfig().resign()).isEqualTo(expectedResign); assertThat(burpConfig.intruderConfig().signingKeyId()).isEqualTo(expectedSigningKeyId); + assertThat(burpConfig.intruderConfig().signingAlgorithm()).isEqualTo(expectedSigningAlgorithm); } @Test diff --git a/src/test/java/burp/config/IntruderConfigTest.java b/src/test/java/burp/config/IntruderConfigTest.java new file mode 100644 index 0000000..4e1fb1d --- /dev/null +++ b/src/test/java/burp/config/IntruderConfigTest.java @@ -0,0 +1,90 @@ +/* +Author : Dolph Flynn + +Copyright 2022 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 burp.config; + +import burp.intruder.IntruderConfig; +import org.junit.jupiter.api.Test; + +import static com.nimbusds.jose.JWSAlgorithm.HS256; +import static org.assertj.core.api.Assertions.assertThat; + +class IntruderConfigTest { + @Test + void givenNullKeyID_whenResignIsSetTrue_thenResignIsFalse() { + IntruderConfig config = new IntruderConfig(); + config.setSigningKeyId(null); + + config.setResign(true); + + assertThat(config.resign()).isFalse(); + } + + @Test + void givenEmptyKeyID_whenResignIsSetTrue_thenResignIsFalse() { + IntruderConfig config = new IntruderConfig(); + config.setSigningKeyId(""); + + config.setResign(true); + + assertThat(config.resign()).isFalse(); + } + + @Test + void givenValidKeyIDAndNullSigningAlgorithm_whenResignIsSetTrue_thenResignIsFalse() { + IntruderConfig config = new IntruderConfig(); + config.setSigningKeyId("keyID"); + + config.setResign(true); + + assertThat(config.resign()).isFalse(); + } + + @Test + void givenValidKeyIDAndNonNullSigningAlgorithm_whenResignIsSetTrue_thenResignIsTrue() { + IntruderConfig config = new IntruderConfig(); + config.setSigningKeyId("keyID"); + config.setSigningAlgorithm(HS256); + + config.setResign(true); + + assertThat(config.resign()).isTrue(); + } + + @Test + void givenResignIsSetTrue_whenNullKeyID_thenResignIsFalse() { + IntruderConfig config = new IntruderConfig(); + config.setSigningKeyId("keyId"); + config.setResign(true); + + config.setSigningKeyId(null); + + assertThat(config.resign()).isFalse(); + } + + @Test + void givenResignIsSetTrue_whenEmptyKeyID_thenResignIsFalse() { + IntruderConfig config = new IntruderConfig(); + config.setSigningKeyId("keyId"); + config.setResign(true); + + config.setSigningKeyId(""); + + assertThat(config.resign()).isFalse(); + } +} \ No newline at end of file diff --git a/src/test/java/burp/intruder/IntruderConfigBuilder.java b/src/test/java/burp/intruder/IntruderConfigBuilder.java new file mode 100644 index 0000000..741849f --- /dev/null +++ b/src/test/java/burp/intruder/IntruderConfigBuilder.java @@ -0,0 +1,44 @@ +package burp.intruder; + +import com.nimbusds.jose.JWSAlgorithm; + +class IntruderConfigBuilder { + private final IntruderConfig config; + + private IntruderConfigBuilder() { + this.config = new IntruderConfig(); + } + + IntruderConfigBuilder withFuzzLocation(FuzzLocation fuzzLocation) { + config.setFuzzLocation(fuzzLocation); + return this; + } + + IntruderConfigBuilder withFuzzParameter(String parameterName) { + config.setFuzzParameter(parameterName); + return this; + } + + IntruderConfigBuilder withSigningKeyId(String signingId) { + config.setSigningKeyId(signingId); + return this; + } + + IntruderConfigBuilder withSigningAlgorithm(JWSAlgorithm algorithm) { + config.setSigningAlgorithm(algorithm); + return this; + } + + IntruderConfigBuilder withResigning(boolean resign) { + config.setResign(resign); + return this; + } + + IntruderConfig build() { + return config; + } + + static IntruderConfigBuilder intruderConfig() { + return new IntruderConfigBuilder(); + } +} diff --git a/src/test/java/burp/intruder/JWSPayloadProcessorTest.java b/src/test/java/burp/intruder/JWSPayloadProcessorTest.java new file mode 100644 index 0000000..5aa8f60 --- /dev/null +++ b/src/test/java/burp/intruder/JWSPayloadProcessorTest.java @@ -0,0 +1,191 @@ +package burp.intruder; + +import burp.api.montoya.MontoyaExtension; +import burp.api.montoya.core.ByteArray; +import burp.api.montoya.core.FakeByteArray; +import burp.api.montoya.internal.MontoyaObjectFactory; +import burp.api.montoya.internal.ObjectFactoryLocator; +import burp.api.montoya.intruder.FakePayloadProcessingResult; +import burp.api.montoya.intruder.PayloadData; +import burp.api.montoya.intruder.PayloadProcessingResult; +import com.blackberry.jwteditor.model.keys.KeysModel; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.stubbing.Answer; + +import static burp.api.montoya.intruder.FakePayloadData.payloadData; +import static burp.api.montoya.intruder.PayloadProcessingAction.USE_PAYLOAD; +import static burp.api.montoya.logging.StubLogging.LOGGING; +import static burp.intruder.FuzzLocation.HEADER; +import static burp.intruder.FuzzLocation.PAYLOAD; +import static burp.intruder.IntruderConfigBuilder.intruderConfig; +import static com.blackberry.jwteditor.KeysModelBuilder.keysModel; +import static com.nimbusds.jose.JWSAlgorithm.RS256; +import static com.nimbusds.jose.JWSAlgorithm.RS512; +import static data.PemData.RSA1024Private; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; + +@ExtendWith(MontoyaExtension.class) +class JWSPayloadProcessorTest { + private static final KeysModel EMPTY_KEYS_MODEL = keysModel().build(); + private static final String KEY_ID = "id"; + + @BeforeEach + void configureMocks() { + MontoyaObjectFactory factory = ObjectFactoryLocator.FACTORY; + + when(factory.usePayload(any(ByteArray.class))).thenAnswer((Answer) i -> + new FakePayloadProcessingResult(i.getArgument(0, ByteArray.class))); + + when(factory.byteArray(anyString())).thenAnswer((Answer) i -> + new FakeByteArray(i.getArgument(0, String.class))); + } + + @Test + void givenBaseValueNotJWS_whenPayloadProcessed_thenPayloadLeftUnchanged() { + String baseValue = "isogeny"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).build(); + IntruderConfig intruderConfig = intruderConfig().withFuzzParameter("role").withFuzzLocation(PAYLOAD).build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, EMPTY_KEYS_MODEL); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo(baseValue); + } + + @Test + void givenBaseValueJWSAndFuzzParameterNotPresent_whenPayloadProcessed_thenPayloadLeftUnchanged() { + String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).build(); + IntruderConfig intruderConfig = intruderConfig().withFuzzParameter("role").withFuzzLocation(PAYLOAD).build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, EMPTY_KEYS_MODEL); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo(baseValue); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInWrongContext_whenPayloadProcessed_thenPayloadLeftUnchanged() { + String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).build(); + IntruderConfig intruderConfig = intruderConfig().withFuzzParameter("alg").withFuzzLocation(PAYLOAD).build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, EMPTY_KEYS_MODEL); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo(baseValue); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInHeader_whenPayloadProcessed_thenPayloadModified() { + String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("RS256").build(); + IntruderConfig intruderConfig = intruderConfig().withFuzzParameter("alg").withFuzzLocation(HEADER).build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, EMPTY_KEYS_MODEL); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInPayload_whenPayloadProcessed_thenPayloadModified() { + String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("emanon").build(); + IntruderConfig intruderConfig = intruderConfig().withFuzzParameter("name").withFuzzLocation(PAYLOAD).build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, EMPTY_KEYS_MODEL); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVtYW5vbiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInPayload_whenSigningKeyLoadedButResignOff_thenPayloadModifiedButNotResigned() { + String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("emanon").build(); + KeysModel keysModel = keysModel().withRSAKey(RSA1024Private, KEY_ID).build(); + IntruderConfig intruderConfig = intruderConfig() + .withFuzzParameter("name") + .withFuzzLocation(PAYLOAD) + .withSigningKeyId(KEY_ID) + .withSigningAlgorithm(RS512) + .build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, keysModel); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVtYW5vbiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInPayload_whenResignOnButUnknownSigningKeyConfigured_thenPayloadModifiedButNotResigned() { + String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("emanon").build(); + KeysModel keysModel = keysModel().withRSAKey(RSA1024Private, KEY_ID).build(); + IntruderConfig intruderConfig = intruderConfig() + .withFuzzParameter("name") + .withFuzzLocation(PAYLOAD) + .withSigningKeyId("rogue") + .withSigningAlgorithm(RS512) + .withResigning(true) + .build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, keysModel); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVtYW5vbiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInPayload_whenResignOnAndSigningKeyPresentAndAlgorithmUnchanged_thenPayloadModifiedAndResignedButAlgUnchanged() { + String baseValue = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.LX5A6Hu00jlQ2n8s1SoVL4BEPjMiF1zrEs3qRtV08sbmsqxXV8bc8LarGm8YZj2OuXWL7aOkdBc9ezOBi5bjxsrtiUmwo5VWlU5Y6PXqGwH5v7w0kpRckdd0IA3nbrR2SyLQ1L1pQJk2PzoCvEpspBPMxtIyrK5MTep3Yx1Xn3aiw3aE1cHzOwK0xBIg-RW5qK5PwPa4H8T7eOOSMytS6N4AiZbeiIVHBWxmjrdp8AuC_fmfM1TQA_O8gK_1QkK3jPWkmbbtb48ut6dxz3H_gvPkPzsRE96nQ1qcOlbJjN0URcR2Tc1ACwZO4VpY4gujo_LwTsLiKQcmq0glFA3SIw"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("emanon").build(); + KeysModel keysModel = keysModel().withRSAKey(RSA1024Private, KEY_ID).build(); + IntruderConfig intruderConfig = intruderConfig() + .withFuzzParameter("name") + .withFuzzLocation(PAYLOAD) + .withSigningKeyId(KEY_ID) + .withSigningAlgorithm(RS512) + .withResigning(true) + .build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, keysModel); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzUxMiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVtYW5vbiIsImlhdCI6MTUxNjIzOTAyMn0.poPOxqjqp-CnC2b7eaf2QvfvAfawzp6k-P1QECIHN7KCTnFIlQbiJC4ZtLPH_0-o3HQcUGZbib3m1CVWeY21FIUTVUmOyjU8XuBohtBXRlXoaKXVWibrm5YiLC3yNQz5uAF-gdBB8ybvsmetK7JIZ8UvQdJ3mdvlAAW-3xFv8fs"); + } + + @Test + void givenBaseValueJWSAndFuzzParameterPresentInPayload_whenResignOnAndSigningKeyPresentAndAlgorithmChanged_thenPayloadAndAlgModifiedAndResigned() { + String baseValue = "eyJhbGciOiJIUzI1NiIsImtpZCI6ImNyZXMiLCJ0eXAiOiJKV1QifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; + PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("emanon").build(); + KeysModel keysModel = keysModel().withRSAKey(RSA1024Private, KEY_ID).build(); + IntruderConfig intruderConfig = intruderConfig() + .withFuzzParameter("name") + .withFuzzLocation(PAYLOAD) + .withSigningKeyId(KEY_ID) + .withSigningAlgorithm(RS256) + .withResigning(true) + .build(); + + JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig, LOGGING, keysModel); + PayloadProcessingResult result = processor.processPayload(payloadData); + + assertThat(result.action()).isEqualTo(USE_PAYLOAD); + assertThat(result.processedPayload().toString()).isEqualTo("eyJraWQiOiJjcmVzIiwidHlwIjoiSldUIiwiYWxnIjoiUlMyNTYifQ.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVtYW5vbiIsImlhdCI6MTUxNjIzOTAyMn0.qZw9QLyX5NAxBioegUsiBoWVpVShdy1EGHwwgOtknyVXhXqQDbXt15IYUkj66_dDIGNuZ76g_BdJLy9V9qG-sTbQgsLykbfLfdqelNhOsw-c_gs7eJPK0DYaYmBZNJayD5EJoOTF0Vs0AR2Rlnhf1PeoK04HZfJhoDS39gs4hRU"); + } +} \ No newline at end of file diff --git a/src/test/java/burp/intruder/JWTPayloadProcessorTest.java b/src/test/java/burp/intruder/JWTPayloadProcessorTest.java deleted file mode 100644 index 396be22..0000000 --- a/src/test/java/burp/intruder/JWTPayloadProcessorTest.java +++ /dev/null @@ -1,105 +0,0 @@ -package burp.intruder; - -import burp.api.montoya.MontoyaExtension; -import burp.api.montoya.core.ByteArray; -import burp.api.montoya.core.FakeByteArray; -import burp.api.montoya.internal.MontoyaObjectFactory; -import burp.api.montoya.internal.ObjectFactoryLocator; -import burp.api.montoya.intruder.FakePayloadProcessingResult; -import burp.api.montoya.intruder.PayloadData; -import burp.api.montoya.intruder.PayloadProcessingResult; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.stubbing.Answer; - -import static burp.api.montoya.intruder.FakePayloadData.payloadData; -import static burp.api.montoya.intruder.PayloadProcessingAction.USE_PAYLOAD; -import static burp.intruder.FuzzLocation.HEADER; -import static burp.intruder.FuzzLocation.PAYLOAD; -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.Mockito.when; - -@ExtendWith(MontoyaExtension.class) -class JWTPayloadProcessorTest { - @BeforeEach - void configureMocks() { - MontoyaObjectFactory factory = ObjectFactoryLocator.FACTORY; - - when(factory.usePayload(any(ByteArray.class))).thenAnswer((Answer) i -> - new FakePayloadProcessingResult(i.getArgument(0, ByteArray.class))); - - when(factory.byteArray(anyString())).thenAnswer((Answer) i -> - new FakeByteArray(i.getArgument(0, String.class))); - } - - @Test - void givenBaseValueNotJWS_whenPayloadProcessed_thenPayloadLeftUnchanged() { - String baseValue = "isogeny"; - PayloadData payloadData = payloadData().withBaseValue(baseValue).build(); - JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig("role", PAYLOAD)); - - PayloadProcessingResult result = processor.processPayload(payloadData); - - assertThat(result.action()).isEqualTo(USE_PAYLOAD); - assertThat(result.processedPayload().toString()).isEqualTo(baseValue); - } - - @Test - void givenBaseValueJWSAndFuzzParameterNotPresent_whenPayloadProcessed_thenPayloadLeftUnchanged() { - String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - PayloadData payloadData = payloadData().withBaseValue(baseValue).build(); - JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig("role", PAYLOAD)); - - PayloadProcessingResult result = processor.processPayload(payloadData); - - assertThat(result.action()).isEqualTo(USE_PAYLOAD); - assertThat(result.processedPayload().toString()).isEqualTo(baseValue); - } - - @Test - void givenBaseValueJWSAndFuzzParameterPresentInWrongContext_whenPayloadProcessed_thenPayloadLeftUnchanged() { - String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - PayloadData payloadData = payloadData().withBaseValue(baseValue).build(); - JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig("alg", PAYLOAD)); - - PayloadProcessingResult result = processor.processPayload(payloadData); - - assertThat(result.action()).isEqualTo(USE_PAYLOAD); - assertThat(result.processedPayload().toString()).isEqualTo(baseValue); - } - - @Test - void givenBaseValueJWSAndFuzzParameterPresentInHeader_whenPayloadProcessed_thenPayloadModified() { - String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("RS256").build(); - JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig("alg", HEADER)); - - PayloadProcessingResult result = processor.processPayload(payloadData); - - assertThat(result.action()).isEqualTo(USE_PAYLOAD); - assertThat(result.processedPayload().toString()).isEqualTo("eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); - } - - @Test - void givenBaseValueJWSAndFuzzParameterPresentInPayload_whenPayloadProcessed_thenPayloadModified() { - String baseValue = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; - PayloadData payloadData = payloadData().withBaseValue(baseValue).withCurrentPayload("emanon").build(); - JWSPayloadProcessor processor = new JWSPayloadProcessor(intruderConfig("name", PAYLOAD)); - - PayloadProcessingResult result = processor.processPayload(payloadData); - - assertThat(result.action()).isEqualTo(USE_PAYLOAD); - assertThat(result.processedPayload().toString()).isEqualTo("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6ImVtYW5vbiIsImlhdCI6MTUxNjIzOTAyMn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); - } - - private static IntruderConfig intruderConfig(String parameterName, FuzzLocation parameterLocation) { - IntruderConfig intruderConfig = new IntruderConfig(); - intruderConfig.setFuzzParameter(parameterName); - intruderConfig.setFuzzLocation(parameterLocation); - - return intruderConfig; - } -} \ No newline at end of file diff --git a/src/test/java/com/blackberry/jwteditor/KeyLoader.java b/src/test/java/com/blackberry/jwteditor/KeyLoader.java new file mode 100644 index 0000000..320d7a1 --- /dev/null +++ b/src/test/java/com/blackberry/jwteditor/KeyLoader.java @@ -0,0 +1,56 @@ +/* +Author : Dolph Flynn + +Copyright 2024 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; + +import com.blackberry.jwteditor.exceptions.PemException; +import com.blackberry.jwteditor.exceptions.UnsupportedKeyException; +import com.blackberry.jwteditor.model.keys.JWKKeyFactory; +import com.blackberry.jwteditor.model.keys.Key; +import com.blackberry.jwteditor.utils.PEMUtils; +import com.nimbusds.jose.jwk.JWK; + +public class KeyLoader { + + public static Key loadECKey(String pem, String keyId) { + try { + JWK jwk = PEMUtils.pemToECKey(pem, keyId); + return JWKKeyFactory.from(jwk); + } catch (PemException | UnsupportedKeyException e) { + throw new IllegalStateException(e); + } + } + + public static Key loadRSAKey(String pem, String keyId) { + try { + JWK jwk = PEMUtils.pemToRSAKey(pem, keyId); + return JWKKeyFactory.from(jwk); + } catch (PemException | UnsupportedKeyException e) { + throw new IllegalStateException(e); + } + } + + public static Key loadOKPKey(String pem, String keyId) { + try { + JWK jwk = PEMUtils.pemToOctetKeyPair(pem, keyId); + return JWKKeyFactory.from(jwk); + } catch (PemException | UnsupportedKeyException e) { + throw new IllegalStateException(e); + } + } +} diff --git a/src/test/java/com/blackberry/jwteditor/KeysModelBuilder.java b/src/test/java/com/blackberry/jwteditor/KeysModelBuilder.java index 647ddfb..5f80128 100644 --- a/src/test/java/com/blackberry/jwteditor/KeysModelBuilder.java +++ b/src/test/java/com/blackberry/jwteditor/KeysModelBuilder.java @@ -18,63 +18,58 @@ package com.blackberry.jwteditor; -import com.blackberry.jwteditor.exceptions.PemException; -import com.blackberry.jwteditor.exceptions.UnsupportedKeyException; -import com.blackberry.jwteditor.model.keys.JWKKeyFactory; import com.blackberry.jwteditor.model.keys.Key; import com.blackberry.jwteditor.model.keys.KeysModel; -import com.blackberry.jwteditor.utils.PEMUtils; -import com.nimbusds.jose.jwk.JWK; import java.util.concurrent.atomic.AtomicInteger; -class KeysModelBuilder { +import static com.blackberry.jwteditor.KeyLoader.*; + +public class KeysModelBuilder { private final AtomicInteger keyId = new AtomicInteger(); private final KeysModel model = new KeysModel(); - KeysModelBuilder withECKey(String pem) { - try { - JWK jwk = PEMUtils.pemToECKey(pem, Integer.toString(keyId.incrementAndGet())); - model.addKey(JWKKeyFactory.from(jwk)); - } catch (PemException | UnsupportedKeyException e) { - throw new IllegalStateException(e); - } + public KeysModelBuilder withECKey(String pem) { + return withECKey(pem, nextKeyId()); + } + public KeysModelBuilder withECKey(String pem, String keyId) { + model.addKey(loadECKey(pem, keyId)); return this; } - KeysModelBuilder withRSAKey(String pem) { - try { - JWK jwk = PEMUtils.pemToRSAKey(pem, Integer.toString(keyId.incrementAndGet())); - model.addKey(JWKKeyFactory.from(jwk)); - } catch (PemException | UnsupportedKeyException e) { - throw new IllegalStateException(e); - } + public KeysModelBuilder withRSAKey(String pem) { + return withRSAKey(pem, nextKeyId()); + } + public KeysModelBuilder withRSAKey(String pem, String keyId) { + model.addKey(loadRSAKey(pem, keyId)); return this; } - KeysModelBuilder withOKPKey(String pem) { - try { - JWK jwk = PEMUtils.pemToOctetKeyPair(pem, Integer.toString(keyId.incrementAndGet())); - model.addKey(JWKKeyFactory.from(jwk)); - } catch (PemException | UnsupportedKeyException e) { - throw new IllegalStateException(e); - } + public KeysModelBuilder withOKPKey(String pem) { + return withOKPKey(pem, nextKeyId()); + } + public KeysModelBuilder withOKPKey(String pem, String keyId) { + model.addKey(loadOKPKey(pem, keyId)); return this; } - KeysModelBuilder withKey(Key key) { + public KeysModelBuilder withKey(Key key) { model.addKey(key); return this; } - KeysModel build() { + public KeysModel build() { return model; } - static KeysModelBuilder keysModel() { + private String nextKeyId() { + return Integer.toString(keyId.incrementAndGet()); + } + + public static KeysModelBuilder keysModel() { return new KeysModelBuilder(); } } diff --git a/src/test/java/com/blackberry/jwteditor/KeysModelTest.java b/src/test/java/com/blackberry/jwteditor/KeysModelTest.java index 0db1b38..a5d11e4 100644 --- a/src/test/java/com/blackberry/jwteditor/KeysModelTest.java +++ b/src/test/java/com/blackberry/jwteditor/KeysModelTest.java @@ -18,12 +18,16 @@ package com.blackberry.jwteditor; +import com.blackberry.jwteditor.model.keys.Key; import com.blackberry.jwteditor.model.keys.KeysModel; -import com.blackberry.jwteditor.model.keys.KeysModelListener.InertKeyModelListener; +import com.blackberry.jwteditor.model.keys.KeysModelListener; +import com.blackberry.jwteditor.model.keys.KeysModelListener.InertKeysModelListener; import com.blackberry.jwteditor.model.keys.PasswordKey; import org.junit.jupiter.api.Test; +import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicInteger; +import java.util.concurrent.atomic.AtomicReference; import static com.blackberry.jwteditor.KeysModelBuilder.keysModel; import static data.PemData.*; @@ -111,7 +115,7 @@ void deleteMultipleKeys_listenerOnlyFiresOncePerKey() { .withKey(new PasswordKey("testKeyId", "secret", 8, 1337)) .withKey(new PasswordKey("another", "shrubbery", 8, 1337)) .build(); - model.addKeyModelListener(new InertKeyModelListener() { + model.addKeyModelListener(new InertKeysModelListener() { @Override public void notifyKeyDeleted(int rowIndex) { noOfListenerInvocations.incrementAndGet(); @@ -122,4 +126,52 @@ public void notifyKeyDeleted(int rowIndex) { assertThat(noOfListenerInvocations.get()).isEqualTo(4); } + + @Test + void addKeyWithNewId_listenerOnlyNotifiedOfKeyInserted() { + AtomicBoolean keyDeleted = new AtomicBoolean(); + AtomicReference insertedKey = new AtomicReference<>(); + Key key = new PasswordKey("testKeyId", "secret", 8, 1337); + KeysModel model = new KeysModel(); + + model.addKeyModelListener(new KeysModelListener() { + @Override + public void notifyKeyInserted(Key key) { + insertedKey.compareAndSet(null, key); + } + + @Override + public void notifyKeyDeleted(int rowIndex) { + keyDeleted.set(true); + } + + @Override + public void notifyKeyDeleted(Key key) { + keyDeleted.set(true); + } + }); + + model.addKey(key); + + assertThat(insertedKey.get()).isSameAs(key); + assertThat(keyDeleted).isFalse(); + } + + @Test + void addKeyWithExistingId_listenerNotifiedOfKeyDeletion() { + AtomicReference deletedKey = new AtomicReference<>(); + Key oldKey = new PasswordKey("testKeyId", "secret", 8, 1337); + KeysModel model = keysModel().withKey(oldKey).build(); + + model.addKeyModelListener(new InertKeysModelListener() { + @Override + public void notifyKeyDeleted(Key key) { + deletedKey.set(key); + } + }); + + model.addKey(new PasswordKey("testKeyId", "sci", 8, 1337)); + + assertThat(deletedKey.get()).isSameAs(oldKey); + } } \ No newline at end of file diff --git a/src/test/java/com/blackberry/jwteditor/view/config/IntruderConfigModelTest.java b/src/test/java/com/blackberry/jwteditor/view/config/IntruderConfigModelTest.java new file mode 100644 index 0000000..a78954d --- /dev/null +++ b/src/test/java/com/blackberry/jwteditor/view/config/IntruderConfigModelTest.java @@ -0,0 +1,480 @@ +package com.blackberry.jwteditor.view.config; + +import burp.intruder.FuzzLocation; +import burp.intruder.IntruderConfig; +import com.blackberry.jwteditor.model.keys.KeysModel; +import com.nimbusds.jose.JWSAlgorithm; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; + +import java.beans.PropertyChangeEvent; +import java.util.ArrayList; +import java.util.List; +import java.util.NoSuchElementException; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicReference; + +import static burp.intruder.FuzzLocation.HEADER; +import static com.blackberry.jwteditor.KeyLoader.*; +import static com.blackberry.jwteditor.view.config.IntruderConfigModel.*; +import static com.nimbusds.jose.JWSAlgorithm.*; +import static data.PemData.*; +import static java.util.Arrays.asList; +import static java.util.Arrays.stream; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertThrows; + +class IntruderConfigModelTest { + private static final String KEY_ID = "id"; + + private final KeysModel keysModel = new KeysModel(); + private final IntruderConfig wrappedConfig = new IntruderConfig(); + private final IntruderConfigModel model = new IntruderConfigModel(keysModel, wrappedConfig); + + @AfterEach + void cleanUp() { + model.clearListeners(); + stream(model.signingKeyIds()).forEach(keysModel::deleteKey); + } + + @Test + void whenSetFuzzParameter_thenValueSetOnModelAndWrappedConfig() { + String parameter = "kid"; + model.setFuzzParameter(parameter); + + assertThat(model.fuzzParameter()).isEqualTo(parameter); + assertThat(wrappedConfig.fuzzParameter()).isEqualTo(parameter); + } + + @Test + void whenGetFuzzParameter_thenValueRetrievedFromWrappedConfig() { + String parameter = "kid"; + wrappedConfig.setFuzzParameter(parameter); + + assertThat(model.fuzzParameter()).isEqualTo(parameter); + } + + @Test + void whenSetFuzzLocation_thenValueSetOnModelAndWrappedConfig() { + FuzzLocation location = HEADER; + model.setFuzzLocation(location); + + assertThat(model.fuzzLocation()).isEqualTo(location); + assertThat(wrappedConfig.fuzzLocation()).isEqualTo(location); + } + + @Test + void whenGetFuzzLocation_thenValueRetrievedFromWrappedConfig() { + FuzzLocation location = HEADER; + wrappedConfig.setFuzzLocation(location); + + assertThat(model.fuzzLocation()).isEqualTo(location); + } + + @Test + void whenGetFuzzLocations_thenAllFuzzLocationValuesReturned() { + assertThat(model.fuzzLocations()).containsExactly(FuzzLocation.values()); + } + + @Test + void whenEmptyKeysModel_thenHasSigningKeyIsFalse() { + assertThat(model.hasSigningKeys()).isFalse(); + } + + @Test + void whenEmptyKeysModel_thenNoSigningKeyIds() { + assertThat(model.signingKeyIds()).isEmpty(); + } + + @Test + void whenEmptyKeysModel_thenSelectedSigningKeyIdIsNull() { + assertThat(model.signingKeyId()).isNull(); + } + + @Test + void whenEmptyKeysModel_thenNoSigningAlgorithms() { + assertThat(model.signingAlgorithms()).isEmpty(); + } + + @Test + void whenEmptyKeysModel_thenSelectedSigningAlgorithmsIsNull() { + assertThat(model.signingAlgorithm()).isNull(); + } + + @Test + void whenEmptyKeysModel_thenResignIsFalse() { + wrappedConfig.setResign(true); + + assertThat(model.resign()).isFalse(); + } + + @Test + void whenKeysModelNotEmptyButHasNoSigningKeys_thenHasSigningKeysIsFalse() { + keysModel.addKey(loadOKPKey(X25519Public, "kid")); + + assertThat(model.hasSigningKeys()).isFalse(); + } + + @Test + void whenKeysModelHasSigningKeys_thenHasSigningKeysIsTrue() { + keysModel.addKey(loadRSAKey(RSA1024Private, "kid")); + + assertThat(model.hasSigningKeys()).isTrue(); + } + + @Test + void whenKeysModelNotEmptyButHasNoSigningKeys_thenSigningKeyIdsEmpty() { + keysModel.addKey(loadOKPKey(X25519Public, "kid")); + + assertThat(model.signingKeyIds()).isEmpty(); + } + + @Test + void whenKeysModelHasSigningKeys_thenSigningKeyIdsNotEmpty() { + keysModel.addKey(loadRSAKey(RSA1024Private, "kid1")); + keysModel.addKey(loadECKey(PRIME256v1PrivateSEC1, "kid2")); + + assertThat(model.signingKeyIds()).containsExactly("kid1", "kid2"); + } + + @Test + void whenKeysModelHasSigningAndNonSigningKeys_thenSigningKeyIdsContainsOnlySigningKeyIds() { + keysModel.addKey(loadRSAKey(RSA1024Private, "kid1")); + keysModel.addKey(loadOKPKey(X25519Public, "kid2")); + keysModel.addKey(loadECKey(PRIME256v1PrivateSEC1, "kid3")); + + assertThat(model.signingKeyIds()).containsExactly("kid1", "kid3"); + } + + @Test + void givenKeysModelHasSigningKey_whenSetSigningKeyId_thenSigningKeyAndSigningAlgorithmsUpdated() { + String keyId = "kid1"; + keysModel.addKey(loadRSAKey(RSA1024Private, keyId)); + + model.setSigningKeyId(keyId); + + assertThat(model.signingKeyId()).isEqualTo(keyId); + assertThat(wrappedConfig.signingKeyId()).isEqualTo(keyId); + assertThat(model.signingAlgorithms()).containsExactly(RS256, RS384, RS512, PS256, PS384); + } + + @Test + void givenKeysModelHasSigningKey_whenSetInvalidSigningKeyId_thenExceptionThrown() { + keysModel.addKey(loadRSAKey(RSA1024Private, "kid1")); + + assertThrows(NoSuchElementException.class, () -> model.setSigningKeyId("invalid")); + } + + @Test + void givenKeysModelHasSigningKey_whenSetSigningKeyId_thenSigningAlgorithmsUpdatedEventPublished() { + List publishedAlgorithms = new ArrayList<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SIGNING_ALGORITHMS_UPDATED) && evt.getNewValue() instanceof JWSAlgorithm[] algorithms) { + publishedAlgorithms.addAll(asList(algorithms)); + } + }); + + String keyId = "kid1"; + keysModel.addKey(loadRSAKey(RSA1024Private, keyId)); + + model.setSigningKeyId(keyId); + + assertThat(publishedAlgorithms).containsExactly(RS256, RS384, RS512, PS256, PS384); + } + + @Test + void givenKeysModelHasSigningKey_whenSetAlgorithm_thenAlgorithmStored() { + String keyId = "kid1"; + keysModel.addKey(loadRSAKey(RSA1024Private, keyId)); + + model.setSigningAlgorithm(PS256); + + assertThat(model.signingAlgorithm()).isEqualTo(PS256); + assertThat(wrappedConfig.signingAlgorithm()).isEqualTo(PS256); + } + + @Test + void givenNoSelectedKeyIdOrAlgorithm_whenSetResign_thenResignFalse() { + model.setResign(true); + + assertThat(model.resign()).isFalse(); + } + + @Test + void givenSelectedAlgorithmButNoKeyId_whenSetResign_thenResignFalse() { + model.setSigningAlgorithm(EdDSA); + + model.setResign(true); + + assertThat(model.resign()).isFalse(); + } + + @Test + void givenSelectedKeyIdAndAlgorithm_whenSetResign_thenResignFalse() { + String keyId = "kid"; + keysModel.addKey(loadRSAKey(RSA1024Private, keyId)); + model.setSigningKeyId(keyId); + model.setSigningAlgorithm(RS256); + + model.setResign(true); + + assertThat(model.resign()).isTrue(); + } + + @Test + void whenKeyAddedToModel_thenSigningKeysUpdatedEventFired() { + List oldIds = new ArrayList<>(); + List newIds = new ArrayList<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SIGNING_KEYS_UPDATED)) { + oldIds.addAll(asList((String[]) evt.getOldValue())); + newIds.addAll(asList((String[]) evt.getNewValue())); + } + }); + + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + + assertThat(oldIds).isEmpty(); + assertThat(newIds).containsExactly(KEY_ID); + } + + @Test + void whenKeyDeletedFromModel_thenSigningKeysUpdatedEventFired() { + List oldIds = new ArrayList<>(); + List newIds = new ArrayList<>(); + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SIGNING_KEYS_UPDATED)) { + oldIds.addAll(asList((String[]) evt.getOldValue())); + newIds.addAll(asList((String[]) evt.getNewValue())); + } + }); + + keysModel.deleteKey(KEY_ID); + + assertThat(oldIds).containsExactly(KEY_ID); + assertThat(newIds).isEmpty(); + } + + @Test + void givenSelectedKey_whenSelectedKeyDeletedFromModel_thenSelectedKeyUpdatedFired() { + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + model.setSigningKeyId(KEY_ID); + AtomicReference event = new AtomicReference<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SELECTED_KEY_UPDATED)) { + event.set(evt); + } + }); + + keysModel.deleteKey(KEY_ID); + + assertThat(event.get().getOldValue()).isEqualTo(KEY_ID); + assertThat(event.get().getNewValue()).isNull(); + } + + @Test + void givenSelectedKey_whenSelectedKeyDeletedFromModel_thenSigningAlgorithmsEventUpdatedFired() { + List oldAlgorithms = new ArrayList<>(); + List newAlgorithms = new ArrayList<>(); + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + model.setSigningKeyId(KEY_ID); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SIGNING_ALGORITHMS_UPDATED)) { + oldAlgorithms.addAll(asList((JWSAlgorithm[]) evt.getOldValue())); + newAlgorithms.addAll(asList((JWSAlgorithm[]) evt.getNewValue())); + } + }); + + keysModel.deleteKey(KEY_ID); + + assertThat(oldAlgorithms).containsExactly(RS256, RS384, RS512, PS256, PS384); + assertThat(newAlgorithms).isEmpty(); + } + + @Test + void givenSelectedAlgorithm_whenSelectedKeyDeletedFromModel_thenSelectedAlgorithmUpdatedFired() { + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + model.setSigningKeyId(KEY_ID); + model.setSigningAlgorithm(RS256); + AtomicReference event = new AtomicReference<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SELECTED_ALGORITHM_UPDATED)) { + event.set(evt); + } + }); + + keysModel.deleteKey(KEY_ID); + + assertThat(event.get().getOldValue()).isEqualTo(RS256); + assertThat(event.get().getNewValue()).isNull(); + } + + @Test + void givenSelectedAlgorithm_whenResigningAndSelectedKeyDeleted_thenResignDisabled() { + AtomicBoolean resign = new AtomicBoolean(true); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(RESIGN_UPDATED) && evt.getNewValue() instanceof Boolean selected) { + resign.set(selected); + } + }); + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + model.setResign(true); + + keysModel.deleteKey(KEY_ID); + + assertThat(model.resign()).isFalse(); + assertThat(resign).isFalse(); + } + + @Test + void givenEmptyModel_whenKeyLoaded_thenKeyAndFirstAlgorithmSelected() { + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + + assertThat(model.signingKeyId()).isEqualTo(KEY_ID); + assertThat(wrappedConfig.signingKeyId()).isEqualTo(KEY_ID); + assertThat(model.signingAlgorithm()).isEqualTo(RS256); + assertThat(wrappedConfig.signingAlgorithm()).isEqualTo(RS256); + assertThat(model.signingAlgorithms()).containsExactly(RS256, RS384, RS512, PS256, PS384); + } + + @Test + void givenEmptyModel_whenKeyLoaded_thenSigningKeysUpdatedEventFired() { + List oldIds = new ArrayList<>(); + List newIds = new ArrayList<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SIGNING_KEYS_UPDATED)) { + oldIds.addAll(asList((String[]) evt.getOldValue())); + newIds.addAll(asList((String[]) evt.getNewValue())); + } + }); + + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + + assertThat(oldIds).isEmpty(); + assertThat(newIds).containsExactly(KEY_ID); + } + + @Test + void givenEmptyModel_whenKeyLoaded_thenSelectedKeyUpdatedEventFired() { + AtomicReference oldKeyId = new AtomicReference<>(); + AtomicReference newKeyId = new AtomicReference<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SELECTED_KEY_UPDATED)) { + oldKeyId.set((String) evt.getOldValue()); + newKeyId.set((String) evt.getNewValue()); + } + }); + + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + + assertThat(oldKeyId.get()).isNull(); + assertThat(newKeyId.get()).isEqualTo(KEY_ID); + } + + @Test + void givenEmptyModel_whenKeyLoaded_thenSigningAlgorithmsUpdatedEventFired() { + List oldAlgorithms = new ArrayList<>(); + List newAlgorithms = new ArrayList<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SIGNING_ALGORITHMS_UPDATED)) { + oldAlgorithms.addAll(asList((JWSAlgorithm[]) evt.getOldValue())); + newAlgorithms.addAll(asList((JWSAlgorithm[]) evt.getNewValue())); + } + }); + + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + + assertThat(oldAlgorithms).isEmpty(); + assertThat(newAlgorithms).containsExactly(RS256, RS384, RS512, PS256, PS384); + } + + @Test + void givenEmptyModel_whenKeyLoaded_thenSelectedAlgorithmUpdatedEventFired() { + AtomicReference oldSigningAlgorithm = new AtomicReference<>(); + AtomicReference newSigningAlgorithm = new AtomicReference<>(); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(SELECTED_ALGORITHM_UPDATED)) { + oldSigningAlgorithm.set((JWSAlgorithm) evt.getOldValue()); + newSigningAlgorithm.set((JWSAlgorithm) evt.getNewValue()); + } + }); + + keysModel.addKey(loadRSAKey(RSA1024Private, KEY_ID)); + + assertThat(oldSigningAlgorithm.get()).isNull(); + assertThat(newSigningAlgorithm.get()).isEqualTo(RS256); + } + + @Test + void givenTwoKeysInModel_whenSelectedKeyDeleted_thenRemainingKeyAndFirstAlgorithmSelected() { + String kid = "kid2"; + keysModel.addKey(loadRSAKey(RSA1024Private, kid)); + keysModel.addKey(loadOKPKey(ED25519Private, KEY_ID)); + + keysModel.deleteKey(kid); + + assertThat(model.signingKeyId()).isEqualTo(KEY_ID); + assertThat(wrappedConfig.signingKeyId()).isEqualTo(KEY_ID); + assertThat(model.signingAlgorithm()).isEqualTo(EdDSA); + assertThat(wrappedConfig.signingAlgorithm()).isEqualTo(EdDSA); + assertThat(model.signingAlgorithms()).containsExactly(EdDSA); + } + + @Test + void givenTwoKeysInModel_whenResigningAndSelectedKeyDeleted_thenResignDisabled() { + AtomicBoolean resign = new AtomicBoolean(true); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(RESIGN_UPDATED) && evt.getNewValue() instanceof Boolean selected) { + resign.set(selected); + } + }); + + String kid = "kid2"; + keysModel.addKey(loadRSAKey(RSA1024Private, kid)); + keysModel.addKey(loadOKPKey(ED25519Private, KEY_ID)); + model.setResign(true); + + keysModel.deleteKey(kid); + + assertThat(model.resign()).isFalse(); + assertThat(resign).isFalse(); + } + + @Test + void givenMultipleKeysInModel_whenSelectedKeyDeleted_thenFirstKeyAndAlgorithmSelected() { + String kid = "kid2"; + keysModel.addKey(loadRSAKey(RSA1024Private, kid)); + keysModel.addKey(loadOKPKey(ED25519Private, KEY_ID)); + keysModel.addKey(loadOKPKey(X448Private, "kid3")); + + keysModel.deleteKey(kid); + + assertThat(model.signingKeyId()).isEqualTo(KEY_ID); + assertThat(wrappedConfig.signingKeyId()).isEqualTo(KEY_ID); + assertThat(model.signingAlgorithm()).isEqualTo(EdDSA); + assertThat(wrappedConfig.signingAlgorithm()).isEqualTo(EdDSA); + assertThat(model.signingAlgorithms()).containsExactly(EdDSA); + } + + @Test + void givenMultipleKeysInModel_whenResigningAndSelectedKeyDeleted_thenResignDisabled() { + AtomicBoolean resign = new AtomicBoolean(true); + model.addPropertyChangeListener(evt -> { + if (evt.getPropertyName().equals(RESIGN_UPDATED) && evt.getNewValue() instanceof Boolean selected) { + resign.set(selected); + } + }); + + String kid = "kid2"; + keysModel.addKey(loadRSAKey(RSA1024Private, kid)); + keysModel.addKey(loadOKPKey(ED25519Private, KEY_ID)); + keysModel.addKey(loadOKPKey(X448Private, "kid3")); + model.setResign(true); + + keysModel.deleteKey(kid); + + assertThat(model.resign()).isFalse(); + assertThat(resign).isFalse(); + } +} \ No newline at end of file