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 |
8 | Credentials can be imported from a file or environment variables.
9 | Automatically select a profile based on the key id in the request.
10 | Resend requests with different credentials.
11 | Context menu item for copying s3 presigned URLs.
12 | Assume a role by providing a role ARN and optional external ID.
13 |
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 | 
125 |
126 | Importing profiles
127 |
128 | 
129 |
130 | Editing a profile
131 |
132 | 
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("");
176 | ActionListener awsProfileSelectionListener = actionEvent -> {
177 | final String profileName = (String)awsProfileNameComboBox.getSelectedItem();
178 | if (profileName != null && !profileName.equals("")) {
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 ""
186 | };
187 | awsProfileNameComboBox.addActionListener(awsProfileSelectionListener);
188 | List awsProfileOptions = SigAwsProfileCredentialProvider.getAvailableProfileNames();
189 | awsProfileOptions.forEach(awsProfileNameComboBox::addItem);
190 | awsProfilePanel.add(awsProfileNameComboBox, newConstraint(1, 1, GridBagConstraints.BASELINE_LEADING));
191 | providerPanel.add(awsProfilePanel, newConstraint(0, providerPanelY++, GridBagConstraints.LINE_START));
192 |
193 | outerPanel.add(providerPanel, newConstraint(0, outerPanelY++, GridBagConstraints.LINE_START));
194 | statusLabel = new JLabel("Ok to submit");
195 | Font defaultFont = statusLabel.getFont();
196 | statusLabel.setFont(new Font(defaultFont.getFamily(), Font.ITALIC, defaultFont.getSize()));
197 | statusLabel.setForeground(BurpExtender.textOrange);
198 | okButton = new JButton("Ok");
199 | JButton cancelButton = new JButton("Cancel");
200 |
201 | JPanel buttonPanel = new JPanel();
202 | buttonPanel.add(okButton);
203 | buttonPanel.add(cancelButton);
204 | outerPanel.add(statusLabel, newConstraint(0, outerPanelY++, 2, 1));
205 | outerPanel.add(buttonPanel, newConstraint(0, outerPanelY++, 2, 1));
206 |
207 | ActionListener providerButtonActionListener = actionEvent -> {
208 | staticCredentialsPanel.setVisible(staticProviderRadioButton.isSelected());
209 | rolePanel.setVisible(assumeRoleProviderRadioButton.isSelected());
210 | httpPanel.setVisible(httpProviderRadioButton.isSelected());
211 | awsProfilePanel.setVisible(awsProfileProviderRadioButton.isSelected());
212 | if (actionEvent.getSource().equals(assumeRoleProviderRadioButton)) {
213 | staticCredentialsPanel.setVisible(true);
214 | }
215 | pack();
216 | };
217 | this.staticProviderRadioButton.addActionListener(providerButtonActionListener);
218 | this.assumeRoleProviderRadioButton.addActionListener(providerButtonActionListener);
219 | this.httpProviderRadioButton.addActionListener(providerButtonActionListener);
220 | this.awsProfileProviderRadioButton.addActionListener(providerButtonActionListener);
221 |
222 | cancelButton.addActionListener(actionEvent -> {
223 | setVisible(false);
224 | dispose();
225 | });
226 | okButton.addActionListener(actionEvent -> {
227 | SigAssumeRoleCredentialProvider assumeRole = null;
228 | final String accessKeyId = accessKeyIdTextField.getText();
229 | final String secretKey = secretKeyTextField.getText();
230 | final String sessionToken = sessionTokenTextField.getText();
231 |
232 | SigCredential staticCredential = null;
233 | try {
234 | if (!sessionToken.isEmpty()) {
235 | staticCredential = new SigTemporaryCredential(accessKeyId, secretKey, sessionToken, Instant.now().getEpochSecond() + 86400);
236 | } else {
237 | staticCredential = new SigStaticCredential(accessKeyId, secretKey);
238 | }
239 | } catch (IllegalArgumentException ignore) {
240 | // ignore
241 | }
242 |
243 | try {
244 | if (!roleArnTextField.getText().isEmpty()) {
245 | if (staticCredential == null) {
246 | throw new IllegalArgumentException("AssumeRole profile requires credentials");
247 | }
248 | if (profile != null && profile.getAssumeRole() != null) {
249 | // edit existing AssumeRole profile
250 | assumeRole = new SigAssumeRoleCredentialProvider.Builder(profile.getAssumeRole())
251 | .withRoleArn(roleArnTextField.getText())
252 | .withCredential(staticCredential)
253 | .tryExternalId(externalIdTextField.getText())
254 | .tryRoleSessionName(sessionNameTextField.getText())
255 | .build();
256 | } else {
257 | // new AssumeRole profile
258 | assumeRole = new SigAssumeRoleCredentialProvider.Builder(roleArnTextField.getText(), staticCredential)
259 | .tryExternalId(externalIdTextField.getText())
260 | .tryRoleSessionName(sessionNameTextField.getText())
261 | .build();
262 | }
263 | }
264 |
265 | SigProfile.Builder newProfileBuilder = new SigProfile.Builder(nameTextField.getText())
266 | .withRegion(regionTextField.getText())
267 | .withService(serviceTextField.getText());
268 | if (!profileKeyIdTextField.getText().isEmpty())
269 | newProfileBuilder.withAccessKeyId(profileKeyIdTextField.getText());
270 |
271 | if (!httpProviderUrlField.getText().isEmpty()) {
272 | newProfileBuilder.withCredentialProvider(new SigHttpCredentialProvider(httpProviderUrlField.getText(), httpProviderHeaderField.getText()),
273 | httpProviderRadioButton.isSelected() ? SigProfile.DEFAULT_HTTP_PRIORITY : SigProfile.DISABLED_PRIORITY);
274 | }
275 |
276 | if (!awsProfileNameField.getText().isEmpty()) {
277 | newProfileBuilder.withCredentialProvider(new SigAwsProfileCredentialProvider(awsProfileNameField.getText()),
278 | awsProfileProviderRadioButton.isSelected() ? SigProfile.DEFAULT_AWS_PROFILE_PRIORITY : SigProfile.DISABLED_PRIORITY);
279 | }
280 |
281 | if (assumeRole != null)
282 | newProfileBuilder.withCredentialProvider(assumeRole, assumeRoleProviderRadioButton.isSelected() ? SigProfile.DEFAULT_ASSUMEROLE_PRIORITY : SigProfile.DISABLED_PRIORITY);
283 |
284 | // if any cred fields are specified, attempt to use them.
285 | if (staticCredential != null) {
286 | newProfileBuilder.withCredentialProvider(new SigStaticCredentialProvider(staticCredential), SigProfile.DEFAULT_STATIC_PRIORITY);
287 | }
288 |
289 | final SigProfile newProfile = newProfileBuilder.build();
290 | if (newProfile.getCredentialProviderCount() <= 0) {
291 | throw new IllegalArgumentException("Must provide at least 1 authentication method");
292 | }
293 | burp.updateProfile(profile, newProfile);
294 | newProfileName = newProfile.getName();
295 | setVisible(false);
296 | dispose();
297 | } catch (IllegalArgumentException exc) {
298 | setStatusLabel("Invalid settings: " + exc.getMessage());
299 | }
300 | });
301 |
302 | // populate fields with existing profile for an "edit" dialog.
303 | staticCredentialsPanel.setVisible(staticProviderRadioButton.isSelected());
304 | httpPanel.setVisible(httpProviderRadioButton.isSelected());
305 | rolePanel.setVisible(assumeRoleProviderRadioButton.isSelected());
306 | awsProfilePanel.setVisible(awsProfileProviderRadioButton.isSelected());
307 | applyProfile(profile);
308 |
309 | add(outerPanel);
310 | pack();
311 | // setting to burp.getUiComponent() is not sufficient for dialogs popped outside the SigV4 tab.
312 | setLocationRelativeTo(SwingUtilities.getWindowAncestor(burp.getUiComponent()));
313 | }
314 |
315 | protected void setStatusLabel(final String message)
316 | {
317 | statusLabel.setText(message);
318 | pack();
319 | }
320 |
321 | protected void applyProfile(final SigProfile profile)
322 | {
323 | if (profile != null) {
324 | nameTextField.setText(profile.getName());
325 | if (profile.getAccessKeyId() != null) {
326 | profileKeyIdTextField.setText(profile.getAccessKeyId());
327 | }
328 | regionTextField.setText(profile.getRegion());
329 | serviceTextField.setText(profile.getService());
330 | if (profile.getStaticCredentialProvider() != null) {
331 | SigCredential credential = profile.getStaticCredentialProvider().getCredential();
332 | accessKeyIdTextField.setText(credential.getAccessKeyId());
333 | secretKeyTextField.setText(credential.getSecretKey());
334 | if (credential.isTemporary()) {
335 | sessionTokenTextField.setText(((SigTemporaryCredential)credential).getSessionToken());
336 | }
337 | if (profile.getStaticCredentialProviderPriority() >= 0) {
338 | staticProviderRadioButton.doClick();
339 | }
340 | }
341 | if (profile.getAssumeRole() != null) {
342 | roleArnTextField.setText(profile.getAssumeRole().getRoleArn());
343 | sessionNameTextField.setText(profile.getAssumeRole().getSessionName());
344 | externalIdTextField.setText(profile.getAssumeRole().getExternalId());
345 | // initialize static creds as well
346 | accessKeyIdTextField.setText(profile.getAssumeRole().getStaticCredential().getAccessKeyId());
347 | secretKeyTextField.setText(profile.getAssumeRole().getStaticCredential().getSecretKey());
348 | if (profile.getAssumeRole().getStaticCredential().isTemporary()) {
349 | sessionTokenTextField.setText(((SigTemporaryCredential)profile.getAssumeRole().getStaticCredential()).getSessionToken());
350 | }
351 | if (profile.getAssumeRolePriority() >= 0) {
352 | assumeRoleProviderRadioButton.doClick();
353 | }
354 | }
355 | if (profile.getHttpCredentialProvider() != null) {
356 | httpProviderUrlField.setText(profile.getHttpCredentialProvider().getRequestUri().toString());
357 | profile.getHttpCredentialProvider().getCustomHeader().ifPresent(s -> httpProviderHeaderField.setText(s));
358 | if (profile.getHttpCredentialProviderPriority() >= 0) {
359 | httpProviderRadioButton.doClick();
360 | }
361 | }
362 | if (profile.getAwsProfileCredentialProvider() != null) {
363 | awsProfileNameField.setText(profile.getAwsProfileCredentialProvider().getProfileName());
364 | if (profile.getAwsProfileCredentialProviderPriority() >= 0) {
365 | awsProfileProviderRadioButton.doClick();
366 | }
367 | }
368 | }
369 | }
370 | }
371 |
372 |
373 | /*
374 | This class implements a JTextField with "Optional" hint text when no user input is present.
375 | */
376 | class JTextFieldHint extends JTextField implements FocusListener
377 | {
378 | private Font defaultFont;
379 | private Color defaultForegroundColor;
380 | final private Color hintForegroundColor = SigProfileEditorDialog.disabledColor;;
381 | private String hintText;
382 | private boolean isHintSet = true;
383 |
384 | public JTextFieldHint(String content, int width, String hintText) {
385 | // set text below to prevent NullPointerException
386 | super(width);
387 | this.hintText = hintText;
388 | init();
389 | setText(content);
390 | }
391 |
392 | void init() {
393 | defaultFont = getFont();
394 | addFocusListener(this);
395 | defaultForegroundColor = getForeground();
396 | if (super.getText().equals("")) {
397 | displayHintText();
398 | }
399 | }
400 |
401 | @Override
402 | public String getText() {
403 | // make sure we don't return "Optional" when these fields are saved
404 | if (isHintSet) {
405 | return "";
406 | }
407 | return super.getText();
408 | }
409 |
410 | @Override
411 | public void setText(final String text) {
412 | if (!text.equals("")) {
413 | setUserText(text);
414 | }
415 | else {
416 | displayHintText();
417 | }
418 | }
419 |
420 | protected void setHintText(final String text) {
421 | this.hintText = text;
422 | if (isHintSet) {
423 | displayHintText();
424 | }
425 | }
426 |
427 | protected void displayHintText() {
428 | isHintSet = true;
429 | setForeground(hintForegroundColor);
430 | super.setText(hintText);
431 | }
432 |
433 | private void setUserText(final String text) {
434 | isHintSet = false;
435 | setFont(defaultFont);
436 | setForeground(defaultForegroundColor);
437 | super.setText(text);
438 | }
439 |
440 | @Override
441 | public void focusGained(FocusEvent focusEvent) {
442 | if (isHintSet) {
443 | setUserText("");
444 | }
445 | }
446 |
447 | @Override
448 | public void focusLost(FocusEvent focusEvent) {
449 | if (super.getText().equals("")) {
450 | displayHintText();
451 | }
452 | }
453 | }
454 |
455 |
--------------------------------------------------------------------------------
/src/main/java/burp/SigProfile.java:
--------------------------------------------------------------------------------
1 | package burp;
2 |
3 | import burp.error.SigCredentialProviderException;
4 | import org.apache.commons.lang3.StringUtils;
5 | import software.amazon.awssdk.profiles.Profile;
6 | import software.amazon.awssdk.profiles.ProfileFile;
7 |
8 | import java.io.IOException;
9 | import java.nio.file.Files;
10 | import java.nio.file.Path;
11 | import java.nio.file.Paths;
12 | import java.nio.file.StandardOpenOption;
13 | import java.time.Instant;
14 | import java.util.*;
15 | import java.util.regex.Matcher;
16 | import java.util.regex.Pattern;
17 |
18 | /*
19 | Class represents a credential set for AWS services. Provides functionality
20 | to import credentials from environment vars or a credential file.
21 | */
22 | public class SigProfile implements Cloneable
23 | {
24 | public static final int DEFAULT_STATIC_PRIORITY = 100;
25 | public static final int DEFAULT_HTTP_PRIORITY = 20;
26 | public static final int DEFAULT_ASSUMEROLE_PRIORITY = 50;
27 | public static final int DEFAULT_AWS_PROFILE_PRIORITY = 60;
28 | public static final int DISABLED_PRIORITY = -1;
29 |
30 | private static final transient LogWriter logger = LogWriter.getLogger();
31 |
32 | private String name;
33 | private String region;
34 | private String service;
35 | // accessKeyId is used to uniquely identify this profile for signing
36 | private String accessKeyId;
37 |
38 | private HashMap credentialProviders;
39 | private HashMap credentialProvidersPriority;
40 |
41 | // see https://docs.aws.amazon.com/IAM/latest/APIReference/API_AccessKey.html
42 | public static final Pattern profileNamePattern = Pattern.compile("^[\\w+=,\\.@\\-]{1,64}$");
43 | public static final Pattern accessKeyIdPattern = Pattern.compile("^[\\w]{16,128}$");
44 | public static final Pattern regionPattern = Pattern.compile("^[a-zA-Z]{1,4}-(?:gov-)?[a-zA-Z]{1,16}-[0-9]{1,2}$");
45 | public static final Pattern servicePattern = Pattern.compile("^[\\w_\\.\\-]{1,64}$");
46 |
47 | public String getName() { return this.name; }
48 | public SigAssumeRoleCredentialProvider getAssumeRole()
49 | {
50 | return getAssumeRoleCredentialProvider();
51 | }
52 |
53 | private SigCredentialProvider getCredentialProviderByName(final String name) {
54 | return credentialProviders.getOrDefault(name, null);
55 | }
56 |
57 | public SigStaticCredentialProvider getStaticCredentialProvider()
58 | {
59 | return (SigStaticCredentialProvider) getCredentialProviderByName(SigStaticCredentialProvider.PROVIDER_NAME);
60 | }
61 |
62 | public SigAssumeRoleCredentialProvider getAssumeRoleCredentialProvider()
63 | {
64 | return (SigAssumeRoleCredentialProvider) getCredentialProviderByName(SigAssumeRoleCredentialProvider.PROVIDER_NAME);
65 | }
66 |
67 | public SigHttpCredentialProvider getHttpCredentialProvider()
68 | {
69 | return (SigHttpCredentialProvider) getCredentialProviderByName(SigHttpCredentialProvider.PROVIDER_NAME);
70 | }
71 |
72 | public SigAwsProfileCredentialProvider getAwsProfileCredentialProvider()
73 | {
74 | return (SigAwsProfileCredentialProvider) getCredentialProviderByName(SigAwsProfileCredentialProvider.PROVIDER_NAME);
75 | }
76 |
77 | public int getStaticCredentialProviderPriority()
78 | {
79 | SigCredentialProvider provider = getStaticCredentialProvider();
80 | if (provider != null)
81 | return credentialProvidersPriority.get(provider.getName());
82 | return DISABLED_PRIORITY;
83 | }
84 |
85 | public int getAssumeRolePriority()
86 | {
87 | SigCredentialProvider provider = getAssumeRoleCredentialProvider();
88 | if (provider != null)
89 | return credentialProvidersPriority.get(provider.getName());
90 | return DISABLED_PRIORITY;
91 | }
92 |
93 | public int getHttpCredentialProviderPriority()
94 | {
95 | SigCredentialProvider provider = getHttpCredentialProvider();
96 | if (provider != null)
97 | return credentialProvidersPriority.get(provider.getName());
98 | return DISABLED_PRIORITY;
99 | }
100 |
101 | public int getAwsProfileCredentialProviderPriority()
102 | {
103 | SigCredentialProvider provider = getAwsProfileCredentialProvider();
104 | if (provider != null)
105 | return credentialProvidersPriority.get(provider.getName());
106 | return DISABLED_PRIORITY;
107 | }
108 |
109 | public int getCredentialProviderCount()
110 | {
111 | return this.credentialProviders.size();
112 | }
113 |
114 | public String getRegion() { return this.region; }
115 | public String getService() { return this.service; }
116 |
117 | // NOTE that this value is used for matching incoming requests only and DOES NOT represent the accessKeyId
118 | // used to sign the request
119 | public String getAccessKeyId() { return this.accessKeyId; }
120 |
121 | /*
122 | get the signature accessKeyId that should be used for selecting this profile
123 | */
124 | public String getAccessKeyIdForProfileSelection()
125 | {
126 | if (getAccessKeyId() != null) {
127 | return getAccessKeyId();
128 | }
129 | if (getStaticCredentialProvider() != null) {
130 | return getStaticCredentialProvider().getCredential().getAccessKeyId();
131 | }
132 | return null;
133 | }
134 |
135 | private void setName(final String name) {
136 | if (profileNamePattern.matcher(name).matches())
137 | this.name = name;
138 | else
139 | throw new IllegalArgumentException("Profile name must match pattern "+profileNamePattern.pattern());
140 | }
141 |
142 | private void setRegion(final String region) {
143 | if (region.equals("") || regionPattern.matcher(region).matches())
144 | this.region = region;
145 | else
146 | throw new IllegalArgumentException("Profile region must match pattern " + regionPattern.pattern());
147 | }
148 |
149 | private void setService(final String service) {
150 | if (service.equals("") || servicePattern.matcher(service).matches())
151 | this.service = service;
152 | else
153 | throw new IllegalArgumentException("Profile service must match pattern " + servicePattern.pattern());
154 | }
155 |
156 | private void setAccessKeyId(final String accessKeyId) {
157 | if (accessKeyIdPattern.matcher(accessKeyId).matches())
158 | this.accessKeyId = accessKeyId;
159 | else
160 | throw new IllegalArgumentException("Profile accessKeyId must match pattern " + accessKeyIdPattern.pattern());
161 | }
162 |
163 | private void setCredentialProvider(final SigCredentialProvider provider, final int priority) {
164 | if (provider == null) {
165 | throw new IllegalArgumentException("Cannot set a null credential provider");
166 | }
167 | this.credentialProviders.put(provider.getName(), provider);
168 | this.credentialProvidersPriority.put(provider.getName(), priority);
169 | }
170 |
171 | public static class Builder {
172 | private SigProfile profile;
173 | public Builder(final String name) {
174 | this.profile = new SigProfile(name);
175 | }
176 | public Builder(final SigProfile profile) {
177 | this.profile = profile.clone();
178 | }
179 | public Builder withAccessKeyId(final String accessKeyId) {
180 | this.profile.setAccessKeyId(accessKeyId);
181 | return this;
182 | }
183 | public Builder withRegion(final String region) {
184 | this.profile.setRegion(region);
185 | return this;
186 | }
187 | public Builder withService(final String service) {
188 | this.profile.setService(service);
189 | return this;
190 | }
191 | public Builder withCredentialProvider(final SigCredentialProvider provider, final int priority) {
192 | // should only have 1 of each type: permanent/static, assumeRole, etc
193 | this.profile.setCredentialProvider(provider, priority);
194 | return this;
195 | }
196 | public SigProfile build() {
197 | return this.profile;
198 | }
199 | }
200 |
201 | public SigProfile clone() {
202 | SigProfile.Builder builder = new SigProfile.Builder(this.name)
203 | .withRegion(this.region)
204 | .withService(this.service);
205 | if (StringUtils.isNotEmpty(this.accessKeyId))
206 | builder.withAccessKeyId(this.accessKeyId);
207 | for (SigCredentialProvider provider : this.credentialProviders.values()) {
208 | builder.withCredentialProvider(provider, this.credentialProvidersPriority.get(provider.getName()));
209 | }
210 | return builder.build();
211 | }
212 |
213 | private SigProfile() {};
214 |
215 | private SigProfile(final String name)
216 | {
217 | setName(name);
218 | this.accessKeyId = null;
219 | this.credentialProviders = new HashMap<>();
220 | this.credentialProvidersPriority = new HashMap<>();
221 | this.region = "";
222 | this.service = "";
223 | }
224 |
225 | public static String getDefaultRegion()
226 | {
227 | final String defaultRegion = System.getenv("AWS_DEFAULT_REGION");
228 | return (defaultRegion == null) ? "" : defaultRegion;
229 | }
230 |
231 | public static SigProfile fromEnvironment()
232 | {
233 | final String envAccessKeyId = System.getenv("AWS_ACCESS_KEY_ID");
234 | if (envAccessKeyId != null) {
235 | final String envSecretKey = System.getenv("AWS_SECRET_ACCESS_KEY");
236 | if (envSecretKey != null) {
237 | SigProfile.Builder builder = new SigProfile.Builder("ENV")
238 | .withAccessKeyId(envAccessKeyId)
239 | .withRegion(getDefaultRegion());
240 | final String envSessionToken = System.getenv("AWS_SESSION_TOKEN");
241 | if (envSessionToken == null) {
242 | builder.withCredentialProvider(new SigStaticCredentialProvider(new SigStaticCredential(envAccessKeyId, envSecretKey)), DEFAULT_STATIC_PRIORITY);
243 | }
244 | else {
245 | builder.withCredentialProvider(new SigStaticCredentialProvider(
246 | new SigTemporaryCredential(envAccessKeyId, envSecretKey, envSessionToken, Instant.now().getEpochSecond() + 900)), DEFAULT_STATIC_PRIORITY);
247 | }
248 | return builder.build();
249 | }
250 | }
251 | return null;
252 | }
253 |
254 | // Extract profiles from text. Format should be one environment variable per line.
255 | // Formatted such that it can be pasted into your shell and recognized by the AWS CLI.
256 | public static SigProfile fromShellVars(final String text)
257 | {
258 | final var patterns = List.of(
259 | Pattern.compile(".*(?AWS_[A-Z_]+)[= ]\"?(?\"?[a-zA-Z0-9/+]+={0,2})\"?[ ]*")
260 | );
261 | String keyId = null;
262 | String keySecret = null;
263 | String keySession = null;
264 | String region = null;
265 | for (String line : text.split("[\r\n;]+")) {
266 | Optional matcher = patterns.stream().map(p -> p.matcher(line)).filter(Matcher::matches).findFirst();
267 | if (matcher.isPresent()) {
268 | final String value = matcher.get().group("value");
269 | switch (matcher.get().group("name")) {
270 | case "AWS_ACCESS_KEY_ID":
271 | keyId = value;
272 | break;
273 | case "AWS_SECRET_ACCESS_KEY":
274 | keySecret = value;
275 | break;
276 | case "AWS_SESSION_TOKEN":
277 | keySession = value;
278 | break;
279 | case "AWS_DEFAULT_REGION":
280 | region = value;
281 | break;
282 | }
283 | }
284 | }
285 |
286 | if (keyId != null && keySecret != null) {
287 | SigCredential credential;
288 | if (keySession != null) {
289 | credential = new SigTemporaryCredential(keyId, keySecret, keySession, Instant.now().getEpochSecond() + 86400);
290 | } else {
291 | credential = new SigStaticCredential(keyId, keySecret);
292 | }
293 | SigProfile.Builder builder = new SigProfile.Builder("env-"+keyId).withAccessKeyId(keyId);
294 | if (region != null) {
295 | builder.withRegion(region);
296 | }
297 | return builder.withCredentialProvider(new SigStaticCredentialProvider(credential), DEFAULT_STATIC_PRIORITY).build();
298 | }
299 | return null;
300 | }
301 |
302 | private static Path getCliConfigPath()
303 | {
304 | Path configPath;
305 | final String envFile = System.getenv("AWS_CONFIG_FILE");
306 | if (envFile != null && Files.exists(Paths.get(envFile))) {
307 | configPath = Paths.get(envFile);
308 | }
309 | else {
310 | configPath = Paths.get(System.getProperty("user.home"), ".aws", "config");
311 | }
312 | return configPath;
313 | }
314 |
315 | public static List fromCLIConfig() {
316 | List profileList = new ArrayList<>();
317 | SigAwsProfileCredentialProvider.getAvailableProfileNames().forEach(name -> {
318 | var awsProfileOption = ProfileFile.defaultProfileFile().profile(name);
319 | if (awsProfileOption.isPresent()) {
320 | final Profile awsProfile = awsProfileOption.get();
321 | Builder newProfileBuilder = new Builder(name)
322 | .withService("")
323 | .withCredentialProvider(new SigAwsProfileCredentialProvider(name), SigProfile.DEFAULT_AWS_PROFILE_PRIORITY);
324 | awsProfile.property("aws_access_key_id")
325 | .ifPresent(newProfileBuilder::withAccessKeyId);
326 | awsProfile.property("region")
327 | .ifPresent(newProfileBuilder::withRegion);
328 | profileList.add(newProfileBuilder.build());
329 | }
330 | });
331 | return profileList;
332 | }
333 |
334 | // refs: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html
335 | // https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-profiles.html
336 | //
337 | // Read profiles from an aws cli credential file. Additional properties may be read from the config
338 | // file where profile names must be specified with a "profile " prefix.
339 | public static List fromCredentialPath(final Path path)
340 | {
341 | // parse credential file
342 | List profileList = new ArrayList<>();
343 | Map> credentials = ConfigParser.parse(path);
344 |
345 | // get aws cli config file if it exists.
346 | Map> config = ConfigParser.parse(getCliConfigPath());
347 |
348 | // build profile list. settings in credentials file will take precedence over the config file.
349 | for (final String name : credentials.keySet()) {
350 | // combine profile settings from credential and config file into a single map. add credentials last
351 | // to overwrite duplicate settings from the config map. we want to prioritize values in the credential file
352 | Map section = new HashMap<>();
353 | section.putAll(config.getOrDefault("profile "+name, new HashMap<>()));
354 | section.putAll(credentials.getOrDefault(name, new HashMap<>()));
355 |
356 | if ((section.containsKey("aws_access_key_id") && section.containsKey("aws_secret_access_key")) || section.containsKey("source_profile")) {
357 | final String region = section.getOrDefault("region", "");
358 | String accessKeyId = section.getOrDefault("aws_access_key_id", null);
359 | String secretAccessKey = section.getOrDefault("aws_secret_access_key", null);
360 | String sessionToken = section.getOrDefault("aws_session_token", null);
361 |
362 | // if source_profile exists, check that profile for creds.
363 | if (section.containsKey("source_profile")) {
364 | final String source = section.get("source_profile");
365 | Map sourceSection = new HashMap<>();
366 | sourceSection.putAll(config.getOrDefault("profile "+source, new HashMap<>()));
367 | sourceSection.putAll(credentials.getOrDefault(source, new HashMap<>()));
368 | if (sourceSection.containsKey("aws_access_key_id") && sourceSection.containsKey("aws_secret_access_key")) {
369 | accessKeyId = sourceSection.get("aws_access_key_id");
370 | secretAccessKey = sourceSection.get("aws_secret_access_key");
371 | sessionToken = sourceSection.getOrDefault("aws_session_token", null);
372 | }
373 | else {
374 | logger.error(String.format("Profile [%s] refers to source_profile [%s] which does not contain credentials.", name, source));
375 | continue;
376 | }
377 | }
378 |
379 | SigProfile.Builder newProfileBuilder = new SigProfile.Builder(name)
380 | .withAccessKeyId(accessKeyId)
381 | .withRegion(region)
382 | .withService(""); // service is not specified in config files
383 | try {
384 | SigCredential staticCredential;
385 | if (sessionToken != null) {
386 | staticCredential = new SigTemporaryCredential(accessKeyId, secretAccessKey, sessionToken, 0);
387 | }
388 | else {
389 | staticCredential = new SigStaticCredential(accessKeyId, secretAccessKey);
390 | }
391 | newProfileBuilder.withCredentialProvider(new SigStaticCredentialProvider(staticCredential), DEFAULT_STATIC_PRIORITY);
392 | final String roleArn = section.getOrDefault("role_arn", null);
393 | if (roleArn != null) {
394 | SigAssumeRoleCredentialProvider assumeRole = new SigAssumeRoleCredentialProvider.Builder(roleArn, staticCredential)
395 | .tryRoleSessionName(section.getOrDefault("role_session_name", null))
396 | .withDurationSeconds(Integer.parseInt(section.getOrDefault("duration_seconds","0")))
397 | .tryExternalId(section.getOrDefault("external_id", null))
398 | .build();
399 | newProfileBuilder.withCredentialProvider(assumeRole, DEFAULT_ASSUMEROLE_PRIORITY);
400 | }
401 | profileList.add(newProfileBuilder.build());
402 | } catch (IllegalArgumentException exc) {
403 | logger.error(String.format("Failed to import profile [%s] from path %s: %s", name, path, exc.getMessage()));
404 | }
405 | }
406 | }
407 | return profileList;
408 | }
409 |
410 | private String formatLine(final String fmt, final Object ... params) {
411 | return String.format(fmt + System.lineSeparator(), params);
412 | }
413 |
414 | private String getExportString()
415 | {
416 | String export = "";
417 | SigCredentialProvider provider = getStaticCredentialProvider();
418 | if (provider != null) {
419 | export += formatLine("[%s]", this.name);
420 | try {
421 | export += provider.getCredential().getExportString();
422 | } catch (SigCredentialProviderException exc) {
423 | logger.error("Failed to export credential: "+export);
424 | return "";
425 | }
426 | if (this.region != null && regionPattern.matcher(this.region).matches()) {
427 | export += formatLine("region = %s", this.region);
428 | }
429 |
430 | SigAssumeRoleCredentialProvider assumeRole = getAssumeRole();
431 | if (assumeRole != null) {
432 | final String roleArn = assumeRole.getRoleArn();
433 | if (roleArn != null) {
434 | export += formatLine("role_arn = %s", roleArn);
435 |
436 | final String sessionName = assumeRole.getSessionName();
437 | if (sessionName != null) {
438 | export += formatLine("role_session_name = %s", sessionName);
439 | }
440 |
441 | final String externalId = assumeRole.getExternalId();
442 | if (externalId != null) {
443 | export += formatLine("external_id = %s", externalId);
444 | }
445 |
446 | export += formatLine("duration_seconds = %d", assumeRole.getDurationSeconds());
447 | // specify that creds for calling sts:AssumeRole are in the same profile
448 | export += formatLine("source_profile = %s", this.name);
449 | }
450 | }
451 | }
452 | return export;
453 | }
454 |
455 | public static int exportToFilePath(final List sigProfiles, final Path exportPath)
456 | {
457 | List exportLines = new ArrayList<>();
458 | for (final SigProfile profile : sigProfiles) {
459 | final String export = profile.getExportString();
460 | if (!export.equals("")) {
461 | exportLines.add(export);
462 | }
463 | }
464 | if (exportLines.size() > 0) {
465 | try {
466 | Files.write(exportPath, exportLines, StandardOpenOption.TRUNCATE_EXISTING, StandardOpenOption.WRITE, StandardOpenOption.CREATE);
467 | }
468 | catch (IOException exc) {
469 | exportLines.clear();
470 | }
471 | }
472 | return exportLines.size();
473 | }
474 |
475 | public SigCredentialProvider getActiveProvider()
476 | {
477 | // remove providers that are disabled (priority -1) and then sort remaining to find highest priority provider
478 | return credentialProviders
479 | .values()
480 | .stream()
481 | .filter(p -> credentialProvidersPriority.get(p.getName()) >= 0)
482 | .min((a, b) -> {
483 | final int ap = credentialProvidersPriority.get(a.getName());
484 | final int bp = credentialProvidersPriority.get(b.getName());
485 | return Integer.compare(ap, bp);
486 | })
487 | .orElse(null);
488 | }
489 |
490 | public SigCredential getCredential() throws SigCredentialProviderException
491 | {
492 | final SigCredentialProvider provider = getActiveProvider();
493 | if (provider == null) {
494 | // this should never occur since a profile can't be created without a provider
495 | throw new SigCredentialProviderException("No active credential provider for profile: " + getName());
496 | }
497 |
498 | return provider.getCredential();
499 | }
500 |
501 | @Override
502 | public String toString() {
503 | return String.format("name = '%s', keyId = '%s', region = '%s', service = '%s'", name, accessKeyId, region, service);
504 | }
505 | }
506 |
--------------------------------------------------------------------------------