How to create Password Digest for use with Web Service?
Asked Answered
S

3

8

I am trying to create a passwordDigest util which can be used in different environments which are capable of running java byte code.

First of all I create nonce. It is done like so.

public static String buildNonce(){
        StringBuffer nonce=new StringBuffer();
        String dateTimeString = Long.toString(new Date().getTime());
        byte[] nonceByte= dateTimeString.getBytes();
        return Base64.encode(nonceByte);
    }

Once I have nonce, I build password digest.

public static String buildPasswordDigest(String userName, String password, String nonce, String dateTime){
    MessageDigest sha1;
    String passwordDigest=null;
    try {
        sha1= MessageDigest.getInstance("SHA-1");
        byte[] hash = MessageDigest.getInstance("SHA-1").digest(password.getBytes("UTF-8"));
        sha1.update(nonce.getBytes("UTF-8"));
        sha1.update(dateTime.getBytes("UTF-8"));
        passwordDigest = new String(Base64.encode(sha1.digest(hash)));
        sha1.reset();
    } catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
        // TODO Auto-generated catch block
        e.printStackTrace();
    }
    return passwordDigest;

In order to test that everything works correctly. I have created a test web service using CXF 2.7. I have manually created SOAP Envelope to test authentication. The envelope looks like this.

 <soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" 
                xmlns:ws="http://ws.mytest.org/" 
                xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance/">
    <soapenv:Header>
        <wsse:Security soapenv:mustUnderstand="1" 
          xmlns:wsse="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd" 
          xmlns:wsu="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd">
          <wsse:UsernameToken wsu:Id="UsernameToken-2">
            <wsse:Username>TEST_USER</wsse:Username>
            <wsse:Password 
            Type="http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest">UZsDSW/vANu6fHg4rAHo2OwsF9s=</wsse:Password
            <wsse:Nonce>MTQwMTMwMDQzNjA3OA==</wsse:Nonce>
            <wsu:Created>2014-05-28T18:07:16.087Z</wsu:Created>
         </wsse:UsernameToken>
        </wsse:Security>
    </soapenv:Header>
    <soapenv:Body>
        <ws:record>
            <val1>1</val1>
            <val2>Some Text</val2>
        </ws:record>
    </soapenv:Body>
</soapenv:Envelope>

When I send the envelope using SOAP UI. I get the following authentication error.

WARNING: Interceptor for {http://ws.mytest.org/}TestService has thrown exception, unwinding now
org.apache.cxf.binding.soap.SoapFault: The security token could not be authenticated or authorized
    at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.createSoapFault(WSS4JInInterceptor.java:788)
    at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage(WSS4JInInterceptor.java:336)
    at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage(WSS4JInInterceptor.java:95)
    at org.apache.cxf.phase.PhaseInterceptorChain.doIntercept(PhaseInterceptorChain.java:272)
    at org.apache.cxf.transport.ChainInitiationObserver.onMessage(ChainInitiationObserver.java:121)
    at org.apache.cxf.transport.http.AbstractHTTPDestination.invoke(AbstractHTTPDestination.java:239)
    at org.apache.cxf.transport.servlet.ServletController.invokeDestination(ServletController.java:248)
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:222)
    at org.apache.cxf.transport.servlet.ServletController.invoke(ServletController.java:153)
    at org.apache.cxf.transport.servlet.CXFNonSpringServlet.invoke(CXFNonSpringServlet.java:167)
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.handleRequest(AbstractHTTPServlet.java:286)
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.doPost(AbstractHTTPServlet.java:206)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:646)
    at org.apache.cxf.transport.servlet.AbstractHTTPServlet.service(AbstractHTTPServlet.java:262)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:303)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.tomcat.websocket.server.WsFilter.doFilter(WsFilter.java:52)
    at org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:241)
    at org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:208)
    at org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:220)
    at org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:122)
    at org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:501)
    at org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:170)
    at org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:98)
    at org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:950)
    at org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:116)
    at org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:408)
    at org.apache.coyote.http11.AbstractHttp11Processor.process(AbstractHttp11Processor.java:1040)
    at org.apache.coyote.AbstractProtocol$AbstractConnectionHandler.process(AbstractProtocol.java:607)
    at org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:313)
    at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)
    at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)
    at java.lang.Thread.run(Thread.java:744)
Caused by: org.apache.ws.security.WSSecurityException: The security token could not be authenticated or authorized
    at org.apache.ws.security.validate.UsernameTokenValidator.verifyDigestPassword(UsernameTokenValidator.java:199)
    at org.apache.ws.security.validate.UsernameTokenValidator.validate(UsernameTokenValidator.java:97)
    at org.apache.ws.security.processor.UsernameTokenProcessor.handleUsernameToken(UsernameTokenProcessor.java:172)
    at org.apache.ws.security.processor.UsernameTokenProcessor.handleToken(UsernameTokenProcessor.java:67)
    at org.apache.ws.security.WSSecurityEngine.processSecurityHeader(WSSecurityEngine.java:396)
    at org.apache.cxf.ws.security.wss4j.WSS4JInInterceptor.handleMessage(WSS4JInInterceptor.java:279)
    ... 31 more

I suspect that I have an issue creating either nonce or password.

Your help is appreciated.

Sumy answered 28/5, 2014 at 18:54 Comment(0)
H
9

WS-Security defines password digest as

Base64 ( SHA1 ( nonce + created + password ) )

not

Base64 ( SHA1 ( password + nonce + created) )

And nonce is supposed to be 128 bits (16 bytes) encoded as Base64. e.g.

java.security.SecureRandom random = java.security.SecureRandom.getInstance("SHA1PRNG");
random.setSeed(System.currentTimeMillis()); 
byte[] nonceBytes = new byte[16]; 
random.nextBytes(nonceBytes); 
String nonce = new String(org.apache.commons.codec.binary.Base64.encodeBase64(nonceBytes), "UTF-8");
Hintz answered 21/1, 2015 at 7:32 Comment(3)
I generated the Username token using SOAP UI. Now trying to generate digest using above formula (by using online tools for Base64 encoding and SHA1 hashing. is + sign above indicates String concatenation ?Devland
The spec at oasis-open.org/committees/download.php/13392/… says that the nonce included in the concatenation is not encoded as base64: "Note that the nonce is hashed using the octet sequence of its decoded value while the timestamp is hashed using the octet sequence of its UTF8 encoding as specified in the contents of the element."Levina
I am also facing the same issue.. But I am not sure on where is this WS-Security digest is defined?Pratique
A
3

The nonce and the digest of the password should be constructed, as it done in the snippet below.

Please, note the order of the elements, and the fact that the non-encoded version of the nonce is used for passwordDigest construction.

import javax.xml.datatype.*;
import java.security.*;
import java.time.Instant;
import java.util.Base64;

import static java.lang.System.currentTimeMillis;
import static java.nio.charset.StandardCharsets.UTF_8;

class Snippet {

    private static final SecureRandom RANDOM;
    private static final int NONCE_SIZE_IN_BYTES = 16;
    private static final String MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1 = "SHA-1";
    private static final String SECURE_RANDOM_ALGORITHM_SHA_1_PRNG = "SHA1PRNG";

    static {
        try {
            RANDOM = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM_SHA_1_PRNG);
            RANDOM.setSeed(currentTimeMillis());
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public static void main(String[] args) throws DatatypeConfigurationException {
        final var nonceBytes = generateNonce();

        final var password = "password";
        final var createdDate = DatatypeFactory.newInstance().newXMLGregorianCalendar(Instant.now().toString());
        final var passwordDigestBytes = constructPasswordDigest(nonceBytes, createdDate, password);

        final var base64Encoder = Base64.getEncoder();
        final var nonceBase64Encoded = base64Encoder.encodeToString(nonceBytes);
        final var passwordDigestBase64Encoded = base64Encoder.encodeToString(passwordDigestBytes);
        System.out.println(String.format("nonce: [%s], password digest: [%s]", nonceBase64Encoded, passwordDigestBase64Encoded));
        System.out.flush();
    }

    private static byte[] generateNonce() {
        var nonceBytes = new byte[NONCE_SIZE_IN_BYTES];
        RANDOM.nextBytes(nonceBytes);
        return nonceBytes;
    }

    /**
     * @noinspection SameParameterValue
     */
    private static byte[] constructPasswordDigest(byte[] nonceBytes, XMLGregorianCalendar createdDate, String password) {
        try {
            final var sha1MessageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1);
            sha1MessageDigest.update(nonceBytes);
            final var createdDateAsString = createdDate.toString();
            sha1MessageDigest.update(createdDateAsString.getBytes(UTF_8));
            sha1MessageDigest.update(password.getBytes(UTF_8));
            return sha1MessageDigest.digest();
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
}

The output of this snippet is:

nonce: [KEyJbsmxL1JdsDHo7kWD6Q==], password digest: [+OiJDs2sEycZECHhQdJ8T9Lt2ns=]

Allocate answered 27/12, 2019 at 12:55 Comment(1)
Thanks for this. The accepted answer suggests the nonce should be included in the hash in its base64-encoded form. This is not correct. This answer correctly shows the nonce included as a byte[]. This documentation agrees: oasis-open.org/committees/download.php/13392/…Rikki
S
2

After days and days of searching, Pavel's answer was the only one that worked for me. The only changes I made to his code are 1) Returning the date of token creation, among with nonce and password digest. This is critical to avoid the "expired token" response. 2) returning the whole header block 3) converting the code to Java 1.8.

Here's the modified Pavel's code.

package test;

import javax.xml.datatype.*;
import java.security.*;
import java.time.Instant;
import java.util.Base64;
import java.util.Base64.Encoder;

import static java.lang.System.currentTimeMillis;
import static java.nio.charset.StandardCharsets.UTF_8;

public class Main {

private static final SecureRandom RANDOM;
private static final int NONCE_SIZE_IN_BYTES = 16;
private static final String MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1 = "SHA-1";
private static final String SECURE_RANDOM_ALGORITHM_SHA_1_PRNG = "SHA1PRNG";

static {
    try {
        RANDOM = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM_SHA_1_PRNG);
        RANDOM.setSeed(currentTimeMillis());
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

public static void main(String[] args) throws DatatypeConfigurationException {
    
    String passwordMd5 = "4C2d85FBEE80145B4";

    generateHeader(passwordMd5);

}

public static void generateHeader(String password) throws DatatypeConfigurationException {

    final byte[] nonceBytes = generateNonce();

    final XMLGregorianCalendar createdDate = DatatypeFactory.newInstance()
            .newXMLGregorianCalendar(Instant.now().toString());
    final byte[] passwordDigestBytes = constructPasswordDigest(nonceBytes, createdDate, password);

    final Encoder base64Encoder = Base64.getEncoder();
    final String nonceBase64Encoded = base64Encoder.encodeToString(nonceBytes);
    final String passwordDigestBase64Encoded = base64Encoder.encodeToString(passwordDigestBytes);
    
    //System.out.println(String.format("nonce: [%s], password digest: [%s]", nonceBase64Encoded, passwordDigestBase64Encoded));

    StringBuilder sb = new StringBuilder();

    sb.append("<soapenv:Header>\n");
    sb.append("<wsse:Security xmlns:wsse=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-secext-1.0.xsd\">\n");
    
    sb.append("<wsse:UsernameToken>");
    sb.append("<wsse:Username>thisIsMyUsername</wsse:Username>");
    
    sb.append("<wsse:Password Type=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-username-token-profile-1.0#PasswordDigest\">"+
    passwordDigestBase64Encoded+"</wsse:Password>\n");
    
    sb.append("<wsse:Nonce EncodingType=\"http://docs.oasis-open.org/wss/2004 /01/oasis-200401-wss-soap-message-security-1.0#Base64Binary\">" +
    nonceBase64Encoded+"</wsse:Nonce>\n");
    
    sb.append("<wsu:Created xmlns:wsu=\"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd\">" +
    createdDate.toString()+"</wsu:Created>\n");
    
    sb.append("</wsse:UsernameToken>\n");
    sb.append("</wsse:Security>\n");
    sb.append("</soapenv:Header>");

    
    System.out.println(sb.toString());
    
    System.out.flush();
    
    System.exit(0);
}

private static byte[] generateNonce() {
    byte[] nonceBytes = new byte[NONCE_SIZE_IN_BYTES];
    RANDOM.nextBytes(nonceBytes);
    return nonceBytes;
}

private static byte[] constructPasswordDigest(byte[] nonceBytes, XMLGregorianCalendar createdDate,
        String password) {
    try {
        final MessageDigest sha1MessageDigest = MessageDigest.getInstance(MESSAGE_DIGEST_ALGORITHM_NAME_SHA_1);
        sha1MessageDigest.update(nonceBytes);
        final String createdDateAsString = createdDate.toString();
    

        sha1MessageDigest.update(createdDateAsString.getBytes(UTF_8));
        sha1MessageDigest.update(password.getBytes(UTF_8));
        return sha1MessageDigest.digest();
    } catch (NoSuchAlgorithmException e) {
        throw new RuntimeException(e);
    }
}

}
Sackett answered 18/11, 2021 at 6:48 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.