简体   繁体   中英

How to create Password Digest for use with Web Service?

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.

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. eg

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");

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=]

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);
    }
}

}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM