├── settings.gradle ├── docs └── screenshots │ ├── ui-example.png │ ├── import-profiles.png │ └── profile-editor.png ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src └── main │ └── java │ └── burp │ ├── error │ └── SigCredentialProviderException.java │ ├── SigCredentialProvider.java │ ├── SigStaticCredential.java │ ├── SigStaticCredentialProvider.java │ ├── MultilineLabel.java │ ├── SigCredential.java │ ├── ConfigParser.java │ ├── SigCredentialSerializer.java │ ├── SigTemporaryCredential.java │ ├── SigCredentialProviderSerializer.java │ ├── LogWriter.java │ ├── SigProfileTestDialog.java │ ├── ExtensionSettings.java │ ├── SigProfileEditorReadOnlyDialog.java │ ├── SigMessageEditorTab.java │ ├── SdkHttpClientForBurp.java │ ├── SigAwsProfileCredentialProvider.java │ ├── SigHttpCredentialProvider.java │ ├── JSONCredentialParser.java │ ├── SigAssumeRoleCredentialProvider.java │ ├── SigProfileImportDialog.java │ ├── AdvancedSettingsDialog.java │ ├── SigProfileEditorDialog.java │ └── SigProfile.java ├── .idea └── misc.xml ├── BappManifest.bmf ├── LICENSE ├── BappDescription.html ├── gradlew.bat ├── .gitignore ├── SETTINGS.md ├── README.md └── gradlew /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'aws-sigv4' 2 | -------------------------------------------------------------------------------- /docs/screenshots/ui-example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/aws-sigv4/master/docs/screenshots/ui-example.png -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/aws-sigv4/master/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /docs/screenshots/import-profiles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/aws-sigv4/master/docs/screenshots/import-profiles.png -------------------------------------------------------------------------------- /docs/screenshots/profile-editor.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PortSwigger/aws-sigv4/master/docs/screenshots/profile-editor.png -------------------------------------------------------------------------------- /src/main/java/burp/error/SigCredentialProviderException.java: -------------------------------------------------------------------------------- 1 | package burp.error; 2 | 3 | public class SigCredentialProviderException extends RuntimeException { 4 | public SigCredentialProviderException(String msg) 5 | { 6 | super(msg); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Sat May 2 02:46:18 UTC 2020 2 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-all.zip 3 | distributionBase=GRADLE_USER_HOME 4 | distributionPath=wrapper/dists 5 | zipStorePath=wrapper/dists 6 | zipStoreBase=GRADLE_USER_HOME 7 | -------------------------------------------------------------------------------- /.idea/misc.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/java/burp/SigCredentialProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.error.SigCredentialProviderException; 4 | 5 | public interface SigCredentialProvider 6 | { 7 | SigCredential getCredential() throws SigCredentialProviderException; 8 | String getName(); 9 | String getClassName(); 10 | } 11 | -------------------------------------------------------------------------------- /BappManifest.bmf: -------------------------------------------------------------------------------- 1 | Uuid: bbb81b07b7cd45448e8c728a38914c92 2 | ExtensionType: 1 3 | Name: AWS Sigv4 4 | RepoName: aws-sigv4 5 | ScreenVersion: 0.2.9 6 | SerialVersion: 6 7 | MinPlatformVersion: 2 8 | ProOnly: False 9 | Author: Anvil Secure 10 | ShortDescription: Used for signing AWS requests with SigV4. 11 | EntryPoint: aws-sigv4-0.2.9-all.jar 12 | BuildCommand: ./gradlew bigJar 13 | SupportedProducts: Pro, Community 14 | -------------------------------------------------------------------------------- /src/main/java/burp/SigStaticCredential.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | /* 4 | This class represents a permanent AWS credential generated with IAM 5 | */ 6 | public class SigStaticCredential extends SigCredential 7 | { 8 | private SigStaticCredential() {}; 9 | 10 | public SigStaticCredential(String accessKeyId, String secretKey) 11 | { 12 | setAccessKeyId(accessKeyId); 13 | setSecretKey(secretKey); 14 | } 15 | 16 | @Override 17 | public boolean isTemporary() { return false; } 18 | 19 | @Override 20 | public String getClassName() 21 | { 22 | return getClass().getName(); 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019 by Anvil Ventures Inc. 2 | 3 | Permission to use, copy, modify, and/or distribute this software for any 4 | purpose with or without fee is hereby granted. 5 | 6 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 7 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND 8 | FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 9 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 10 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 11 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 12 | PERFORMANCE OF THIS SOFTWARE. 13 | -------------------------------------------------------------------------------- /src/main/java/burp/SigStaticCredentialProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | public class SigStaticCredentialProvider implements SigCredentialProvider 4 | { 5 | 6 | public static final String PROVIDER_NAME = "Static"; 7 | 8 | private SigCredential credential; 9 | 10 | private SigStaticCredentialProvider() {}; 11 | 12 | public SigStaticCredentialProvider(SigCredential credential) 13 | { 14 | this.credential = credential; 15 | } 16 | 17 | @Override 18 | public SigCredential getCredential() { 19 | return credential; 20 | } 21 | 22 | @Override 23 | public String getName() { 24 | return PROVIDER_NAME; 25 | } 26 | 27 | @Override 28 | public String getClassName() { return getClass().getName();} 29 | 30 | } 31 | -------------------------------------------------------------------------------- /src/main/java/burp/MultilineLabel.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | 5 | /* 6 | Provides a JTextArea styled like a JLabel which handles multiline text. 7 | Text is not centered. 8 | 9 | Reference: https://stackoverflow.com/questions/26420428/how-to-word-wrap-text-in-jlabel 10 | */ 11 | public class MultilineLabel extends JTextArea { 12 | private void init() { 13 | setWrapStyleWord(true); 14 | setLineWrap(true); 15 | setEditable(false); 16 | setFocusable(false); 17 | setBackground(UIManager.getColor("Label.background")); 18 | setFont(UIManager.getFont("Label.font")); 19 | setBorder(UIManager.getBorder("Label.border")); 20 | setColumns(35); 21 | } 22 | MultilineLabel(String text) { 23 | super(text); 24 | init(); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /BappDescription.html: -------------------------------------------------------------------------------- 1 |

This is a Burp extension for signing AWS requests with SigV4. Signature Version 4 is a process to add authentication information to AWS HTTP requests. More information can be found here

2 | 3 |

SigV4 uses a timestamp to give signatures a limited lifetime. When using tools like Burp repeater, this plugin will automatically compute a new signature with the current timestamp. You can also repeat requests using different AWS credentials.

4 | 5 |

Features:

6 | 7 | 14 | 15 |

For more information check out the README

16 | -------------------------------------------------------------------------------- /src/main/java/burp/SigCredential.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.util.regex.Pattern; 4 | 5 | public abstract class SigCredential 6 | { 7 | protected static final Pattern accessKeyIdPattern = Pattern.compile("^[\\w]{16,128}$"); 8 | protected static final Pattern secretKeyPattern = Pattern.compile("^[a-zA-Z0-9/+]{40,128}$"); // base64 characters. not sure on length 9 | 10 | private String accessKeyId; 11 | private String secretKey; 12 | 13 | abstract boolean isTemporary(); 14 | 15 | abstract String getClassName(); 16 | 17 | public String getAccessKeyId() 18 | { 19 | return accessKeyId; 20 | } 21 | 22 | public String getSecretKey() 23 | { 24 | return secretKey; 25 | } 26 | 27 | protected void setAccessKeyId(final String accessKeyId) { 28 | if (accessKeyIdPattern.matcher(accessKeyId).matches()) 29 | this.accessKeyId = accessKeyId; 30 | else 31 | throw new IllegalArgumentException("Credential accessKeyId must match pattern "+accessKeyIdPattern.pattern()); 32 | } 33 | 34 | protected void setSecretKey(final String secretKey) { 35 | if (secretKeyPattern.matcher(secretKey).matches()) 36 | this.secretKey = secretKey; 37 | else 38 | throw new IllegalArgumentException("Credential secretKey must match pattern "+secretKeyPattern.pattern()); 39 | } 40 | 41 | public String getExportString() 42 | { 43 | String export = ""; 44 | export += String.format("aws_access_key_id = %s\n", getAccessKeyId()); 45 | export += String.format("aws_secret_access_key = %s\n", getSecretKey()); 46 | return export; 47 | } 48 | 49 | public String toString() 50 | { 51 | return String.format("accessKeyId = %s, secretKey = %s", this.accessKeyId, this.secretKey); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/burp/ConfigParser.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.HashMap; 7 | import java.util.Iterator; 8 | import java.util.Map; 9 | import java.util.regex.Matcher; 10 | import java.util.regex.Pattern; 11 | 12 | /* 13 | class for parsing aws cli config files. 14 | see https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html 15 | */ 16 | public class ConfigParser 17 | { 18 | private static final Pattern sectionPattern = Pattern.compile("^\\s*\\[\\s*([^]]{1,256}?)\\s*\\]\\s*$"); 19 | private static final Pattern valuePattern = Pattern.compile("^\\s*([^=]{1,256}?)\\s*=\\s*(.{1,760}?)\\s*$"); 20 | 21 | public static Map> parse(final Path path) 22 | { 23 | Map> config = new HashMap<>(); 24 | try { 25 | Map sectionMap = null; 26 | for (Iterator i = Files.lines(path).iterator(); i.hasNext();) { 27 | final String line = i.next(); 28 | Matcher sectionMatch = sectionPattern.matcher(line); 29 | if (sectionMatch.matches()) { 30 | final String sectionName = sectionMatch.group(1); 31 | sectionMap = new HashMap<>(); 32 | config.put(sectionName, sectionMap); 33 | } 34 | else if (sectionMap != null) { 35 | Matcher valueMatch = valuePattern.matcher(line); 36 | if (valueMatch.matches()) { 37 | final String key = valueMatch.group(1); 38 | final String value = valueMatch.group(2); 39 | sectionMap.put(key, value); 40 | } 41 | } 42 | } 43 | } catch (IOException ignore) { 44 | } 45 | return config; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/burp/SigCredentialSerializer.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.google.gson.*; 4 | 5 | import java.lang.reflect.Type; 6 | import java.util.Map; 7 | 8 | public class SigCredentialSerializer implements JsonSerializer, JsonDeserializer 9 | { 10 | public final static String CLASS_NAME = "className"; 11 | 12 | private static final Map handledClasses = Map.of( 13 | SigStaticCredential.class.getName(), SigStaticCredential.class, 14 | SigTemporaryCredential.class.getName(), SigTemporaryCredential.class); 15 | 16 | 17 | @Override 18 | public JsonElement serialize(SigCredential src, Type typeOfSrc, JsonSerializationContext context) 19 | { 20 | JsonObject obj = context.serialize(src).getAsJsonObject(); 21 | obj.addProperty(CLASS_NAME, src.getClassName()); 22 | return obj; 23 | } 24 | 25 | @Override 26 | public SigCredential deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException 27 | { 28 | final JsonObject obj = json.getAsJsonObject(); 29 | final String className = obj.get(CLASS_NAME).getAsString(); 30 | if (!handledClasses.containsKey(className)) { 31 | return null; 32 | } 33 | obj.remove(CLASS_NAME); // this is a meta property 34 | Class credentialClass; 35 | try { 36 | @SuppressWarnings("unchecked") 37 | Class tempCredentialClass = (Class) handledClasses.get(className); 38 | if (!SigCredential.class.isAssignableFrom(tempCredentialClass)) { 39 | throw new JsonParseException("Class does not implement SigCredential: "+className); 40 | } 41 | credentialClass = tempCredentialClass; 42 | } catch (ClassCastException exc) { 43 | throw new JsonParseException("Failed to handle class: "+className); 44 | } 45 | return context.deserialize(obj, credentialClass); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/burp/SigTemporaryCredential.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.time.Instant; 4 | 5 | /* 6 | This class represents temporary credentials that utilize a session token in addition to a secret key. 7 | */ 8 | public class SigTemporaryCredential extends SigCredential 9 | { 10 | private static final long CREDENTIAL_RENEWAL_AGE = 30; // seconds before expiration 11 | 12 | private long expireTimeEpochSeconds; 13 | private String sessionToken; 14 | 15 | private SigTemporaryCredential() {}; 16 | 17 | public static boolean shouldRenewCredential(final SigTemporaryCredential credential) { 18 | return ((credential == null) || (credential.secondsToExpire() < CREDENTIAL_RENEWAL_AGE)); 19 | } 20 | 21 | public SigTemporaryCredential(String accessKeyId, String secretKey, String sessionToken, long expireTimeEpochSeconds) 22 | { 23 | setAccessKeyId(accessKeyId); 24 | setSecretKey(secretKey); 25 | setSessionToken(sessionToken); 26 | this.expireTimeEpochSeconds = expireTimeEpochSeconds; 27 | } 28 | 29 | 30 | public String getSessionToken() 31 | { 32 | return sessionToken; 33 | } 34 | 35 | @Override 36 | public boolean isTemporary() 37 | { 38 | return true; 39 | } 40 | 41 | @Override 42 | public String getClassName() 43 | { 44 | return getClass().getName(); 45 | } 46 | 47 | protected void setSessionToken(final String sessionToken) { 48 | this.sessionToken = sessionToken; 49 | } 50 | 51 | // return the number of seconds until the temporary credentials expire 52 | public long secondsToExpire() 53 | { 54 | return expireTimeEpochSeconds - Instant.now().getEpochSecond(); 55 | } 56 | 57 | @Override 58 | public String getExportString() { 59 | String export = super.getExportString(); 60 | export += String.format("aws_session_token = %s\n", getSessionToken()); 61 | return export; 62 | } 63 | 64 | public String toString() 65 | { 66 | return String.format("accessKeyId = %s, secretKey = %s, sessionToken = %s", getAccessKeyId(), getSecretKey(), getSessionToken()); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/burp/SigCredentialProviderSerializer.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.google.gson.*; 4 | 5 | import java.lang.reflect.Type; 6 | import java.util.Map; 7 | 8 | public class SigCredentialProviderSerializer implements JsonSerializer, JsonDeserializer 9 | { 10 | public final static String CLASS_NAME = "className"; 11 | 12 | private static final Map handledClasses = Map.of( 13 | SigStaticCredentialProvider.class.getName(), SigStaticCredentialProvider.class, 14 | SigHttpCredentialProvider.class.getName(), SigHttpCredentialProvider.class, 15 | SigAssumeRoleCredentialProvider.class.getName(), SigAssumeRoleCredentialProvider.class, 16 | SigAwsProfileCredentialProvider.class.getName(), SigAwsProfileCredentialProvider.class); 17 | 18 | @Override 19 | public JsonElement serialize(SigCredentialProvider src, Type typeOfSrc, JsonSerializationContext context) 20 | { 21 | JsonObject obj = context.serialize(src).getAsJsonObject(); 22 | obj.addProperty(CLASS_NAME, src.getClassName()); 23 | return obj; 24 | } 25 | 26 | @Override 27 | public SigCredentialProvider deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException 28 | { 29 | final JsonObject obj = json.getAsJsonObject(); 30 | final String className = obj.get(CLASS_NAME).getAsString(); 31 | if (!handledClasses.containsKey(className)) { 32 | return null; 33 | } 34 | obj.remove(CLASS_NAME); // this is a meta property 35 | Class providerClass; 36 | try { 37 | @SuppressWarnings("unchecked") 38 | Class tempProviderClass = (Class) handledClasses.get(className); 39 | if (!SigCredentialProvider.class.isAssignableFrom(tempProviderClass)) { 40 | throw new JsonParseException("Class does not implement SigCredentialProvider: "+className); 41 | } 42 | providerClass = tempProviderClass; 43 | } catch (ClassCastException exc) { 44 | throw new JsonParseException("Failed to handle class: "+className); 45 | } 46 | return context.deserialize(obj, providerClass); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/burp/LogWriter.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.io.OutputStream; 4 | import java.io.PrintWriter; 5 | 6 | public class LogWriter 7 | { 8 | final public static int DEBUG_LEVEL = 0; 9 | final public static int INFO_LEVEL = 1; 10 | final public static int ERROR_LEVEL = 2; 11 | final public static int DEFAULT_LEVEL = ERROR_LEVEL; 12 | final public static int FATAL_LEVEL = 3; 13 | 14 | private PrintWriter out; 15 | private PrintWriter err; 16 | private int logLevel; 17 | 18 | private static LogWriter logWriter; 19 | 20 | private LogWriter() 21 | { 22 | this.logLevel = DEFAULT_LEVEL; 23 | } 24 | 25 | public static LogWriter getLogger() 26 | { 27 | if (logWriter == null) 28 | logWriter = new LogWriter(); 29 | return logWriter; 30 | } 31 | 32 | public static String levelNameFromInt(final int level) 33 | { 34 | switch (level) { 35 | case DEBUG_LEVEL: 36 | return "debug"; 37 | case INFO_LEVEL: 38 | return "info"; 39 | case ERROR_LEVEL: 40 | return "error"; 41 | case FATAL_LEVEL: 42 | return "fatal"; 43 | } 44 | return "*INVALID*"; 45 | } 46 | 47 | public void configure(OutputStream outStream, OutputStream errStream, int logLevel) 48 | { 49 | this.out = new PrintWriter(outStream, true); 50 | this.err = new PrintWriter(errStream, true); 51 | this.logLevel = logLevel; 52 | } 53 | 54 | public void setLevel(int level) 55 | { 56 | if (level >= DEBUG_LEVEL && level <= FATAL_LEVEL) 57 | this.logLevel = level; 58 | } 59 | 60 | public int getLevel() { return this.logLevel; } 61 | 62 | private void log(final String message, int level) 63 | { 64 | if (this.logLevel <= level) { 65 | if (level >= ERROR_LEVEL) { 66 | this.err.println(message); 67 | } 68 | else { 69 | this.out.println(message); 70 | } 71 | } 72 | } 73 | 74 | public void debug(final String message) 75 | { 76 | log("[DEBUG] " + message, DEBUG_LEVEL); 77 | } 78 | 79 | public void info(final String message) 80 | { 81 | log("[INFO] " + message, INFO_LEVEL); 82 | } 83 | 84 | public void error(final String message) 85 | { 86 | log("[ERROR] " + message, ERROR_LEVEL); 87 | } 88 | 89 | public void fatal(final String message) 90 | { 91 | log("[FATAL] " + message, FATAL_LEVEL); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/main/java/burp/SigProfileTestDialog.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import software.amazon.awssdk.services.sts.model.GetCallerIdentityResponse; 4 | 5 | import javax.swing.*; 6 | import java.awt.*; 7 | 8 | 9 | public class SigProfileTestDialog extends JDialog 10 | { 11 | private final static int NAME_COLUMN = 0; 12 | private final static int VALUE_COLUMN = 1; 13 | private final static int PROFILE_ROW = 0; 14 | private final static int ACCOUNT_ID_ROW = 1; 15 | private final static int ARN_ROW = 2; 16 | private final static int USER_ID_ROW = 3; 17 | private JTable resultTable; 18 | 19 | private void init(final SigProfile profile) { 20 | Object[][] data = { 21 | {"Profile", profile.getName()}, 22 | {"AccountId", "..."}, 23 | {"Arn", "..."}, 24 | {"UserId", "..."} 25 | }; 26 | resultTable = new JTable(data, new String[]{"key", "value"}) { 27 | @Override 28 | public boolean isCellEditable(int row, int column) { 29 | return false; 30 | } 31 | }; 32 | resultTable.getColumnModel().getColumn(NAME_COLUMN).setPreferredWidth(100); 33 | resultTable.getColumnModel().getColumn(VALUE_COLUMN).setPreferredWidth(450); 34 | JPanel contentPanel = new JPanel(new BorderLayout()); 35 | contentPanel.add(resultTable, BorderLayout.CENTER); 36 | 37 | JButton closeButton = new JButton("Close"); 38 | closeButton.addActionListener(actionEvent -> { 39 | setVisible(false); 40 | dispose(); 41 | }); 42 | contentPanel.add(closeButton, BorderLayout.PAGE_END); 43 | 44 | // not necessary but adds a nice border 45 | JScrollPane outerScrollPane = new JScrollPane(contentPanel); 46 | add(outerScrollPane); 47 | pack(); 48 | setLocationRelativeTo(SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent())); 49 | } 50 | 51 | public SigProfileTestDialog(Frame owner, final SigProfile profile, boolean modal) { 52 | super(owner, profile.getName(), modal); 53 | init(profile); 54 | } 55 | 56 | public SigProfileTestDialog(Frame owner, final SigProfile profile, boolean modal, final GetCallerIdentityResponse response) { 57 | super(owner, profile.getName(), modal); 58 | init(profile); 59 | updateWithResult(response); 60 | } 61 | 62 | public void updateWithResult(final GetCallerIdentityResponse response) { 63 | resultTable.setValueAt(response.account(), ACCOUNT_ID_ROW, VALUE_COLUMN); 64 | resultTable.setValueAt(response.arn(), ARN_ROW, VALUE_COLUMN); 65 | resultTable.setValueAt(response.userId(), USER_ID_ROW, VALUE_COLUMN); 66 | pack(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and WebStorm 2 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 3 | 4 | # User-specific stuff 5 | .idea/**/workspace.xml 6 | .idea/**/tasks.xml 7 | .idea/**/usage.statistics.xml 8 | .idea/**/dictionaries 9 | .idea/**/shelf 10 | 11 | # Generated files 12 | .idea/**/contentModel.xml 13 | 14 | # Sensitive or high-churn files 15 | .idea/**/dataSources/ 16 | .idea/**/dataSources.ids 17 | .idea/**/dataSources.local.xml 18 | .idea/**/sqlDataSources.xml 19 | .idea/**/dynamic.xml 20 | .idea/**/uiDesigner.xml 21 | .idea/**/dbnavigator.xml 22 | 23 | # Gradle 24 | .idea/**/gradle.xml 25 | .idea/**/libraries 26 | 27 | # Gradle and Maven with auto-import 28 | # When using Gradle or Maven with auto-import, you should exclude module files, 29 | # since they will be recreated, and may cause churn. Uncomment if using 30 | # auto-import. 31 | .idea/modules.xml 32 | .idea/*.iml 33 | .idea/modules 34 | .idea/artifacts 35 | .idea/compiler.xml 36 | .idea/jarRepositories.xml 37 | *.iml 38 | *.ipr 39 | 40 | # CMake 41 | cmake-build-*/ 42 | 43 | # Mongo Explorer plugin 44 | .idea/**/mongoSettings.xml 45 | 46 | # File-based project format 47 | *.iws 48 | 49 | # IntelliJ 50 | out/ 51 | 52 | # mpeltonen/sbt-idea plugin 53 | .idea_modules/ 54 | 55 | # JIRA plugin 56 | atlassian-ide-plugin.xml 57 | 58 | # Cursive Clojure plugin 59 | .idea/replstate.xml 60 | 61 | # Crashlytics plugin (for Android Studio and IntelliJ) 62 | com_crashlytics_export_strings.xml 63 | crashlytics.properties 64 | crashlytics-build.properties 65 | fabric.properties 66 | 67 | # Editor-based Rest Client 68 | .idea/httpRequests 69 | 70 | # Android studio 3.1+ serialized cache file 71 | .idea/caches/build_file_checksums.ser 72 | 73 | ######################## 74 | ## Java specific https://raw.githubusercontent.com/github/gitignore/master/Java.gitignore 75 | ######################## 76 | 77 | # Compiled class file 78 | *.class 79 | 80 | # Log file 81 | *.log 82 | 83 | # BlueJ files 84 | *.ctxt 85 | 86 | # Mobile Tools for Java (J2ME) 87 | .mtj.tmp/ 88 | 89 | # Package Files # 90 | *.jar 91 | *.war 92 | *.nar 93 | *.ear 94 | *.zip 95 | *.tar.gz 96 | *.rar 97 | 98 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 99 | hs_err_pid* 100 | 101 | ################# 102 | # maven 103 | ################# 104 | target/ 105 | pom.xml.tag 106 | pom.xml.releaseBackup 107 | pom.xml.versionsBackup 108 | pom.xml.next 109 | release.properties 110 | dependency-reduced-pom.xml 111 | buildNumber.properties 112 | .mvn/timing.properties 113 | .mvn/wrapper/maven-wrapper.jar 114 | 115 | # other 116 | .idea/codeStyles/ 117 | .idea/.name 118 | .idea/vcs.xml 119 | .idea/misc.xml 120 | stale_outputs_checked 121 | 122 | ################# 123 | # gradle 124 | ################# 125 | .gradle 126 | /build/ 127 | 128 | # Ignore Gradle GUI config 129 | gradle-app.setting 130 | 131 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) 132 | !gradle-wrapper.jar 133 | 134 | # Cache of project 135 | .gradletasknamecache 136 | -------------------------------------------------------------------------------- /src/main/java/burp/ExtensionSettings.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.google.gson.annotations.Since; 4 | import lombok.*; 5 | import lombok.experimental.Accessors; 6 | import lombok.experimental.NonFinal; 7 | 8 | import java.util.List; 9 | import java.util.Map; 10 | 11 | @Builder 12 | @Accessors(fluent=true) 13 | @Value 14 | @AllArgsConstructor 15 | @NoArgsConstructor 16 | public class ExtensionSettings { 17 | 18 | // use this field to track settings version. when adding a new setting, bump this value 19 | // and annotate the new setting with @Since(x.y). when the extension loads an 20 | // old settings file, it will just use the defaults for new settings. 21 | public static final double SETTINGS_VERSION = 0.1; 22 | 23 | // ref: https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html 24 | public static final long PRESIGNED_URL_LIFETIME_MIN_SECONDS = 1; 25 | public static final long PRESIGNED_URL_LIFETIME_MAX_SECONDS = 604800; // 7 days 26 | public static final long PRESIGNED_URL_LIFETIME_DEFAULT_SECONDS = 900; // 15 minutes 27 | 28 | public static final String CONTENT_MD5_UPDATE = "update"; // recompute a valid md5 29 | public static final String CONTENT_MD5_REMOVE = "remove"; // remove the header 30 | public static final String CONTENT_MD5_IGNORE = "ignore"; // do nothing 31 | public static final String CONTENT_MD5_DEFAULT = CONTENT_MD5_IGNORE; 32 | 33 | @Setter(AccessLevel.NONE) 34 | double settingsVersion = SETTINGS_VERSION; 35 | 36 | @Since(0) 37 | @Builder.Default 38 | int logLevel = LogWriter.DEFAULT_LEVEL; 39 | 40 | @Since(0) 41 | @Builder.Default 42 | String extensionVersion = "0.0.0"; 43 | 44 | @Since(0) 45 | @Builder.Default 46 | boolean persistProfiles = false; 47 | 48 | @Since(0) 49 | @Builder.Default 50 | boolean extensionEnabled = true; 51 | 52 | @Since(0) 53 | @Builder.Default 54 | String defaultProfileName = null; 55 | 56 | @Since(0) 57 | @Builder.Default 58 | List customSignedHeaders = List.of(); 59 | 60 | @Since(0) 61 | @Builder.Default 62 | boolean customSignedHeadersOverwrite = false; 63 | 64 | @Since(0) 65 | @Builder.Default 66 | List additionalSignedHeaderNames = List.of(); 67 | 68 | @Since(0) 69 | @Builder.Default 70 | boolean inScopeOnly = false; 71 | 72 | @Since(0) 73 | @Builder.Default 74 | boolean preserveHeaderOrder = true; 75 | 76 | @Since(0) 77 | @Builder.Default 78 | boolean updateContentSha256 = true; 79 | 80 | @Since(0) 81 | @Builder.Default 82 | @NonFinal 83 | @With 84 | long presignedUrlLifetimeInSeconds = PRESIGNED_URL_LIFETIME_DEFAULT_SECONDS; 85 | 86 | @Since(0) 87 | @Builder.Default 88 | @NonFinal 89 | @With 90 | String contentMD5HeaderBehavior = CONTENT_MD5_IGNORE; 91 | 92 | @Since(0) 93 | @Builder.Default 94 | Map profiles = Map.of(); 95 | 96 | @Since(0) 97 | @Builder.Default 98 | boolean signingEnabledForProxy = true; 99 | 100 | @Since(0) 101 | @Builder.Default 102 | boolean signingEnabledForSpider = true; 103 | 104 | @Since(0) 105 | @Builder.Default 106 | boolean signingEnabledForScanner = true; 107 | 108 | @Since(0) 109 | @Builder.Default 110 | boolean signingEnabledForIntruder = true; 111 | 112 | @Since(0) 113 | @Builder.Default 114 | boolean signingEnabledForRepeater = true; 115 | 116 | @Since(0) 117 | @Builder.Default 118 | boolean signingEnabledForSequencer = true; 119 | 120 | @Since(0) 121 | @Builder.Default 122 | boolean signingEnabledForExtender = true; 123 | 124 | @Since(0.1) 125 | @Builder.Default 126 | boolean addProfileComment = false; 127 | } 128 | -------------------------------------------------------------------------------- /src/main/java/burp/SigProfileEditorReadOnlyDialog.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | import java.awt.*; 5 | import java.awt.event.ActionEvent; 6 | import java.awt.event.ActionListener; 7 | 8 | /* 9 | This class provides a dialog for filling out missing signature fields when adding 10 | sigv4 to a request. Since region and service are optional profile parameters, this 11 | dialog can be used to prompt the user for them without modifying the profile. The 12 | reason service and region are optional is because an empty value means the region 13 | and service from the original request should be used. However, when adding a new 14 | signature, service and region are not available in the original request (located 15 | in the Authorization header). 16 | */ 17 | public class SigProfileEditorReadOnlyDialog extends SigProfileEditorDialog 18 | { 19 | private SigProfile editedProfile; 20 | 21 | public SigProfile getProfile() { return editedProfile; } 22 | 23 | public SigProfileEditorReadOnlyDialog(Frame owner, String title, boolean modal, SigProfile profile) 24 | { 25 | super(owner, title, modal, profile); 26 | if (profile == null) { 27 | throw new IllegalArgumentException("Profile editor dialog requires an existing profile to populate fields"); 28 | } 29 | this.regionTextField.setHintText("Required"); 30 | this.serviceTextField.setHintText("Required"); 31 | if (this.regionTextField.getText().isEmpty()) { 32 | // populate region from the environment (if defined) 33 | this.regionTextField.setText(SigProfile.getDefaultRegion()); 34 | } 35 | 36 | focusEmptyField(); 37 | 38 | this.okButton.removeActionListener(this.okButton.getActionListeners()[0]); 39 | this.okButton.addActionListener(new ActionListener() 40 | { 41 | @Override 42 | public void actionPerformed(ActionEvent actionEvent) 43 | { 44 | try { 45 | // don't allow empty region and service. a user can manually edit these in the message editor if they desire. 46 | final String region = (regionTextField.getText().length() > 0) ? regionTextField.getText() : profile.getRegion(); 47 | final String service = (serviceTextField.getText().length() > 0) ? serviceTextField.getText() : profile.getService(); 48 | if (region.equals("") || service.equals("")) { 49 | throw new IllegalArgumentException("region and service must not be blank"); 50 | } 51 | editedProfile = new SigProfile.Builder(profile) 52 | .withRegion(region) 53 | .withService(service) 54 | .build(); 55 | setVisible(false); 56 | dispose(); 57 | } catch (IllegalArgumentException exc) { 58 | setStatusLabel("Invalid settings: " + exc.getMessage()); 59 | } 60 | } 61 | }); 62 | } 63 | 64 | public void focusEmptyField() { 65 | this.serviceTextField.requestFocus(); 66 | if (this.regionTextField.getText().isEmpty()) { 67 | this.regionTextField.requestFocus(); 68 | } 69 | } 70 | 71 | private void disableField(JTextField textField) 72 | { 73 | textField.setEditable(false); 74 | textField.setForeground(disabledColor); 75 | textField.setFocusable(false); 76 | } 77 | 78 | public void disableForEdit() 79 | { 80 | disableField(this.nameTextField); 81 | disableField(this.profileKeyIdTextField); 82 | disableField(this.secretKeyTextField); 83 | disableField(this.sessionTokenTextField); 84 | 85 | providerPanel.setVisible(false); 86 | pack(); 87 | setLocationRelativeTo(SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent())); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/burp/SigMessageEditorTab.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import java.awt.*; 4 | 5 | /* 6 | this class provides a non-editable request tab for displaying the request after it has been signed. 7 | */ 8 | public class SigMessageEditorTab implements IMessageEditorTab 9 | { 10 | private IMessageEditorController controller; 11 | private final BurpExtender burp = BurpExtender.getBurp(); 12 | private ITextEditor messageTextEditor; 13 | private byte[] content; 14 | 15 | public SigMessageEditorTab(IMessageEditorController controller, boolean editable) 16 | { 17 | this.controller = controller; 18 | } 19 | 20 | @Override 21 | public String getTabCaption() { 22 | return BurpExtender.DISPLAY_NAME; 23 | } 24 | 25 | @Override 26 | public Component getUiComponent() { 27 | this.messageTextEditor = this.burp.callbacks.createTextEditor(); 28 | this.messageTextEditor.setEditable(false); // this is just a preview of the signed message 29 | return this.messageTextEditor.getComponent(); 30 | } 31 | 32 | @Override 33 | public boolean isEnabled(byte[] content, boolean isRequest) { 34 | // enable for requests only 35 | if (isRequest) { 36 | // we only check if its an aws request here, skipping checks for whether signing is enabled or if its in scope. 37 | // this is because isEnabled() is only called once, so toggling in-scope only or signing enabled will have no 38 | // effect on current editor tabs. 39 | IRequestInfo requestInfo = this.burp.helpers.analyzeRequest(content); 40 | return BurpExtender.isAws4Request(requestInfo); 41 | } 42 | return false; 43 | } 44 | 45 | @Override 46 | public void setMessage(byte[] content, boolean isRequest) { 47 | if (this.burp.isSigningEnabled()) { 48 | IRequestInfo requestInfo = this.burp.helpers.analyzeRequest(this.controller.getHttpService(), content); 49 | 50 | // if request is not in scope, display a warning instead 51 | if (this.burp.isInScopeOnlyEnabled()) { 52 | if (!this.burp.callbacks.isInScope(requestInfo.getUrl())) { 53 | this.messageTextEditor.setText(this.burp.helpers.stringToBytes("Request URL is not in scope: "+requestInfo.getUrl())); 54 | return; 55 | } 56 | } 57 | 58 | this.content = content; 59 | final SigProfile profile = this.burp.getSigningProfile(requestInfo.getHeaders()); 60 | this.messageTextEditor.setText(this.burp.helpers.stringToBytes("Signing request...")); 61 | 62 | // thread this to prevent blocking the UI thread. signRequest can trigger an http request if temp credentials are configured. 63 | (new Thread(() -> { 64 | try { 65 | final byte[] requestBytes = burp.signRequest(this.controller.getHttpService(), this.content, profile); 66 | if (requestBytes == null) { 67 | this.messageTextEditor.setText(this.burp.helpers.stringToBytes("Failed to sign request with profile: " + profile.getName())); 68 | return; 69 | } 70 | this.messageTextEditor.setText(requestBytes); 71 | return; 72 | } catch (Exception ignored) { 73 | } 74 | this.messageTextEditor.setText(this.burp.helpers.stringToBytes("Failed to sign message with SigV4")); 75 | })).start(); 76 | } 77 | else { 78 | this.messageTextEditor.setText(this.burp.helpers.stringToBytes("SigV4 signing is disabled")); 79 | } 80 | } 81 | 82 | @Override 83 | public byte[] getMessage() { 84 | return this.content; 85 | } 86 | 87 | @Override 88 | public boolean isModified() { 89 | return false; 90 | } 91 | 92 | @Override 93 | public byte[] getSelectedData() { 94 | return null; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /SETTINGS.md: -------------------------------------------------------------------------------- 1 | # Extension Settings 2 | 3 | This file describes the JSON settings for the extension. Most settings can be 4 | configured in the UI tab, but some more advanced settings are only available by 5 | first exporting the settings JSON, modifying the setting, then importing. 6 | 7 | ### AdditionalSignedHeaderNames 8 | 9 | **UI name: Signed Headers** 10 | 11 | By default, SigV4 will include headers such as "Host", "X-Amz-Date", and others. 12 | Any header names specified here will be added to the signature if they exist in the 13 | original request. Note that some headers cannot be included, such as User-Agent as 14 | the AWS SDK will ignore them. 15 | 16 | ### ContentMD5HeaderBehavior 17 | 18 | **UI name: Advanced -> ContentMD5 Header Behavior** 19 | 20 | Takes 3 possible values that determine handling of the Content-MD5 header: 21 | 22 | * `remove` Remove the Content-MD5 header. 23 | * `ignore` Leave the Content-MD5 header alone. This is the Default. 24 | * `update` Update the Content-MD5 header with a valid digest. 25 | 26 | S3 uploads may optionally use this header so it must either be updated or removed 27 | if the request body changes. Note that CustomSignedHeaders are added after the 28 | Content-MD5 header is processed and so are not affected. If Content-MD5 is present, 29 | S3 requires it to be included in the signature. 30 | 31 | ### CustomSignedHeaders 32 | 33 | **UI name: Custom Signed Headers** 34 | 35 | Specifies headers (name and value) to add to all signatures regardless of profile. 36 | 37 | ### CustomSignedHeadersOverwrite 38 | 39 | **UI name: Overwrite existing headers** 40 | 41 | If true, overwrite headers in the original request with the custom header of the 42 | same name. If false, custom headers are simply appended. 43 | 44 | ### DefaultProfileName 45 | 46 | **UI name: Default Profile** 47 | 48 | If specified, sign all requests with the named profile. By default, the AccessKeyId 49 | in the original request's Authorization header is matched with the unique KeyId 50 | (or AccessKeyId if blank) of a profile. Additionally, a profile can be specified in 51 | the header X-BurpSigV4-Profile which will take priority. 52 | 53 | ### ExtensionEnabled 54 | 55 | **UI name: Signing Enabled** 56 | 57 | If true, enable SigV4 signing. This is still subject to scope control settings, 58 | such as *InScopeOnly*. If false, do not sign any outgoing requests but context 59 | menu actions will remain. 60 | 61 | ### ExtensionVersion 62 | 63 | The version of the AWS SigV4 plugin that generated the settings file. 64 | 65 | ### InScopeOnly 66 | 67 | **UI name: In-scope Only** 68 | 69 | If true, only sign requests that are defined in the project scope. 70 | 71 | ### LogLevel 72 | 73 | **UI name: Log Level** 74 | 75 | Set verbosity of logging to Extender logs. Debug is the most verbose. 76 | 77 | ### PersistentProfiles 78 | 79 | **UI name: Persist Profiles** 80 | 81 | If true, save profiles to Burp settings store. This will include any configured 82 | credentials. Alternatively, there is some support for saving profiles by using 83 | the "Export" button. 84 | 85 | ### PreserveHeaderOrder 86 | 87 | **UI name: Advanced -> Preserve Header Order** 88 | 89 | If true, preserve the order of request headers after signing. This is simply for 90 | aesthetic reasons when displaying the signed request in the message editor tab. 91 | 92 | ### PresignedUrlLifetimeInSeconds 93 | 94 | **UI name: Advanced -> Presigned URL Lifetime Seconds** 95 | 96 | Sets the lifetime of a presigned URL created using the "Copy Signed URL" context 97 | menu item. See https://docs.aws.amazon.com/AmazonS3/latest/API/sigv4-query-string-auth.html. 98 | 99 | ### SerializedProfileList 100 | 101 | **UI name: AWS Credentials** 102 | 103 | If *PersistentProfiles* is true, this contains the GSON serialized profiles. 104 | 105 | ### SigningEnabledFor* 106 | 107 | **UI name: Advanced -> Tools Enabled for Signing** 108 | 109 | This setting exists for each Burp tool. If true, signing will be enabled for 110 | requests originating from that tool. Default is to sign requests for all tools. -------------------------------------------------------------------------------- /src/main/java/burp/SdkHttpClientForBurp.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import software.amazon.awssdk.http.*; 5 | 6 | import java.io.ByteArrayInputStream; 7 | import java.io.IOException; 8 | import java.net.URI; 9 | import java.util.*; 10 | 11 | /* 12 | An http client impl for aws sdk that uses Burp networking. This is used 13 | to make sure the aws sdk uses any configured upstream proxies. 14 | */ 15 | public class SdkHttpClientForBurp implements SdkHttpClient { 16 | private static final IBurpExtenderCallbacks callbacks = BurpExtender.getBurp().callbacks; 17 | private static final IExtensionHelpers helpers = BurpExtender.getBurp().helpers; 18 | 19 | @Override 20 | public ExecutableHttpRequest prepareRequest(HttpExecuteRequest request) { 21 | return new ExecutableHttpRequestForBurp(request); 22 | } 23 | 24 | @Override 25 | public void close() { 26 | // nothing to do 27 | } 28 | 29 | private static class ExecutableHttpRequestForBurp implements ExecutableHttpRequest { 30 | final private HttpExecuteRequest request; 31 | 32 | public ExecutableHttpRequestForBurp(HttpExecuteRequest request) { 33 | this.request = request; 34 | } 35 | 36 | @Override 37 | public HttpExecuteResponse call() throws IOException { 38 | // 39 | // Handle request 40 | // 41 | 42 | // get request body 43 | byte[] content = {}; 44 | if (request.contentStreamProvider().isPresent()) { 45 | content = request.contentStreamProvider().get().newStream().readAllBytes(); 46 | } 47 | 48 | final URI requestUri = request.httpRequest().getUri(); 49 | final String requestPathQuery = String.format("%s%s", 50 | StringUtils.isEmpty(requestUri.getRawPath()) ? "/" : requestUri.getRawPath(), 51 | StringUtils.isEmpty(requestUri.getRawQuery()) ? "" : "?" + requestUri.getRawQuery()); 52 | 53 | // get request headers. first header for Burp HTTP requests is the verb line 54 | List requestHeaders = new ArrayList<>(); 55 | requestHeaders.add(String.format("%s %s HTTP/1.1", 56 | request.httpRequest().method(), 57 | requestPathQuery)); 58 | 59 | // tell this extension to ignore this SigV4 request 60 | requestHeaders.add(BurpExtender.SKIP_SIGNING_HEADER); 61 | 62 | // flatten header map into a list 63 | Map> requestHeadersMap = request.httpRequest().headers(); 64 | requestHeadersMap.keySet().forEach(k -> { 65 | requestHeadersMap.get(k).forEach(v -> { 66 | requestHeaders.add(String.format("%s: %s", k, v)); 67 | }); 68 | }); 69 | 70 | // send request through Burp 71 | final byte[] responseBytes = callbacks.makeHttpRequest( 72 | request.httpRequest().host(), 73 | request.httpRequest().port(), 74 | request.httpRequest().protocol().equalsIgnoreCase("https"), 75 | helpers.buildHttpMessage(requestHeaders, content)); 76 | if (responseBytes == null || responseBytes.length == 0) 77 | throw new IOException("Failed to send Http request through Burp"); 78 | 79 | // 80 | // Handle response 81 | // 82 | final IResponseInfo responseInfo = helpers.analyzeResponse(responseBytes); 83 | final byte[] body = Arrays.copyOfRange(responseBytes, responseInfo.getBodyOffset(), responseBytes.length); 84 | SdkHttpResponse.Builder builder = SdkHttpFullResponse.builder() 85 | .statusCode(responseInfo.getStatusCode()); 86 | responseInfo.getHeaders().stream() 87 | .skip(1) // eg HTTP/1.1 200 OK 88 | .forEach(h -> { 89 | String[] header = BurpExtender.splitHeader(h); 90 | builder.appendHeader(header[0], header[1]); 91 | }); 92 | return HttpExecuteResponse.builder() 93 | .response(builder.build()) 94 | .responseBody(AbortableInputStream.create(new ByteArrayInputStream(body))) 95 | .build(); 96 | } 97 | 98 | @Override 99 | public void abort() { 100 | // nothing to do 101 | } 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/burp/SigAwsProfileCredentialProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.error.SigCredentialProviderException; 4 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 5 | import software.amazon.awssdk.auth.credentials.AwsCredentials; 6 | import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; 7 | import software.amazon.awssdk.auth.credentials.ProfileCredentialsProvider; 8 | import software.amazon.awssdk.core.exception.SdkClientException; 9 | import software.amazon.awssdk.profiles.ProfileFileSupplier; 10 | 11 | import java.time.Instant; 12 | import java.util.List; 13 | 14 | public class SigAwsProfileCredentialProvider implements SigCredentialProvider { 15 | 16 | public static final String PROVIDER_NAME = "AwsProfile"; 17 | protected static LogWriter logger = LogWriter.getLogger(); 18 | private transient long expirationInEpochSeconds = 0; 19 | private transient SigCredential latestCredential = null; 20 | private String profileName; 21 | 22 | private SigAwsProfileCredentialProvider() { }; 23 | 24 | public SigAwsProfileCredentialProvider(final String profileName) { 25 | if (profileName == null || profileName.equals("")) { 26 | throw new IllegalArgumentException("Profile name must not be empty"); 27 | } 28 | this.profileName = profileName; 29 | }; 30 | 31 | public String getProfileName() { 32 | return profileName; 33 | } 34 | 35 | private boolean isCredentialExpired() { 36 | // check if the creds will be expired in the next 5 seconds 37 | if (expirationInEpochSeconds < (Instant.now().getEpochSecond() + 5)) { 38 | return true; 39 | } 40 | if (latestCredential == null) { 41 | return true; 42 | } 43 | return false; 44 | } 45 | 46 | @Override 47 | synchronized public SigCredential getCredential() throws SigCredentialProviderException { 48 | if (!isCredentialExpired()) { 49 | return latestCredential; 50 | } 51 | logger.debug(String.format("Refreshing credentials: profile=%s, expired=%d, now=%d", profileName, expirationInEpochSeconds, Instant.now().getEpochSecond())); 52 | 53 | AwsCredentials credential; 54 | try (var profileCredentialsProvider = ProfileCredentialsProvider.builder().profileFile(ProfileFileSupplier.defaultSupplier()).profileName(profileName).build()) { 55 | credential = profileCredentialsProvider.resolveCredentials(); 56 | } catch (Exception exc) { 57 | var cause = exc.getCause(); 58 | String msg = exc.getMessage(); 59 | if (cause != null) { 60 | msg += ": "+cause.getMessage(); 61 | } 62 | throw new SigCredentialProviderException(msg); 63 | } 64 | try { 65 | if (credential instanceof AwsBasicCredentials) { 66 | // Check AWS credential file every 60 seconds. Even though they are static creds, there may be a process periodically updating 67 | // the credential file. 68 | expirationInEpochSeconds = Instant.now().getEpochSecond() + 60; 69 | latestCredential = new SigStaticCredential(credential.accessKeyId(), credential.secretAccessKey()); 70 | return latestCredential; 71 | } else if (credential instanceof AwsSessionCredentials session) { 72 | long expiration = Instant.now().getEpochSecond() + 60; 73 | if (session.expirationTime().isPresent()) { 74 | expiration = session.expirationTime().get().getEpochSecond(); 75 | } 76 | expirationInEpochSeconds = expiration; 77 | logger.debug("Refreshed credentials with expiry of "+expiration); 78 | latestCredential = new SigTemporaryCredential(session.accessKeyId(), session.secretAccessKey(), session.sessionToken(), expiration); 79 | return latestCredential; 80 | } 81 | } catch (IllegalArgumentException exc) { 82 | throw new SigCredentialProviderException(exc.getMessage()); 83 | } 84 | throw new SigCredentialProviderException("Encountered unknown credential type for profile: "+profileName); 85 | } 86 | 87 | @Override 88 | public String getName() { 89 | return PROVIDER_NAME; 90 | } 91 | 92 | @Override 93 | public String getClassName() { 94 | return getClass().getName(); 95 | } 96 | 97 | public static List getAvailableProfileNames() { 98 | return ProfileFileSupplier.defaultSupplier().get().profiles().keySet().stream().sorted().toList(); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AWS SigV4 2 | This is a Burp extension for signing AWS requests with SigV4. Signature Version 4 is a process to add 3 | authentication information to AWS HTTP requests. More information can be found here: 4 | https://docs.aws.amazon.com/general/latest/gr/signature-version-4.html 5 | 6 | SigV4 uses a timestamp to give signatures a limited lifetime. When using tools like Burp repeater, 7 | this plugin will automatically compute a new signature with the current timestamp. You can also 8 | repeat requests using different AWS credentials. 9 | 10 | ## Features 11 | - Credentials can be imported from a file or environment variables. 12 | - Automatically select a profile based on the key id in the request. 13 | - Resend requests with different credentials. 14 | - Context menu item for copying s3 presigned URLs. 15 | - Assume a role by providing a role ARN and optional external ID 16 | 17 | 18 | ## Build Instructions 19 | This assumes gradle is installed properly as well as a Java Development Kit. 20 | 21 | ``` 22 | $ ./gradlew bigJar 23 | > Task :compileJava 24 | 25 | BUILD SUCCESSFUL in 1s 26 | 2 actionable task: 2 executed 27 | $ 28 | ``` 29 | 30 | That will result in a newly created `build/libs` directory with a single JAR 31 | containing all the dependencies named `aws-sigv4--all.jar`. This JAR can be 32 | loaded into Burp using the Extender tab. 33 | 34 | Loading the project up in IntelliJ IDEA should also make it easy to build the 35 | source. 36 | 37 | 38 | ## Usage 39 | Hit the "Import" button to open the credential import dialog. From there, you can choose a 40 | file to import or select the "Auto" button to check default file locations and 41 | the environment for credentials. See https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where 42 | for expected file format. You can also manually add credentials by clicking "Add" in the main tab. 43 | In addition to the credentials file, the plugin will also check if the profile exists in the config 44 | file and it will pull in parameters from there. 45 | 46 | At a minimum, a profile should contain a name and at least 1 credential provider. Outgoing requests 47 | will be signed with the profile whose keyId matches the accessKeyId in the original request. If 48 | the accessKeyId is not recognized, the message will be sent unmodified. Alternatively, a 49 | "Default Profile" can be set which will be used to sign all outgoing requests regardless 50 | of the original accessKeyId. The plugin will also look for the "X-BurpSigV4-Profile" HTTP header 51 | for a profile name to use, with highest priority. 52 | 53 | Region and service should almost always be left blank. This will ensure the region and 54 | service in the original request are used which is desired in most cases. If your credential 55 | or config file contains a region for a named profile, that will be used. 56 | 57 | Profiles will be saved in the Burp settings store, including AWS keys, if "Persist Profiles" 58 | is checked. You can also "Export" credentials to a file for importing later or for use 59 | with the aws cli. 60 | 61 | ### Credentials 62 | 63 | Configure profiles to obtain credentials in any of the following ways. 64 | 65 | **Static Credentials** 66 | 67 | Permanent credentials issued by IAM or temporary credentials with a session token can be 68 | entered here. 69 | 70 | **AssumeRole** 71 | 72 | IAM roles can be assumed by entering a roleArn. Authorized credentials for calling sts:AssumeRole 73 | should be entered in the "Credentials" form for assuming the specified role. 74 | 75 | **HttpGet** 76 | 77 | If you are retrieving credentials in some other manner, you can serve them over HTTP and 78 | configure this form with the URL. An HTTP GET request will be issued to the URL and responses 79 | will be expected in 1 of 2 formats: 80 | 81 | ```json 82 | { 83 | "AccessKeyId": "", 84 | "SecretAccessKey": "" 85 | } 86 | ``` 87 | 88 | or 89 | 90 | ```json 91 | { 92 | "AccessKeyId": "", 93 | "SecretAccessKey": "", 94 | "SessionToken": "", 95 | "Expiration": "" 96 | } 97 | ``` 98 | 99 | Permanent credentials (no "SessionToken") will be fetched every time they are used. Temporary credentials 100 | will only be fetched when they are nearing expiration. Expiration should be specified in epoch seconds or 101 | as an ISO 8601 timestamp. 102 | 103 | **AWS Profile** 104 | 105 | Fetch credentials from the files used by the AWS CLI ([docs](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html)). 106 | 107 | ### Environment 108 | https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-envvars.html 109 | 110 | The following environment variables are recognized: 111 | - AWS_ACCESS_KEY_ID 112 | - AWS_SECRET_ACCESS_KEY 113 | - AWS_SESSION_TOKEN 114 | - AWS_DEFAULT_REGION 115 | - AWS_CONFIG_FILE 116 | - AWS_SHARED_CREDENTIALS_FILE 117 | 118 | If using the aws cli, set AWS_CA_BUNDLE to the path of your burp certificate (in PEM format). 119 | 120 | ## Screenshots 121 | 122 | UI tab 123 | 124 | ![UI](docs/screenshots/ui-example.png) 125 | 126 | Importing profiles 127 | 128 | ![Importing Profiles](docs/screenshots/import-profiles.png) 129 | 130 | Editing a profile 131 | 132 | ![Importing Profiles](docs/screenshots/profile-editor.png) 133 | 134 | ## Development 135 | 136 | Enable debug output for the aws sdk by adding the following property at the command line: 137 | 138 | ``` 139 | -Dorg.slf4j.simpleLogger.defaultLogLevel=trace 140 | ``` 141 | -------------------------------------------------------------------------------- /src/main/java/burp/SigHttpCredentialProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.error.SigCredentialProviderException; 4 | import lombok.Getter; 5 | import org.apache.commons.lang3.StringUtils; 6 | 7 | import java.net.MalformedURLException; 8 | import java.net.URI; 9 | import java.net.URISyntaxException; 10 | import java.net.URL; 11 | import java.util.Arrays; 12 | import java.util.List; 13 | import java.util.Optional; 14 | 15 | public class SigHttpCredentialProvider implements SigCredentialProvider 16 | { 17 | public static final String PROVIDER_NAME = "HttpGet"; 18 | private static final IBurpExtenderCallbacks callbacks = BurpExtender.getBurp().callbacks; 19 | private static final IExtensionHelpers helpers = BurpExtender.getBurp().helpers; 20 | 21 | @Getter 22 | private URI requestUri; 23 | private List customHeaders = List.of(); 24 | private transient SigCredential credential; 25 | 26 | public Optional getCustomHeader() { 27 | if (customHeaders.size() > 0) { 28 | return Optional.of(customHeaders.get(0)); 29 | } 30 | return Optional.empty(); 31 | } 32 | 33 | private SigHttpCredentialProvider() {}; 34 | 35 | public SigHttpCredentialProvider(String url, String header) { 36 | init(url, header); 37 | } 38 | 39 | 40 | private void init(String url, String header) { 41 | try { 42 | requestUri = new URL(url).toURI(); 43 | } catch (MalformedURLException | URISyntaxException exc) { 44 | throw new IllegalArgumentException("Invalid URL provided to HttpProvider: " + url); 45 | } 46 | 47 | if (!Arrays.asList("http", "https").contains(requestUri.getScheme())) { 48 | throw new IllegalArgumentException("Invalid protocol. Must be http(s)"); 49 | } 50 | 51 | if (StringUtils.isNotEmpty(header)) { 52 | String[] nameAndValue = BurpExtender.splitHeader(header); 53 | if (nameAndValue[0].length() == 0) { 54 | throw new IllegalArgumentException("Empty header"); 55 | } 56 | customHeaders = List.of(nameAndValue[0] + ": " + nameAndValue[1]); 57 | } 58 | } 59 | 60 | /* 61 | NOTE: Synchronization is intentionally omitted here for performance reasons. It is possible that 2 or more 62 | threads could refresh the credentials at the same time which is fine since a copy of valid credentials 63 | is always returned. For static credentials, synchronization is not desired at all. The http server is free to 64 | switch between static and temporary credentials for successive calls. 65 | */ 66 | private SigCredential renewCredential() throws SigCredentialProviderException { 67 | byte[] response; 68 | try { 69 | IRequestInfo request = helpers.analyzeRequest(helpers.buildHttpRequest(requestUri.toURL())); 70 | List headers = request.getHeaders(); 71 | headers.addAll(customHeaders); 72 | response = callbacks.makeHttpRequest(requestUri.getHost(), 73 | requestUri.getPort(), 74 | requestUri.getScheme().equalsIgnoreCase("https"), 75 | helpers.buildHttpMessage(headers, null)); 76 | } catch (MalformedURLException | IllegalArgumentException e) { 77 | throw new IllegalArgumentException("Invalid URL for HttpGet: " + requestUri); 78 | } 79 | 80 | if (response == null) { 81 | throw new SigCredentialProviderException("Failed to get response from " + requestUri); 82 | } 83 | 84 | IResponseInfo responseInfo = helpers.analyzeResponse(response); 85 | if (responseInfo.getStatusCode() != 200) 86 | throw new SigCredentialProviderException(String.format("GET request returned error: %d %s", responseInfo.getStatusCode(), requestUri)); 87 | final String responseBody = helpers.bytesToString(Arrays.copyOfRange(response, responseInfo.getBodyOffset(), response.length)); 88 | 89 | // expect similar object to sts:AssumeRole 90 | Optional profile = JSONCredentialParser.profileFromAssumeRoleJSON(responseBody); 91 | if (profile.isEmpty()) { 92 | throw new SigCredentialProviderException("Failed to parse HttpProvider response"); 93 | } 94 | return profile.get().getCredential(); 95 | } 96 | 97 | @Override 98 | public SigCredential getCredential() throws SigCredentialProviderException 99 | { 100 | SigCredential credentialCopy = credential; 101 | if (credentialCopy == null) { 102 | credentialCopy = renewCredential(); 103 | } 104 | else { 105 | if (credentialCopy.isTemporary()) { 106 | if (SigTemporaryCredential.shouldRenewCredential(((SigTemporaryCredential)credentialCopy))) { 107 | // fewer than 30 seconds until expiration, refresh 108 | credentialCopy = renewCredential(); 109 | } 110 | } 111 | else { 112 | // always refresh permanent credentials. seems counter-intuitive but if the user 113 | // isn't just using a static provider there must be a reason. 114 | credentialCopy = renewCredential(); 115 | } 116 | } 117 | if (credentialCopy == null) { 118 | throw new SigCredentialProviderException("Failed to get credential from "+ requestUri); 119 | } 120 | return credentialCopy; 121 | } 122 | 123 | @Override 124 | public String getName() { 125 | return PROVIDER_NAME; 126 | } 127 | 128 | @Override 129 | public String getClassName() { return getClass().getName();} 130 | 131 | } 132 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn () { 37 | echo "$*" 38 | } 39 | 40 | die () { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save () { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /src/main/java/burp/JSONCredentialParser.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import com.google.gson.*; 4 | 5 | import java.time.DateTimeException; 6 | import java.time.Instant; 7 | import java.time.format.DateTimeFormatter; 8 | import java.util.Optional; 9 | 10 | public class JSONCredentialParser { 11 | protected static LogWriter logger = LogWriter.getLogger(); 12 | 13 | public static Optional profileFromJSON(final String jsonText) { 14 | Optional profile = profileFromCognitoJSON(jsonText); 15 | if (profile.isPresent()) { 16 | return profile; 17 | } 18 | return profileFromAssumeRoleJSON(jsonText); 19 | } 20 | 21 | private static long expirationTimeToEpochSeconds(final String expiry) { 22 | try { 23 | return Long.parseLong(expiry); 24 | } catch (NumberFormatException ignored) { 25 | } 26 | try { 27 | return Instant.from(DateTimeFormatter.ISO_INSTANT.parse(expiry)).getEpochSecond(); 28 | } catch (DateTimeException ignored) { 29 | } 30 | throw new IllegalArgumentException("Failed to parse expiration timestamp"); 31 | } 32 | 33 | // Convert a timestamp provided as a 1) long, 2) long as a string, or 3) ISO 8601 timestamp 34 | private static long jsonExpirationTimeToEpochSeconds(final JsonElement jsonExpiration) { 35 | try { 36 | return jsonExpiration.getAsLong(); 37 | } catch (NumberFormatException ignore) { 38 | } 39 | return expirationTimeToEpochSeconds(jsonExpiration.getAsString()); 40 | } 41 | 42 | public static Optional profileFromAssumeRoleJSON(final String jsonText) { 43 | try { 44 | JsonObject jsonObject = new Gson().fromJson(jsonText, JsonObject.class); 45 | SigCredential staticCredential; 46 | if (jsonObject.has("SessionToken")) { 47 | staticCredential = new SigTemporaryCredential( 48 | jsonObject.get("AccessKeyId").getAsString(), 49 | jsonObject.get("SecretAccessKey").getAsString(), 50 | jsonObject.get("SessionToken").getAsString(), 51 | jsonExpirationTimeToEpochSeconds(jsonObject.get("Expiration"))); 52 | } else { 53 | staticCredential = new SigStaticCredential( 54 | jsonObject.get("AccessKeyId").getAsString(), 55 | jsonObject.get("SecretAccessKey").getAsString()); 56 | } 57 | SigProfile profile = new SigProfile.Builder(jsonObject.get("AccessKeyId").getAsString()). 58 | withCredentialProvider( 59 | new SigStaticCredentialProvider(staticCredential), 60 | SigProfile.DEFAULT_STATIC_PRIORITY). 61 | build(); 62 | return Optional.of(profile); 63 | } catch (JsonParseException | NullPointerException | IllegalArgumentException exc) { 64 | logger.error("Not a valid STS JSON credentials object: " + exc.getMessage()); 65 | } 66 | return Optional.empty(); 67 | } 68 | 69 | public static Optional profileFromCognitoJSON(final String jsonText) { 70 | // Try to parse as Cognito. 71 | // See https://docs.aws.amazon.com/cognitoidentity/latest/APIReference/API_GetCredentialsForIdentity.html#API_GetCredentialsForIdentity_ResponseSyntax 72 | try { 73 | JsonObject jsonObject = new Gson().fromJson(jsonText, JsonObject.class); 74 | // If this is from Cognito, we may have an IdentityId to use as the profile name. 75 | String profileName = null; 76 | if (jsonObject.get("IdentityId") != null) { 77 | profileName = jsonObject.get("IdentityId").getAsString(); 78 | } 79 | if (jsonObject.get("Credentials") != null) { 80 | jsonObject = jsonObject.get("Credentials").getAsJsonObject(); 81 | } 82 | if (jsonObject.get("AccessKeyId") == null || jsonObject.get("SecretKey") == null) { 83 | logger.error("Invalid JSON credentials object. AccessKeyId and SecretKey are required."); 84 | return Optional.empty(); 85 | } 86 | if (profileName == null) { 87 | profileName = jsonObject.get("AccessKeyId").getAsString(); 88 | } 89 | SigCredential staticCredential; 90 | if (jsonObject.get("SessionToken") != null) { 91 | long expiration = (System.currentTimeMillis() / 1000) + 43200; 92 | if (jsonObject.get("Expiration") != null) { 93 | try { 94 | expiration = jsonObject.get("Expiration").getAsLong(); 95 | } catch (ClassCastException | NumberFormatException e) { 96 | logger.error("Invalid Expiration. Expected an integer."); 97 | } 98 | } 99 | staticCredential = new SigTemporaryCredential( 100 | jsonObject.get("AccessKeyId").getAsString(), 101 | jsonObject.get("SecretKey").getAsString(), 102 | jsonObject.get("SessionToken").getAsString(), 103 | expiration); 104 | } else { 105 | staticCredential = new SigStaticCredential( 106 | jsonObject.get("AccessKeyId").getAsString(), 107 | jsonObject.get("SecretKey").getAsString()); 108 | } 109 | SigProfile profile = new SigProfile.Builder(profileName). 110 | withCredentialProvider( 111 | new SigStaticCredentialProvider(staticCredential), 112 | SigProfile.DEFAULT_STATIC_PRIORITY). 113 | build(); 114 | return Optional.of(profile); 115 | } catch (JsonSyntaxException e) { 116 | logger.error("Not a valid Cognito JSON credentials object"); 117 | } 118 | return Optional.empty(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/main/java/burp/SigAssumeRoleCredentialProvider.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import burp.error.SigCredentialProviderException; 4 | import org.apache.commons.lang3.StringUtils; 5 | import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; 6 | import software.amazon.awssdk.auth.credentials.AwsCredentials; 7 | import software.amazon.awssdk.auth.credentials.AwsSessionCredentials; 8 | import software.amazon.awssdk.regions.Region; 9 | import software.amazon.awssdk.services.sts.StsClient; 10 | import software.amazon.awssdk.services.sts.model.AssumeRoleRequest; 11 | import software.amazon.awssdk.services.sts.model.AssumeRoleResponse; 12 | import software.amazon.awssdk.services.sts.model.Credentials; 13 | import software.amazon.awssdk.services.sts.model.StsException; 14 | 15 | import java.util.regex.Pattern; 16 | 17 | public class SigAssumeRoleCredentialProvider implements SigCredentialProvider, Cloneable 18 | { 19 | public static final Pattern externalIdPattern = Pattern.compile("^[a-zA-Z0-9=@:/,._-]{2,1024}$"); 20 | public static final Pattern roleArnPattern = Pattern.compile("^arn:aws:iam::[0-9]{12}:role(?:/|/[\u0021-\u007E]{1,510}/)[0-9a-zA-Z+=,.@_-]{1,64}$"); // regionless 21 | public static final Pattern roleSessionNamePattern = Pattern.compile("^[a-zA-Z0-9+=@,._-]{2,64}$"); 22 | public static final String PROVIDER_NAME = "STSAssumeRole"; 23 | 24 | private String roleArn; 25 | private String sessionName; 26 | private int durationSeconds; 27 | private String externalId; 28 | 29 | private transient SigTemporaryCredential temporaryCredential; 30 | private SigCredential staticCredential; 31 | private final transient BurpExtender burp = BurpExtender.getBurp(); 32 | 33 | public static final int CREDENTIAL_LIFETIME_MIN = 900; 34 | public static final int CREDENTIAL_LIFETIME_MAX = 43200; 35 | public static final String ROLE_SESSION_NAME_DEFAULT_PREFIX = "BurpSigV4"; 36 | 37 | public String getRoleArn() 38 | { 39 | return this.roleArn; 40 | } 41 | public String getExternalId() { return this.externalId; } 42 | public String getSessionName() 43 | { 44 | return this.sessionName; 45 | } 46 | public int getDurationSeconds() 47 | { 48 | return this.durationSeconds; 49 | } 50 | 51 | public SigCredential getStaticCredential() 52 | { 53 | return this.staticCredential; 54 | } 55 | 56 | private SigAssumeRoleCredentialProvider() {}; 57 | 58 | private SigAssumeRoleCredentialProvider(final String roleArn, final SigCredential credential) 59 | { 60 | setRoleArn(roleArn); 61 | this.staticCredential = credential; 62 | this.sessionName = createDefaultRoleSessionName(); 63 | this.durationSeconds = CREDENTIAL_LIFETIME_MIN; 64 | this.externalId = ""; 65 | } 66 | 67 | private void setExternalId(final String externalId) { 68 | if (externalIdPattern.matcher(externalId).matches()) 69 | this.externalId = externalId; 70 | else 71 | throw new IllegalArgumentException("AssumeRole externalId must match pattern "+externalIdPattern.pattern()); 72 | } 73 | 74 | private void setDurationSeconds(int durationSeconds) 75 | { 76 | // duration must be in range [900, 43200] 77 | if (durationSeconds < CREDENTIAL_LIFETIME_MIN) { 78 | durationSeconds = CREDENTIAL_LIFETIME_MIN; 79 | } 80 | else if (durationSeconds > CREDENTIAL_LIFETIME_MAX) { 81 | durationSeconds = CREDENTIAL_LIFETIME_MAX; 82 | } 83 | this.durationSeconds = durationSeconds; 84 | } 85 | 86 | private void setRoleArn(final String roleArn) 87 | { 88 | if (roleArnPattern.matcher(roleArn).matches()) 89 | this.roleArn = roleArn; 90 | else 91 | throw new IllegalArgumentException("AssumeRole roleArn must match pattern "+roleArnPattern.pattern()); 92 | } 93 | 94 | private void setRoleSessionName(final String roleSessionName) 95 | { 96 | if (roleSessionNamePattern.matcher(roleSessionName).matches()) 97 | this.sessionName = roleSessionName; 98 | else 99 | throw new IllegalArgumentException("AssumeRole roleSessionName must match pattern "+roleSessionNamePattern.pattern()); 100 | } 101 | 102 | protected SigAssumeRoleCredentialProvider clone() 103 | { 104 | return new SigAssumeRoleCredentialProvider.Builder(this.roleArn, this.staticCredential) 105 | .withDurationSeconds(this.durationSeconds) 106 | .withRoleSessionName(this.sessionName) 107 | .tryExternalId(this.externalId) 108 | .build(); 109 | } 110 | 111 | public static class Builder { 112 | private SigAssumeRoleCredentialProvider assumeRole; 113 | public Builder(final String roleArn, final SigCredential credential) { 114 | this.assumeRole = new SigAssumeRoleCredentialProvider(roleArn, credential); 115 | } 116 | public Builder(final SigAssumeRoleCredentialProvider assumeRole) { 117 | this.assumeRole = assumeRole.clone(); 118 | } 119 | // with -> strict, try -> lax 120 | public Builder withRoleArn(final String roleArn) { 121 | this.assumeRole.setRoleArn(roleArn); 122 | return this; 123 | } 124 | public Builder withRoleSessionName(final String sessionName) { 125 | this.assumeRole.setRoleSessionName(sessionName); 126 | return this; 127 | } 128 | public Builder tryRoleSessionName(final String sessionName) { 129 | if (StringUtils.isNotEmpty(sessionName)) 130 | withRoleSessionName(sessionName); 131 | else 132 | this.assumeRole.sessionName = createDefaultRoleSessionName(); 133 | return this; 134 | } 135 | public Builder withDurationSeconds(final int durationSeconds) { 136 | this.assumeRole.setDurationSeconds(durationSeconds); 137 | return this; 138 | } 139 | public Builder withCredential(SigCredential credential) { 140 | if (credential == null) { 141 | throw new IllegalArgumentException("AssumeRole permanent credential cannot be null"); 142 | } 143 | this.assumeRole.staticCredential = credential; 144 | return this; 145 | } 146 | public Builder withExternalId(final String externalId) { 147 | this.assumeRole.setExternalId(externalId); 148 | return this; 149 | } 150 | public Builder tryExternalId(final String externalId) { 151 | if (StringUtils.isNotEmpty(externalId)) 152 | withExternalId(externalId); 153 | else 154 | this.assumeRole.externalId = ""; 155 | return this; 156 | } 157 | public SigAssumeRoleCredentialProvider build() { 158 | return this.assumeRole; 159 | } 160 | } 161 | 162 | private static String createDefaultRoleSessionName() 163 | { 164 | return String.format("%s_%d", ROLE_SESSION_NAME_DEFAULT_PREFIX, System.currentTimeMillis()); 165 | } 166 | 167 | @Override 168 | public String getName() { 169 | return PROVIDER_NAME; 170 | } 171 | 172 | @Override 173 | public String getClassName() { return getClass().getName(); } 174 | 175 | @Override 176 | public SigCredential getCredential() throws SigCredentialProviderException 177 | { 178 | SigTemporaryCredential credentialCopy = this.temporaryCredential; 179 | if (SigTemporaryCredential.shouldRenewCredential(credentialCopy)) { 180 | // signature is expired or about to expire. get new credentials 181 | credentialCopy = renewCredential(); 182 | } 183 | if (credentialCopy == null) { 184 | throw new SigCredentialProviderException("Failed to retrieve temp credentials for: "+this.roleArn); 185 | } 186 | return credentialCopy; 187 | } 188 | 189 | /* 190 | Fetch new temporary credentials. This is synchronized so multiple threads don't try to refresh creds 191 | at the same time. The result would be additional, unnecessary calls to STS but is otherwise harmless. 192 | */ 193 | private synchronized SigTemporaryCredential renewCredential() throws SigCredentialProviderException 194 | { 195 | // ensure creds weren't just renewed by another thread 196 | SigTemporaryCredential credentialCopy = this.temporaryCredential; 197 | if (!SigTemporaryCredential.shouldRenewCredential(credentialCopy)) { 198 | return credentialCopy; 199 | } 200 | 201 | burp.logger.info("Fetching temporary credentials for role "+this.roleArn); 202 | this.temporaryCredential = null; 203 | AwsCredentials credentials; 204 | if (staticCredential.isTemporary()) { 205 | credentials = AwsSessionCredentials.create(staticCredential.getAccessKeyId(), staticCredential.getSecretKey(), ((SigTemporaryCredential) staticCredential).getSessionToken()); 206 | } else { 207 | credentials = AwsBasicCredentials.create(staticCredential.getAccessKeyId(), staticCredential.getSecretKey()); 208 | } 209 | StsClient stsClient = StsClient.builder() 210 | .httpClient(new SdkHttpClientForBurp()) 211 | .region(Region.US_EAST_1) 212 | .credentialsProvider(() -> credentials) 213 | .build(); 214 | 215 | AssumeRoleRequest.Builder requestBuilder = AssumeRoleRequest.builder() 216 | .roleArn(this.roleArn) 217 | .roleSessionName(this.sessionName) 218 | .durationSeconds(this.durationSeconds); 219 | if (StringUtils.isNotEmpty(this.externalId)) { 220 | requestBuilder.externalId(this.externalId); 221 | } 222 | 223 | try { 224 | AssumeRoleResponse roleResponse = stsClient.assumeRole(requestBuilder.build()); 225 | Credentials creds = roleResponse.credentials(); 226 | credentialCopy = new SigTemporaryCredential( 227 | creds.accessKeyId(), 228 | creds.secretAccessKey(), 229 | creds.sessionToken(), 230 | creds.expiration().getEpochSecond()); 231 | } catch (StsException exc) { 232 | throw new SigCredentialProviderException("Failed to get role credentials: "+exc.getMessage()); 233 | } 234 | this.temporaryCredential = credentialCopy; 235 | return credentialCopy; 236 | } 237 | 238 | } 239 | -------------------------------------------------------------------------------- /src/main/java/burp/SigProfileImportDialog.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | import javax.swing.border.TitledBorder; 5 | import javax.swing.table.DefaultTableModel; 6 | import java.awt.*; 7 | import java.awt.datatransfer.Clipboard; 8 | import java.awt.datatransfer.DataFlavor; 9 | import java.awt.datatransfer.UnsupportedFlavorException; 10 | import java.awt.event.ActionEvent; 11 | import java.awt.event.ActionListener; 12 | import java.io.IOException; 13 | import java.nio.file.Files; 14 | import java.nio.file.Path; 15 | import java.nio.file.Paths; 16 | import java.util.ArrayList; 17 | import java.util.Collections; 18 | import java.util.HashMap; 19 | import java.util.List; 20 | 21 | class NewSigProfile 22 | { 23 | public String source; 24 | public SigProfile sigProfile; 25 | 26 | public NewSigProfile(SigProfile sigProfile, String source) 27 | { 28 | this.sigProfile = sigProfile; 29 | this.source = source; 30 | } 31 | } 32 | 33 | public class SigProfileImportDialog extends JDialog 34 | { 35 | private static final int SELECT_COLUMN_INDEX = 0; 36 | private static final int NAME_COLUMN_INDEX = 1; 37 | private static final int KEYID_COLUMN_INDEX = 2; 38 | 39 | private BurpExtender burp = BurpExtender.getBurp(); 40 | private JTable profileTable; 41 | private JLabel hintLabel; 42 | private HashMap profileNameMap; 43 | 44 | public SigProfileImportDialog(Frame owner, String title, boolean modal) 45 | { 46 | super(owner, title, modal); 47 | this.profileNameMap = new HashMap<>(); 48 | setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 49 | 50 | JPanel outerPanel = new JPanel(new GridBagLayout()); 51 | outerPanel.setBorder(new TitledBorder("")); 52 | 53 | // import from file buttons 54 | JPanel importButtonPanel = new JPanel(); 55 | TitledBorder importBorder= new TitledBorder("Source"); 56 | importBorder.setTitleColor(BurpExtender.textOrange); 57 | importButtonPanel.setBorder(importBorder); 58 | JButton autoImportButton = new JButton("Auto"); 59 | JButton chooseImportButton = new JButton("File"); 60 | JButton envImportButton = new JButton("Env"); 61 | JButton shellImportButton = new JButton("Clipboard"); 62 | importButtonPanel.add(autoImportButton); 63 | importButtonPanel.add(chooseImportButton); 64 | importButtonPanel.add(envImportButton); 65 | importButtonPanel.add(shellImportButton); 66 | 67 | autoImportButton.addActionListener(new ActionListener() { 68 | @Override 69 | public void actionPerformed(ActionEvent actionEvent) { 70 | importProfilesFromEnvironment(); 71 | importProfilesFromCLI(); 72 | } 73 | }); 74 | 75 | chooseImportButton.addActionListener(new ActionListener() { 76 | @Override 77 | public void actionPerformed(ActionEvent actionEvent) { 78 | final Path path = getChosenImportPath(); 79 | if (path != null) { 80 | importProfilesFromFile(path); 81 | } 82 | } 83 | }); 84 | 85 | envImportButton.addActionListener(new ActionListener() { 86 | @Override 87 | public void actionPerformed(ActionEvent actionEvent) { 88 | importProfilesFromEnvironment(); 89 | } 90 | }); 91 | 92 | shellImportButton.addActionListener(new ActionListener() { 93 | @Override 94 | public void actionPerformed(ActionEvent actionEvent) { 95 | importProfilesFromClipboard(); 96 | } 97 | }); 98 | 99 | // select all/none buttons 100 | JPanel selectButtonPanel = new JPanel(); 101 | TitledBorder selectBorder = new TitledBorder("Select"); 102 | selectBorder.setTitleColor(BurpExtender.textOrange); 103 | selectButtonPanel.setBorder(selectBorder); 104 | JButton selectAllButton = new JButton("All"); 105 | JButton selectNoneButton = new JButton("None"); 106 | selectButtonPanel.add(selectAllButton); 107 | selectButtonPanel.add(selectNoneButton); 108 | 109 | JPanel topButtonPanel = new JPanel(); 110 | topButtonPanel.add(importButtonPanel); 111 | topButtonPanel.add(selectButtonPanel); 112 | 113 | selectAllButton.addActionListener(new ActionListener() { 114 | @Override 115 | public void actionPerformed(ActionEvent actionEvent) { 116 | DefaultTableModel model = (DefaultTableModel) profileTable.getModel(); 117 | for (int i = 0; i < model.getRowCount(); i++) { 118 | model.setValueAt(true, i, SELECT_COLUMN_INDEX); 119 | } 120 | } 121 | }); 122 | 123 | selectNoneButton.addActionListener(new ActionListener() { 124 | @Override 125 | public void actionPerformed(ActionEvent actionEvent) { 126 | DefaultTableModel model = (DefaultTableModel) profileTable.getModel(); 127 | for (int i = 0; i < model.getRowCount(); i++) { 128 | model.setValueAt(false, i, SELECT_COLUMN_INDEX); 129 | } 130 | } 131 | }); 132 | 133 | // import table 134 | profileTable = new JTable(new DefaultTableModel(new Object[]{"Import", "Name", "KeyId", "Source"}, 0) { 135 | @Override 136 | public boolean isCellEditable(int row, int column) { 137 | // prevent table cells from being edited. must use dialog to edit. 138 | return column == SELECT_COLUMN_INDEX; 139 | } 140 | 141 | @Override 142 | public Class getColumnClass(int columnIndex) { 143 | if (columnIndex == SELECT_COLUMN_INDEX) { 144 | return Boolean.class; 145 | } 146 | return super.getColumnClass(columnIndex); 147 | } 148 | }); 149 | 150 | profileTable.setAutoResizeMode(JTable.AUTO_RESIZE_LAST_COLUMN); 151 | profileTable.getColumnModel().getColumn(SELECT_COLUMN_INDEX).setMinWidth(60); 152 | profileTable.getColumnModel().getColumn(SELECT_COLUMN_INDEX).setMaxWidth(60); 153 | profileTable.getColumnModel().getColumn(NAME_COLUMN_INDEX).setMinWidth(150); 154 | profileTable.getColumnModel().getColumn(NAME_COLUMN_INDEX).setMaxWidth(300); 155 | profileTable.getColumnModel().getColumn(KEYID_COLUMN_INDEX).setMinWidth(220); 156 | profileTable.getColumnModel().getColumn(KEYID_COLUMN_INDEX).setMaxWidth(300); 157 | JScrollPane profileScrollPane = new JScrollPane(profileTable); 158 | profileScrollPane.setPreferredSize(new Dimension(900, 200)); 159 | 160 | JPanel lowerButtonPanel = new JPanel(); 161 | JButton okButton = new JButton("Ok"); 162 | JButton cancelButton = new JButton("Cancel"); 163 | lowerButtonPanel.add(okButton); 164 | lowerButtonPanel.add(cancelButton); 165 | 166 | okButton.addActionListener(new ActionListener() { 167 | @Override 168 | public void actionPerformed(ActionEvent actionEvent) { 169 | try { 170 | addSelectedProfiles(); 171 | } catch (IllegalArgumentException exc) { 172 | hintLabel.setText(exc.getMessage()); 173 | return; 174 | } 175 | setVisible(false); 176 | dispose(); 177 | } 178 | }); 179 | 180 | cancelButton.addActionListener(new ActionListener() { 181 | @Override 182 | public void actionPerformed(ActionEvent actionEvent) { 183 | setVisible(false); 184 | dispose(); 185 | } 186 | }); 187 | 188 | GridBagConstraints c00 = new GridBagConstraints(); 189 | c00.anchor = GridBagConstraints.FIRST_LINE_START; 190 | c00.gridy = 0; 191 | GridBagConstraints c01 = new GridBagConstraints(); 192 | c01.gridy = 1; 193 | GridBagConstraints c02 = new GridBagConstraints(); 194 | c02.gridy = 2; 195 | GridBagConstraints c03 = new GridBagConstraints(); 196 | c03.gridy = 3; 197 | hintLabel = new JLabel("Ok to import selected profiles"); 198 | Font defaultFont = hintLabel.getFont(); 199 | hintLabel.setFont(new Font(defaultFont.getFamily(), Font.ITALIC, defaultFont.getSize())); 200 | hintLabel.setForeground(BurpExtender.textOrange); 201 | outerPanel.add(topButtonPanel, c00); 202 | outerPanel.add(profileScrollPane, c01); 203 | outerPanel.add(hintLabel, c02); 204 | outerPanel.add(lowerButtonPanel, c03); 205 | 206 | add(outerPanel); 207 | pack(); 208 | setLocationRelativeTo(BurpExtender.getBurp().getUiComponent()); 209 | } 210 | 211 | public void addSelectedProfiles() 212 | { 213 | DefaultTableModel model = (DefaultTableModel) profileTable.getModel(); 214 | for (int i = 0; i < model.getRowCount(); i++) { 215 | if ((boolean)model.getValueAt(i, SELECT_COLUMN_INDEX)) { 216 | final String name = (String) model.getValueAt(i, NAME_COLUMN_INDEX); 217 | SigProfile profile = this.profileNameMap.get(name).sigProfile; 218 | burp.addProfile(profile); 219 | model.setValueAt(false, i, SELECT_COLUMN_INDEX); 220 | } 221 | } 222 | } 223 | 224 | private void updateImportTable(List profiles, final String source) 225 | { 226 | // preserve selection status of current profiles. 227 | HashMap selectionMap = new HashMap<>(); 228 | DefaultTableModel model = (DefaultTableModel) this.profileTable.getModel(); 229 | for (int i = 0; i < model.getRowCount(); i++) { 230 | final String name = (String) model.getValueAt(i, NAME_COLUMN_INDEX); 231 | selectionMap.put(name, (boolean) model.getValueAt(i, SELECT_COLUMN_INDEX)); 232 | } 233 | model.setRowCount(0); // clear table 234 | 235 | for (SigProfile profile : profiles) { 236 | this.profileNameMap.put(profile.getName(), new NewSigProfile(profile, source)); 237 | if (!selectionMap.containsKey(profile.getName())) { 238 | selectionMap.put(profile.getName(), true); 239 | } 240 | } 241 | 242 | // sort by name in table 243 | List profileNames = new ArrayList<>(this.profileNameMap.keySet()); 244 | Collections.sort(profileNames); 245 | 246 | for (final String name : profileNames) { 247 | NewSigProfile newProfile = this.profileNameMap.get(name); 248 | model.addRow(new Object[]{selectionMap.get(name), newProfile.sigProfile.getName(), newProfile.sigProfile.getAccessKeyIdForProfileSelection(), newProfile.source}); 249 | } 250 | } 251 | 252 | private Path getAutoImportPath() 253 | { 254 | // favor path defined in environment. fallback to default path. 255 | final String envFile = System.getenv("AWS_SHARED_CREDENTIALS_FILE"); 256 | if (envFile != null) { 257 | Path credPath = Paths.get(envFile); 258 | if (Files.exists(credPath)) { 259 | return credPath; 260 | } 261 | } 262 | 263 | Path credPath = Paths.get(System.getProperty("user.home"), ".aws", "credentials"); 264 | if (Files.exists(credPath)) { 265 | return credPath; 266 | } 267 | return null; 268 | } 269 | 270 | private Path getChosenImportPath() 271 | { 272 | String chooserPath = System.getProperty("user.home"); 273 | final Path importPath = getAutoImportPath(); 274 | if (importPath != null) { 275 | chooserPath = importPath.toString(); 276 | } 277 | JFileChooser chooser = new JFileChooser(chooserPath); 278 | chooser.setFileHidingEnabled(false); 279 | if (chooser.showOpenDialog(burp.getUiComponent()) == JFileChooser.APPROVE_OPTION) { 280 | return Paths.get(chooser.getSelectedFile().getPath()); 281 | } 282 | return null; 283 | } 284 | 285 | private void importProfilesFromCLI() { 286 | List profiles = SigProfile.fromCLIConfig(); 287 | burp.logger.info(String.format("Importing %d credentials from default CLI location", profiles.size())); 288 | updateImportTable(profiles, "**AWS CLI**"); 289 | } 290 | 291 | private void importProfilesFromFile(final Path credPath) 292 | { 293 | if (!Files.exists(credPath)) { 294 | burp.logger.error(String.format("Attempted to import credentials from non-existent file: %s", credPath)); 295 | } 296 | List profiles = SigProfile.fromCredentialPath(credPath); 297 | burp.logger.info(String.format("Importing %d credentials from: %s", profiles.size(), credPath)); 298 | updateImportTable(profiles, credPath.toString()); 299 | } 300 | 301 | private void importProfilesFromEnvironment() 302 | { 303 | // try to import creds from environment variables 304 | List profiles = new ArrayList<>(); 305 | SigProfile profile = SigProfile.fromEnvironment(); 306 | if (profile != null) { 307 | profiles.add(profile); 308 | } 309 | updateImportTable(profiles, "**environment**"); 310 | } 311 | 312 | private void importProfilesFromClipboard() 313 | { 314 | // try to import creds from the clipboard. format should be one env var per line, as used by the aws cli. 315 | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 316 | String text; 317 | try { 318 | text = (String)clipboard.getData(DataFlavor.stringFlavor); 319 | } catch (IOException | UnsupportedFlavorException e) { 320 | return; 321 | } 322 | 323 | SigProfile profile = SigProfile.fromShellVars(text); 324 | if (profile != null) { 325 | updateImportTable(List.of(profile), "**clipboard**"); 326 | } 327 | } 328 | } 329 | -------------------------------------------------------------------------------- /src/main/java/burp/AdvancedSettingsDialog.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import lombok.Getter; 4 | 5 | import javax.swing.*; 6 | import javax.swing.border.TitledBorder; 7 | import java.awt.*; 8 | import java.awt.datatransfer.Clipboard; 9 | import java.awt.datatransfer.DataFlavor; 10 | import java.awt.datatransfer.StringSelection; 11 | import java.awt.datatransfer.UnsupportedFlavorException; 12 | import java.io.IOException; 13 | 14 | public class AdvancedSettingsDialog extends JDialog { 15 | 16 | private static AdvancedSettingsDialog settingsDialog = null; 17 | private JLabel statusLabel; 18 | private static final String DEFAULT_STATUS_LABEL_TEXT = "Ok to submit"; 19 | 20 | protected final JCheckBox signingEnabledForProxyCheckbox = new JCheckBox("Proxy"); 21 | protected final JCheckBox signingEnabledForSpiderCheckBox = new JCheckBox("Spider"); 22 | protected final JCheckBox signingEnabledForScannerCheckBox = new JCheckBox("Scanner"); 23 | protected final JCheckBox signingEnabledForIntruderCheckBox = new JCheckBox("Intruder"); 24 | protected final JCheckBox signingEnabledForRepeaterCheckBox = new JCheckBox("Repeater"); 25 | protected final JCheckBox signingEnabledForSequencerCheckBox = new JCheckBox("Sequencer"); 26 | protected final JCheckBox signingEnabledForExtenderCheckBox = new JCheckBox("Extender"); 27 | 28 | @Getter private long presignedUrlLifetimeSeconds = ExtensionSettings.PRESIGNED_URL_LIFETIME_DEFAULT_SECONDS; 29 | private JTextField presignedUrlLifetimeTextField = new JTextField(Long.toString(ExtensionSettings.PRESIGNED_URL_LIFETIME_DEFAULT_SECONDS), 5); 30 | 31 | protected final JCheckBox preserveHeaderOrderCheckBox = new JCheckBox("Preserve Header Order"); 32 | protected final JCheckBox updateContentSha256CheckBox = new JCheckBox("Update content-sha256 Header"); 33 | protected final JCheckBox addProfileCommentCheckBox = new JCheckBox("Add Profile Comment"); 34 | private final JComboBox contentMD5HeaderBehaviorComboBox = new JComboBox<>(); 35 | 36 | public String getContentMD5HeaderBehavior() { 37 | return contentMD5HeaderBehaviorComboBox.getSelectedItem().toString(); 38 | } 39 | 40 | private AdvancedSettingsDialog(Frame owner, String title, boolean modal) { 41 | super(owner, title, modal); 42 | 43 | int outerPanelY = 0; 44 | JPanel outerPanel = new JPanel(); 45 | outerPanel.setLayout(new GridBagLayout()); 46 | 47 | JPanel toolPanel = new JPanel(); 48 | toolPanel.setBorder(new TitledBorder("Tools Enabled for Signing")); 49 | toolPanel.add(signingEnabledForProxyCheckbox); 50 | toolPanel.add(signingEnabledForSpiderCheckBox); 51 | toolPanel.add(signingEnabledForScannerCheckBox); 52 | toolPanel.add(signingEnabledForIntruderCheckBox); 53 | toolPanel.add(signingEnabledForRepeaterCheckBox); 54 | toolPanel.add(signingEnabledForSequencerCheckBox); 55 | toolPanel.add(signingEnabledForExtenderCheckBox); 56 | GridBagConstraints c00 = new GridBagConstraints(); 57 | c00.gridx = 0; 58 | c00.gridy = outerPanelY++; 59 | c00.anchor = GridBagConstraints.LINE_START; 60 | outerPanel.add(toolPanel, c00); 61 | 62 | JPanel miscPanel = new JPanel(new GridBagLayout()); 63 | miscPanel.setBorder(new TitledBorder("Misc")); 64 | int miscPanelY = 0; 65 | JPanel miscComboBoxPanel = new JPanel(); 66 | contentMD5HeaderBehaviorComboBox.addItem(ExtensionSettings.CONTENT_MD5_IGNORE); 67 | contentMD5HeaderBehaviorComboBox.addItem(ExtensionSettings.CONTENT_MD5_UPDATE); 68 | contentMD5HeaderBehaviorComboBox.addItem(ExtensionSettings.CONTENT_MD5_REMOVE); 69 | contentMD5HeaderBehaviorComboBox.setSelectedItem(ExtensionSettings.CONTENT_MD5_DEFAULT); 70 | miscComboBoxPanel.add(new JLabel("ContentMD5 Header Behavior")); 71 | miscComboBoxPanel.add(contentMD5HeaderBehaviorComboBox); 72 | miscComboBoxPanel.add(new JLabel("Presigned URL Lifetime Seconds")); 73 | miscComboBoxPanel.add(presignedUrlLifetimeTextField); 74 | 75 | JPanel miscCheckBoxPanel = new JPanel(); 76 | miscCheckBoxPanel.add(preserveHeaderOrderCheckBox); 77 | miscCheckBoxPanel.add(updateContentSha256CheckBox); 78 | miscCheckBoxPanel.add(addProfileCommentCheckBox); 79 | 80 | GridBagConstraints cm00 = new GridBagConstraints(); cm00.gridx = 0; cm00.gridy = miscPanelY++; cm00.anchor = GridBagConstraints.LINE_START; 81 | GridBagConstraints cm01 = new GridBagConstraints(); cm01.gridx = 0; cm01.gridy = miscPanelY++; cm01.anchor = GridBagConstraints.LINE_START; 82 | miscPanel.add(miscComboBoxPanel, cm00); 83 | miscPanel.add(miscCheckBoxPanel, cm01); 84 | GridBagConstraints c03 = new GridBagConstraints(); 85 | c03.gridx = 0; 86 | c03.gridy = outerPanelY++; 87 | c03.anchor = GridBagConstraints.LINE_START; 88 | outerPanel.add(miscPanel, c03); 89 | 90 | // import/export settings json with dialogs 91 | JPanel importExportPanel = new JPanel(); 92 | JButton settingsImportButton = new JButton("Import"); 93 | settingsImportButton.addActionListener(actionEvent -> { 94 | JDialog dialog = new JDialog((Frame)null, "Import Settings Json", true); 95 | JPanel mainPanel = new JPanel(new BorderLayout()); 96 | JTextArea textPanel = new JTextArea(); 97 | JScrollPane scrollPane = new JScrollPane(textPanel); 98 | mainPanel.add(scrollPane, BorderLayout.CENTER); 99 | JPanel buttonPanel = new JPanel(); 100 | JButton okButton = new JButton("Ok"); 101 | okButton.addActionListener(actionEvent1 -> { 102 | BurpExtender.getBurp().importExtensionSettingsFromJson(textPanel.getText()); 103 | dialog.setVisible(false); 104 | }); 105 | buttonPanel.add(okButton); 106 | JButton pasteButton = new JButton("Paste"); 107 | pasteButton.addActionListener(actionEvent1 -> { 108 | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 109 | try { 110 | textPanel.setText((String)clipboard.getData(DataFlavor.stringFlavor)); 111 | } catch (UnsupportedFlavorException | IOException ignored) { 112 | } 113 | }); 114 | buttonPanel.add(pasteButton); 115 | JButton cancelButton = new JButton("Cancel"); 116 | cancelButton.addActionListener(actionEvent1 -> { 117 | dialog.setVisible(false); 118 | }); 119 | buttonPanel.add(cancelButton); 120 | mainPanel.add(buttonPanel, BorderLayout.PAGE_END); 121 | dialog.add(mainPanel); 122 | dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 123 | // set dialog location and size 124 | final Point burpLocation = SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent()).getLocation(); 125 | dialog.setLocation(burpLocation); 126 | dialog.setPreferredSize(SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent()).getBounds().getSize()); 127 | dialog.pack(); 128 | dialog.setVisible(true); 129 | }); 130 | JButton settingsExportButton = new JButton("Export"); 131 | settingsExportButton.addActionListener(actionEvent -> { 132 | // display settings json in a new dialog 133 | JDialog dialog = new JDialog((Frame)null, "Export Settings Json", true); 134 | JPanel mainPanel = new JPanel(new BorderLayout()); 135 | JTextArea textPanel = new JTextArea(); 136 | textPanel.setText(BurpExtender.getBurp().exportExtensionSettingsToJson()); 137 | textPanel.setCaretPosition(0); // scroll to top 138 | textPanel.setEditable(false); 139 | JScrollPane scrollPane = new JScrollPane(textPanel); 140 | mainPanel.add(scrollPane, BorderLayout.CENTER); 141 | JPanel buttonPanel = new JPanel(); 142 | JButton copyToClipboardButton = new JButton("Copy to clipboard"); 143 | copyToClipboardButton.addActionListener(actionEvent12 -> { 144 | Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard(); 145 | clipboard.setContents(new StringSelection(textPanel.getText()), null); 146 | }); 147 | buttonPanel.add(copyToClipboardButton); 148 | JButton closeButton = new JButton("Close"); 149 | closeButton.addActionListener(actionEvent1 -> { 150 | dialog.setVisible(false); 151 | }); 152 | buttonPanel.add(closeButton); 153 | mainPanel.add(buttonPanel, BorderLayout.PAGE_END); 154 | dialog.add(mainPanel); 155 | dialog.setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 156 | // place dialog in upper left of burp window 157 | final Point burpLocation = SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent()).getLocation(); 158 | dialog.setLocation(burpLocation); 159 | // make sure dialog height and width do not exceed burp window height and width 160 | final int height = SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent()).getBounds().getSize().height; 161 | final int width = SwingUtilities.getWindowAncestor(BurpExtender.getBurp().getUiComponent()).getBounds().getSize().width; 162 | dialog.pack(); 163 | dialog.setSize(Integer.min(width, dialog.getSize().width), height); 164 | dialog.setVisible(true); 165 | }); 166 | importExportPanel.setBorder(new TitledBorder("Settings JSON")); 167 | importExportPanel.add(settingsImportButton); 168 | importExportPanel.add(settingsExportButton); 169 | GridBagConstraints c02 = new GridBagConstraints(); 170 | c02.gridx = 0; 171 | c02.gridy = outerPanelY++; 172 | c02.anchor = GridBagConstraints.LINE_START; 173 | outerPanel.add(importExportPanel, c02); 174 | 175 | // status message 176 | statusLabel = new JLabel(DEFAULT_STATUS_LABEL_TEXT); 177 | Font defaultFont = statusLabel.getFont(); 178 | statusLabel.setFont(new Font(defaultFont.getFamily(), Font.ITALIC, defaultFont.getSize())); 179 | statusLabel.setForeground(BurpExtender.textOrange); 180 | GridBagConstraints c04 = new GridBagConstraints(); 181 | c04.gridx = 0; 182 | c04.gridy = outerPanelY++; 183 | c04.anchor = GridBagConstraints.CENTER; 184 | outerPanel.add(statusLabel, c04); 185 | 186 | JPanel lowerButtonPanel = new JPanel(); 187 | JButton okButton = new JButton("Ok"); 188 | okButton.addActionListener(actionEvent -> { 189 | try { 190 | validateSettings(); 191 | setVisible(false); 192 | statusLabel.setText(DEFAULT_STATUS_LABEL_TEXT); 193 | } catch (IllegalArgumentException e) { 194 | // TODO: line wrap 195 | statusLabel.setText(e.getMessage()); 196 | } 197 | pack(); 198 | }); 199 | lowerButtonPanel.add(okButton); 200 | GridBagConstraints c01 = new GridBagConstraints(); 201 | c01.gridx = 0; 202 | c01.gridy = outerPanelY++; 203 | c01.anchor = GridBagConstraints.PAGE_END; 204 | outerPanel.add(lowerButtonPanel, c01); 205 | 206 | add(outerPanel); 207 | pack(); 208 | } 209 | 210 | // Validate settings updated in this dialog only. This dialog is initialized with values that should have already 211 | // been validated. 212 | // 213 | // Note that settings which require validation are not saved until validation succeeds ("Ok" button is pressed 214 | // and dialog disappears). Other settings take effect immediately (all check boxes and combo boxes). 215 | private void validateSettings() { 216 | long lifetime; 217 | try { 218 | lifetime = Long.parseLong(presignedUrlLifetimeTextField.getText()); 219 | } catch (NumberFormatException e) { 220 | throw new IllegalArgumentException("Expected an integer for presigned URL lifetime"); 221 | } 222 | 223 | if (lifetime < ExtensionSettings.PRESIGNED_URL_LIFETIME_MIN_SECONDS || lifetime > ExtensionSettings.PRESIGNED_URL_LIFETIME_MAX_SECONDS) { 224 | throw new IllegalArgumentException(String.format("Presigned URL lifetime must be between %d and %d, inclusive", 225 | ExtensionSettings.PRESIGNED_URL_LIFETIME_MIN_SECONDS, ExtensionSettings.PRESIGNED_URL_LIFETIME_MAX_SECONDS)); 226 | } 227 | presignedUrlLifetimeSeconds = lifetime; 228 | } 229 | 230 | public void applyExtensionSettings(final ExtensionSettings settings) { 231 | signingEnabledForProxyCheckbox.setSelected(settings.signingEnabledForProxy()); 232 | signingEnabledForSpiderCheckBox.setSelected(settings.signingEnabledForSpider()); 233 | signingEnabledForScannerCheckBox.setSelected(settings.signingEnabledForScanner()); 234 | signingEnabledForIntruderCheckBox.setSelected(settings.signingEnabledForIntruder()); 235 | signingEnabledForRepeaterCheckBox.setSelected(settings.signingEnabledForRepeater()); 236 | signingEnabledForSequencerCheckBox.setSelected(settings.signingEnabledForSequencer()); 237 | signingEnabledForExtenderCheckBox.setSelected(settings.signingEnabledForExtender()); 238 | 239 | preserveHeaderOrderCheckBox.setSelected(settings.preserveHeaderOrder()); 240 | updateContentSha256CheckBox.setSelected(settings.updateContentSha256()); 241 | addProfileCommentCheckBox.setSelected(settings.addProfileComment()); 242 | contentMD5HeaderBehaviorComboBox.setSelectedItem(settings.contentMD5HeaderBehavior()); 243 | presignedUrlLifetimeSeconds = settings.presignedUrlLifetimeInSeconds(); 244 | } 245 | 246 | // recenter the dialog every time 247 | public void setVisible(final boolean visible) { 248 | if (visible) { 249 | setLocationRelativeTo(BurpExtender.getBurp().getUiComponent()); 250 | } 251 | super.setVisible(visible); 252 | } 253 | 254 | public static AdvancedSettingsDialog get() { 255 | if (settingsDialog == null) { 256 | settingsDialog = new AdvancedSettingsDialog(null, "Advanced Settings", true); 257 | } 258 | return settingsDialog; 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /src/main/java/burp/SigProfileEditorDialog.java: -------------------------------------------------------------------------------- 1 | package burp; 2 | 3 | import javax.swing.*; 4 | import javax.swing.border.TitledBorder; 5 | import java.awt.*; 6 | import java.awt.event.ActionListener; 7 | import java.awt.event.FocusEvent; 8 | import java.awt.event.FocusListener; 9 | import java.time.Instant; 10 | import java.util.List; 11 | 12 | public class SigProfileEditorDialog extends JDialog 13 | { 14 | static final Color disabledColor = new Color(161, 161, 161); 15 | private static final BurpExtender burp = BurpExtender.getBurp(); 16 | 17 | protected JTextField nameTextField; 18 | protected JTextField profileKeyIdTextField; 19 | protected JTextFieldHint regionTextField; 20 | protected JTextFieldHint serviceTextField; 21 | 22 | protected JButton okButton; 23 | protected JPanel providerPanel; 24 | 25 | // static creds fields 26 | private JRadioButton staticProviderRadioButton; 27 | private JTextField accessKeyIdTextField; 28 | protected JTextField secretKeyTextField; 29 | protected JTextField sessionTokenTextField; 30 | 31 | // Assume role fields 32 | private JRadioButton assumeRoleProviderRadioButton; 33 | private JTextField roleArnTextField; 34 | private JTextField sessionNameTextField; 35 | private JTextField externalIdTextField; 36 | 37 | // Http provider 38 | private JRadioButton httpProviderRadioButton; 39 | private JRadioButton awsProfileProviderRadioButton; 40 | private JTextField httpProviderUrlField; 41 | private JTextField httpProviderHeaderField; 42 | 43 | private JTextField awsProfileNameField; 44 | 45 | private JLabel statusLabel; 46 | private String newProfileName = null; 47 | 48 | // allow creator of dialog to get the profile that was created 49 | public String getNewProfileName() { return newProfileName; } 50 | 51 | private static GridBagConstraints newConstraint(int gridx, int gridy, int gridwidth, int gridheight) 52 | { 53 | GridBagConstraints c = new GridBagConstraints(); 54 | c.gridy = gridy; 55 | c.gridx = gridx; 56 | c.gridwidth = gridwidth; 57 | c.gridheight = gridheight; 58 | c.insets = new Insets(2, 2, 2, 2); 59 | return c; 60 | } 61 | 62 | private static GridBagConstraints newConstraint(int gridx, int gridy, int anchor) 63 | { 64 | GridBagConstraints c = newConstraint(gridx, gridy, 1, 1); 65 | c.anchor = anchor; 66 | return c; 67 | } 68 | 69 | private static GridBagConstraints newConstraint(int gridx, int gridy) 70 | { 71 | return newConstraint(gridx, gridy, 1, 1); 72 | } 73 | 74 | /* 75 | return a dialog with a form for editing profiles. optional profile param can be used to populate the form. 76 | set profile to null for a create form. 77 | */ 78 | public SigProfileEditorDialog(Frame owner, String title, boolean modal, SigProfile profile) 79 | { 80 | super(owner, title, modal); 81 | setResizable(false); 82 | setDefaultCloseOperation(JDialog.DISPOSE_ON_CLOSE); 83 | 84 | JPanel outerPanel = new JPanel(new GridBagLayout()); 85 | final int TEXT_FIELD_WIDTH = 40; 86 | int outerPanelY = 0; 87 | int providerPanelY = 0; 88 | 89 | // panel for required fields 90 | JPanel basicPanel = new JPanel(new GridBagLayout()); 91 | basicPanel.setBorder(new TitledBorder("Profile")); 92 | basicPanel.add(new JLabel("Name"), newConstraint(0, 0, GridBagConstraints.LINE_START)); 93 | this.nameTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH, "Required"); 94 | basicPanel.add(nameTextField, newConstraint(1, 0)); 95 | basicPanel.add(new JLabel("KeyId"), newConstraint(0, 1, GridBagConstraints.LINE_START)); 96 | this.profileKeyIdTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH, "Optional - Match with AccessKeyId in incoming requests"); 97 | this.profileKeyIdTextField.setToolTipText("Look for this AccessKeyId in a request to automatically select this profile"); 98 | basicPanel.add(profileKeyIdTextField, newConstraint(1, 1)); 99 | basicPanel.add(new JLabel("Region"), newConstraint(0, 2, GridBagConstraints.LINE_START)); 100 | // for add profile dialog, fill default region 101 | this.regionTextField = new JTextFieldHint(profile == null ? SigProfile.getDefaultRegion() : "", TEXT_FIELD_WIDTH, "Optional"); 102 | basicPanel.add(regionTextField, newConstraint(1, 2)); 103 | basicPanel.add(new JLabel("Service"), newConstraint(0, 3, GridBagConstraints.LINE_START)); 104 | this.serviceTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH, "Optional"); 105 | basicPanel.add(serviceTextField, newConstraint(1, 3)); 106 | outerPanel.add(basicPanel, newConstraint(0, outerPanelY++, GridBagConstraints.LINE_START)); 107 | 108 | providerPanel = new JPanel(new GridBagLayout()); 109 | 110 | // RadioButton panel for selecting credential provider 111 | staticProviderRadioButton = new JRadioButton("Static credentials"); 112 | staticProviderRadioButton.setSelected(true); //default 113 | assumeRoleProviderRadioButton = new JRadioButton("AssumeRole"); 114 | httpProviderRadioButton = new JRadioButton("HttpGet"); 115 | awsProfileProviderRadioButton = new JRadioButton("AWS Profile"); 116 | ButtonGroup providerButtonGroup = new ButtonGroup(); 117 | providerButtonGroup.add(staticProviderRadioButton); 118 | providerButtonGroup.add(assumeRoleProviderRadioButton); 119 | providerButtonGroup.add(httpProviderRadioButton); 120 | providerButtonGroup.add(awsProfileProviderRadioButton); 121 | JPanel providerButtonPanel = new JPanel(new FlowLayout()); 122 | providerButtonPanel.add(staticProviderRadioButton); 123 | providerButtonPanel.add(assumeRoleProviderRadioButton); 124 | providerButtonPanel.add(httpProviderRadioButton); 125 | providerButtonPanel.add(awsProfileProviderRadioButton); 126 | providerPanel.add(providerButtonPanel, newConstraint(0, providerPanelY++, GridBagConstraints.LINE_START)); 127 | 128 | // panel for static credentials 129 | JPanel staticCredentialsPanel = new JPanel(new GridBagLayout()); 130 | staticCredentialsPanel.setBorder(new TitledBorder("Credentials")); 131 | staticCredentialsPanel.add(new JLabel("AccessKeyId"), newConstraint(0, 0, GridBagConstraints.LINE_START)); 132 | this.accessKeyIdTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH-3, "Required"); 133 | staticCredentialsPanel.add(accessKeyIdTextField, newConstraint(1, 0)); 134 | staticCredentialsPanel.add(new JLabel("SecretKey"), newConstraint(0, 1, GridBagConstraints.LINE_START)); 135 | this.secretKeyTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH-3, "Required"); 136 | staticCredentialsPanel.add(secretKeyTextField, newConstraint(1, 1)); 137 | staticCredentialsPanel.add(new JLabel("SessionToken"), newConstraint(0, 2, GridBagConstraints.LINE_START)); 138 | this.sessionTokenTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH-3, "Optional"); 139 | staticCredentialsPanel.add(sessionTokenTextField, newConstraint(1, 2)); 140 | providerPanel.add(staticCredentialsPanel, newConstraint(0, providerPanelY++, GridBagConstraints.LINE_START)); 141 | 142 | // panel for assume role fields 143 | JPanel rolePanel = new JPanel(new GridBagLayout()); 144 | rolePanel.setBorder(new TitledBorder("Role")); 145 | rolePanel.add(new JLabel("RoleArn"), newConstraint(0, 0, GridBagConstraints.LINE_START)); 146 | this.roleArnTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH-3, "Required"); 147 | rolePanel.add(this.roleArnTextField, newConstraint(1, 0)); 148 | rolePanel.add(new JLabel("SessionName"), newConstraint(0, 1, GridBagConstraints.LINE_START)); 149 | this.sessionNameTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH-3, "Optional"); 150 | rolePanel.add(this.sessionNameTextField, newConstraint(1, 1)); 151 | rolePanel.add(new JLabel("ExternalId"), newConstraint(0, 2, GridBagConstraints.LINE_START)); 152 | this.externalIdTextField = new JTextFieldHint("", TEXT_FIELD_WIDTH-3, "Optional"); 153 | rolePanel.add(this.externalIdTextField, newConstraint(1, 2)); 154 | providerPanel.add(rolePanel, newConstraint(0, providerPanelY++, GridBagConstraints.LINE_START)); 155 | 156 | // panel for http provided creds 157 | JPanel httpPanel = new JPanel(new GridBagLayout()); 158 | httpPanel.setBorder(new TitledBorder("Http Credentials")); 159 | httpPanel.add(new JLabel("GET Url"), newConstraint(0, 0, GridBagConstraints.LINE_START)); 160 | this.httpProviderUrlField = new JTextFieldHint("", TEXT_FIELD_WIDTH-2, "Required"); 161 | httpPanel.add(this.httpProviderUrlField, newConstraint(1, 0)); 162 | httpPanel.add(new JLabel("Header"), newConstraint(0, 1, GridBagConstraints.LINE_START)); 163 | httpProviderHeaderField = new JTextFieldHint("", TEXT_FIELD_WIDTH-2, "Optional"); 164 | httpPanel.add(httpProviderHeaderField, newConstraint(1, 1)); 165 | providerPanel.add(httpPanel, newConstraint(0, providerPanelY++, GridBagConstraints.LINE_START)); 166 | 167 | // panel for AWS profile creds 168 | JPanel awsProfilePanel = new JPanel(new GridBagLayout()); 169 | awsProfilePanel.setBorder(new TitledBorder("AWS Profile Credentials")); 170 | awsProfilePanel.add(new JLabel("Profile Name"), newConstraint(0, 0, GridBagConstraints.LINE_START)); 171 | this.awsProfileNameField = new JTextFieldHint("", TEXT_FIELD_WIDTH-2, "Required"); 172 | awsProfilePanel.add(this.awsProfileNameField, newConstraint(1, 0)); 173 | // build combobox of all profile names so user can populate the name field automatically 174 | JComboBox awsProfileNameComboBox = new JComboBox<>(); 175 | awsProfileNameComboBox.addItem("")) { 179 | awsProfileNameField.setText(profileName); 180 | if (nameTextField.getText().equals("")) { 181 | // autofill the profile name if it's empty 182 | nameTextField.setText(profileName); 183 | } 184 | } 185 | awsProfileNameComboBox.setSelectedIndex(0); // reset to "