0

I am working the Apache Cloudstack API. I am generally working with the org.apache.commons.codec.binary.Base64 base64 encoder. In generating the command I use the following code (from the example given):

private String generateUrl(String commands) throws Exception {
        //Signature: This is the hashed signature of the Base URL that is generated using a combination of the user’s Secret Key and the HMAC SHA-1 hashing algorithm.

        /*
        1. For each field-value pair (as separated by a '&') in the Command String, URL encode each value so that it can be safely sent via HTTP GET.
        2. Lower case the entire Command String and sort it alphabetically via the field for each field-value pair. The result of this step would look like the following.
        3. Take the sorted Command String and run it through the HMAC SHA-1 hashing algorithm (most programming languages offer a utility method to do this) with the user’s Secret Key. Base64 encode the resulting byte array in UTF-8 so that it can be safely transmitted via HTTP. The final string produced after Base64 encoding should be "Lxx1DM40AjcXU%2FcaiK8RAP0O1hU%3D".
           By reconstructing the final URL in the format Base URL+API Path+Command String+Signature, the final URL should look like:
        */

        commands += "&id=" + apiReferenceId;
        commands += "&response=json";

        // Step 1: Make sure your APIKey is toLowerCased and URL encoded
        String encodedApiKey = URLEncoder.encode(apiKey.toLowerCase(), "UTF-8");

        // Step 2: toLowerCase all the parameters, URL encode each parameter value, and the sort the parameters in alphabetical order
        // Please note that if any parameters with a '&' as a value will cause this test client to fail since we are using '&' to delimit
        // the string

        List<String> sortedParams = new ArrayList<String>();
        sortedParams.add("apikey="+encodedApiKey);
        StringTokenizer st = new StringTokenizer(commands, "&");
        while (st.hasMoreTokens()) {
            String paramValue = st.nextToken().toLowerCase();
            String param = paramValue.substring(0, paramValue.indexOf("="));
            String value = URLEncoder.encode(paramValue.substring(paramValue.indexOf("=")+1, paramValue.length()), "UTF-8");
            sortedParams.add(param + "=" + value);
        }
        Collections.sort(sortedParams);
        System.out.println("Sorted Parameters: " + sortedParams);

        // Step 3: Construct the sorted URL and sign and URL encode the sorted URL with your secret key
        String sortedUrl = null;
        boolean first = true;
        for (String param : sortedParams) {
            if (first) {
                sortedUrl = param;
                first = false;
            } else {
                sortedUrl = sortedUrl + "&" + param;
            }
        }
        Logger.debug("sorted URL : " + sortedUrl);

        Mac mac = Mac.getInstance("HmacSHA1");
        SecretKeySpec keySpec = new SecretKeySpec(secretKey.getBytes(), "HmacSHA1");
        mac.init(keySpec);
        mac.update(sortedUrl.getBytes());
        byte[] encryptedBytes = mac.doFinal();
        String encodedSignature = URLEncoder.encode(Base64.encodeBase64String(encryptedBytes), "UTF-8");

        // Step 4: Construct the final URL we want to send to the CloudStack Management Server
        // Final result should look like:
        // http(s)://://client/api?&apiKey=&signature=

        String finalUrl = apiUrl + "?" + commands + "&apiKey=" + apiKey + "&signature=" + encodedSignature;
        return finalUrl;
}

However, this generates a 401 error.

<?xml version="1.0" encoding="UTF-8"?><stopvirtualmachineresponse cloud-stack-version="4.2.1"><errorcode>401</errorcode><errortext>unable to verify user credentials and/or request signature</errortext></stopvirtualmachineresponse>

When I use:

String encodedSignature = URLEncoder.encode(org.postgresql.util.Base64.encodeBytes(encryptedBytes), "UTF-8");

it works fine. Is there a way I can standardise on the Apache Commons, or is there a specific reason it fails when I use that library?

Luuk D. Jansen
  • 4,402
  • 8
  • 47
  • 90

1 Answers1

0

A guess. I do see secretKey.getBytes() and other getBytes(). Assuming that secretKey is a String, it should be:

secretKey.getBytes(StandardCharsets.UTF_8)

As the default charset is that of the current operating system. And you probably are running on different machines, Windows for development, Linux for server.

The only other variation lies in line wrapping and terminating fillers (dashes). Apache commons codecs Base64 has some control parameters for that.

By the way there are some other (non-Sun!) Base64 APIs at several spots in JavaEE, though apache commons is a good choice. Example: javax.xml.bind.DatatypeConverter.parseBase64Binary.

Joop Eggen
  • 107,315
  • 7
  • 83
  • 138
  • I tried using String encodedSignature = URLEncoder.encode(new Base64(1024).encodeToString(encryptedBytes), "UTF-8"); but that too failed. I have a feeling it might be with adding line endings or so, as that would change the total hash, but can't really put my finger on it. – Luuk D. Jansen Mar 03 '14 at 20:35
  • Indeed. Though one might try `secretKey.getBytes("Cp1252")` too (Winows extended Latin-1). Good luck. – Joop Eggen Mar 03 '14 at 21:21