View Javadoc
1   /**
2    * Portions of this software was developed by employees of the National Institute
3    * of Standards and Technology (NIST), an agency of the Federal Government and is
4    * being made available as a public service. Pursuant to title 17 United States
5    * Code Section 105, works of NIST employees are not subject to copyright
6    * protection in the United States. This software may be subject to foreign
7    * copyright. Permission in the United States and in foreign countries, to the
8    * extent that NIST may hold copyright, to use, copy, modify, create derivative
9    * works, and distribute this software and its documentation without fee is hereby
10   * granted on a non-exclusive basis, provided that this notice and disclaimer
11   * of warranty appears in all copies.
12   *
13   * THE SOFTWARE IS PROVIDED 'AS IS' WITHOUT ANY WARRANTY OF ANY KIND, EITHER
14   * EXPRESSED, IMPLIED, OR STATUTORY, INCLUDING, BUT NOT LIMITED TO, ANY WARRANTY
15   * THAT THE SOFTWARE WILL CONFORM TO SPECIFICATIONS, ANY IMPLIED WARRANTIES OF
16   * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND FREEDOM FROM
17   * INFRINGEMENT, AND ANY WARRANTY THAT THE DOCUMENTATION WILL CONFORM TO THE
18   * SOFTWARE, OR ANY WARRANTY THAT THE SOFTWARE WILL BE ERROR FREE.  IN NO EVENT
19   * SHALL NIST BE LIABLE FOR ANY DAMAGES, INCLUDING, BUT NOT LIMITED TO, DIRECT,
20   * INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES, ARISING OUT OF, RESULTING FROM,
21   * OR IN ANY WAY CONNECTED WITH THIS SOFTWARE, WHETHER OR NOT BASED UPON WARRANTY,
22   * CONTRACT, TORT, OR OTHERWISE, WHETHER OR NOT INJURY WAS SUSTAINED BY PERSONS OR
23   * PROPERTY OR OTHERWISE, AND WHETHER OR NOT LOSS WAS SUSTAINED FROM, OR AROSE OUT
24   * OF THE RESULTS OF, OR USE OF, THE SOFTWARE OR SERVICES PROVIDED HEREUNDER.
25   */
26  
27  package gov.nist.secauto.swid.client.totp;
28  
29  import java.nio.ByteBuffer;
30  import java.security.InvalidKeyException;
31  import java.security.NoSuchAlgorithmException;
32  
33  import javax.crypto.Mac;
34  import javax.crypto.spec.SecretKeySpec;
35  
36  /**
37   * A direct implementation of https://tools.ietf.org/html/rfc4226 and
38   * https://tools.ietf.org/html/rfc6238 C = counter K = key Digits = length of OTP Note that a strict
39   * implementation of RFC 4226 will only support HMAC-SHA-1.
40   */
41  public class Hotp {
42  
43    public static enum HashAlgorithm {
44      SHA1("HMACSHA1"),
45      SHA256("HMACSHA256"),
46      SHA512("HMACSHA512");
47  
48      private String name;
49  
50      private HashAlgorithm(String name) {
51        this.name = name;
52      }
53  
54      public String getAlgorithmName() {
55        return name;
56      }
57    }
58  
59    private static class Digits {
60      private long value;
61      private int length;
62  
63      /**
64       * @param length
65       *          the number of digits in the OTP
66       */
67      private Digits(int length) {
68        if (length < 6 && length > 9) {
69          // 6 is the minimum in the RFC and 19 is the maximum that a long
70          // can represent
71          // 9 is the maximum number of digits supported in the RFC
72          throw new IllegalArgumentException("the length must be between 6 and 9 digits");
73        }
74        this.length = length;
75        StringBuilder builder = new StringBuilder("1");
76        for (int i = 0; i < length; i++) {
77          builder.append('0');
78        }
79        value = Long.valueOf(builder.toString());
80      }
81  
82      public int length() {
83        return length;
84      }
85  
86      public long mod(long input) {
87        return input % value;
88      }
89    }
90  
91    /** the hash algorithm */
92    private HashAlgorithm algorithm;
93    /** the number of digits */
94    private Digits digits;
95  
96    /**
97     * Creates a new instance
98     * 
99     * @param algorithm
100    *          the algorithm to use
101    * @param length
102    *          the number of digits
103    */
104   public Hotp(HashAlgorithm algorithm, int length) {
105     this.algorithm = algorithm;
106     this.digits = new Digits(length);
107   }
108 
109   /**
110    * Generates an HOTP value according to RFC 4226
111    * 
112    * @param key
113    *          the shared secret ('K')
114    * @param counter
115    *          the counter ('C') value
116    * @return the HOTP value
117    */
118   public String generate(byte[] key, long counter) {
119     try {
120       byte[] hs = hashString(key, counter);
121       long hotp = truncate(hs);
122       return pad(Long.toString(hotp), this.digits.length());
123     } catch (InvalidKeyException e) {
124       throw new RuntimeException("configuration error, invalid key", e);
125     } catch (NoSuchAlgorithmException e) {
126       throw new RuntimeException("configuration error, missing algorithm support", e);
127     }
128   }
129 
130   /**
131    * A helper function for padding a String up to a minimum number of characters
132    * 
133    * @param s
134    *          the string to pad
135    * @param minLength
136    *          the minimum
137    * @return the string or a string padded up to the minimum length
138    */
139   private String pad(String s, int minLength) {
140     int padding = minLength - s.length();
141     if (padding <= 0) {
142       // nothing to do
143       return s;
144     }
145     StringBuilder builder = new StringBuilder();
146     for (int i = 0; i < padding; i++) {
147       builder.append("0");
148     }
149     builder.append(s);
150     return builder.toString();
151   }
152 
153   /**
154    * Performs dynamic truncation as described in section 5.3
155    * 
156    * @param hash
157    *          the input hash
158    * @return the truncated value
159    */
160   private long truncate(byte[] hash) {
161     // put selected bytes into result int
162     int offset = hash[hash.length - 1] & 0xf;
163     int binary = ((hash[offset] & 0x7f) << 24) | ((hash[offset + 1] & 0xff) << 16) | ((hash[offset + 2] & 0xff) << 8)
164         | (hash[offset + 3] & 0xff);
165     return digits.mod(binary);
166   }
167 
168   /**
169    * Generates HS as described in step 1. of section 5.3
170    * 
171    * @param key
172    *          the secret Key or 'K' value
173    * @param counter
174    *          the counter or 'C' value
175    * @return hash string
176    * @throws NoSuchAlgorithmException
177    * @throws InvalidKeyException
178    */
179   private byte[] hashString(byte[] key, long counter) throws NoSuchAlgorithmException, InvalidKeyException {
180     Mac mac = Mac.getInstance(algorithm.getAlgorithmName());
181     SecretKeySpec macKey = new SecretKeySpec(key, algorithm.getAlgorithmName());
182     ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES);
183     buffer.putLong(counter);
184     mac.init(macKey);
185     return mac.doFinal(buffer.array());
186   }
187 
188 }