View Javadoc

1   package org.astrogrid.security;
2   
3   import java.security.MessageDigest;
4   import java.security.SecureRandom;
5   import java.util.Date;
6   import javax.xml.soap.SOAPException;
7   
8   /***
9    * A password with associated nonce and timestamp, as specified in
10   * WS-Security. This class supports the password hashing and encoding
11   * algorithms of WS-Security.
12   *
13   * @author Guy Rixon
14   */
15  public class PasswordEncoding {
16  
17    /***
18     * The password, before hashing.
19     */
20    private String plainPassword;
21  
22    /***
23     * The password, after hashing and base-64 encoding.
24     */
25    private String encodedPassword;
26  
27    /***
28     * The nonce, unencoded and unhashed.
29     */
30    protected byte[] nonce;
31  
32    /***
33     * The timestamp as an ISO8601 string.
34     */
35    protected String timestamp;
36  
37    /***
38     * A formatter for using ISO8601 timestamps.
39     */
40    private static Iso8601DateFormat iso8601 = new Iso8601DateFormat();
41  
42  
43    /***
44     * Constructs a Password. The associated nonce is generated
45     * and stored internally. The timestamp is taken and stored
46     * internally.
47     *
48     * @param password the plain-text password
49     */
50    public PasswordEncoding (String password) throws SOAPException {
51      try {
52        this.plainPassword = password;
53        SecureRandom prng = SecureRandom.getInstance("SHA1PRNG");
54        this.nonce = new byte[16];
55        prng.nextBytes(nonce);
56        this.timestamp = iso8601.format(new Date());
57      }
58      catch (Exception e) {
59        throw new SOAPException("Failed to construct a Password", e);
60      }
61    }
62  
63  
64    /***
65     * Constructs a password. The given nonce, timestamp and
66     * encoded password are stored internally; no new nonces
67     * or timestamps are generated.
68     */
69    public PasswordEncoding (String password,
70                             String nonce,
71                             String timestamp) throws SOAPException {
72      try {
73        this.nonce = Base64.decode(nonce);
74        this.timestamp = timestamp;
75        this.encodedPassword = password;
76      }
77      catch (Exception e) {
78        throw new SOAPException("Failed to construct a password", e);
79      }
80    }
81  
82  
83    /***
84     * Returns the nonce in a form ready to serialize as XML.
85     *
86     * @return the nonce, in base-64 encoding
87     */
88    public String getNonce () {
89      return Base64.encodeBytes(this.nonce);
90    }
91  
92  
93    /***
94     * Returns the timestamp, in a form ready to serialize as XML.
95     *
96     * @return the timestamp in ISO8601 format with no encoding.
97     */
98    public String getTimestamp () {
99      return this.timestamp;
100   }
101 
102 
103   /***
104    * Hash, encode and return the password. The algorithm combines the
105    * nonce, timestamp and clear-text password as prescribed by
106    * WS-Security.  See the class comments for details.
107    *
108    * @return the encoded password
109    * @throws SOAPException if anything goes wrong
110    */
111   public String getEncodedPassword () throws SOAPException {
112     try {
113       return this.encodePassword(this.plainPassword,
114                                  this.nonce,
115                                  this.timestamp);
116     }
117     catch (Exception e) {
118       throw new SOAPException("Failed to encoded a password", e);
119     }
120   }
121 
122 
123   /***
124    * Tests a plain-text password against the cached password.
125    * The given password is hashed and encoded and then tested
126    * against the encoded password set at construction.
127    */
128   public boolean match (String plainPassword) throws SOAPException {
129     try {
130       String candidate = this.encodePassword(plainPassword,
131                                              this.nonce,
132                                              this.timestamp);
133       return (candidate.equals(this.encodedPassword));
134     }
135     catch (Exception e) {
136       throw new SOAPException("Failed to encode a password", e);
137     }
138   }
139 
140 
141   /***
142    * Applies the hashing and encoding algorithms.
143    */
144   private String encodePassword(String plainPassword,
145                                 byte[] nonce,
146                                 String timestamp) throws Exception {
147 
148     // Concatenate nonce, timestamp and password.
149     // This has to be done with byte arrays since the nonce
150     // is not character data.
151     byte[] timestampBytes = timestamp.getBytes("UTF8");
152     byte[] passwordBytes  = plainPassword.getBytes("UTF8");
153     int totalLength = nonce.length
154                     + timestampBytes.length
155                     + passwordBytes.length;
156     byte[] ensemble = new byte[totalLength];
157     int i = 0;
158     for (int j = 0; j < nonce.length; j++) {
159       ensemble[i] = this.nonce[j];
160       i++;
161     }
162     for (int k = 0; k < timestampBytes.length; k++) {
163       ensemble[i] = timestampBytes[k];
164       i++;
165     }
166     for (int l = 0; l < passwordBytes.length; l++) {
167       ensemble[i] = passwordBytes[l];
168       i++;
169     }
170     System.out.println("timestampBytes: " + new String(timestampBytes));
171     System.out.println("passwordBytes:  " + new String(passwordBytes));
172     System.out.println("nonce:          " + new String(nonce));
173     System.out.println("ensemble:       " + new String(ensemble));
174 
175     // Hash the assembled byte array.
176     MessageDigest md = MessageDigest.getInstance("SHA");
177     md.update(ensemble);
178     byte[] digest = md.digest();
179     System.out.println("digest:         " + new String(digest));
180 
181     // Encode the bytes and return them as a string.
182     return Base64.encodeBytes(digest);
183   }
184 
185 }