View Javadoc

1   package com.atlassian.seraph.util;
2   
3   import org.apache.log4j.Logger;
4   
5   import javax.crypto.Cipher;
6   import javax.crypto.SecretKey;
7   import javax.crypto.SecretKeyFactory;
8   import javax.crypto.spec.PBEKeySpec;
9   import javax.crypto.spec.PBEParameterSpec;
10  import java.io.BufferedReader;
11  import java.io.IOException;
12  import java.io.InputStreamReader;
13  import java.security.GeneralSecurityException;
14  import java.security.NoSuchAlgorithmException;
15  import java.security.spec.InvalidKeySpecException;
16  
17  public class EncryptionUtils
18  {
19      private static final Logger log = Logger.getLogger(EncryptionUtils.class);
20  
21      // Salt
22      private static final byte[] SALT = {
23          (byte)0xc2, (byte)0x74, (byte)0x24, (byte)0x4c,
24          (byte)0x7c, (byte)0xd8, (byte)0xee, (byte)0x99
25      };
26  
27      private static final int ITERATIONS = 21;
28  
29      private PBEParameterSpec pbeParamSpec = new PBEParameterSpec(SALT, ITERATIONS);
30      private SecretKey pbeKey;
31  
32      public String encrypt(String data)
33      {
34          try
35          {
36              Cipher pbeCipher = createCipher(true);
37  
38              // Encrypt the cleartext
39              return encode(pbeCipher.doFinal(data.getBytes()));
40          } catch (Exception ex)
41          {
42              // Should never happen
43              throw new RuntimeException(ex);
44          }
45      }
46  
47      private Cipher createCipher(boolean encrypt) throws GeneralSecurityException
48      {
49          if (pbeKey == null)
50              throw new IllegalStateException("The password has not be set");
51  
52          // Create PBE Cipher
53          Cipher pbeCipher = Cipher.getInstance("PBEWithMD5AndDES");
54  
55          // Initialize PBE Cipher with key and parameters
56          pbeCipher.init((encrypt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE), pbeKey, pbeParamSpec);
57          return pbeCipher;
58      }
59  
60      public String decrypt(String encData)
61      {
62          try
63          {
64              Cipher pbeCipher = createCipher(false);
65              return new String(pbeCipher.doFinal(decode(encData)));
66          } catch (Exception e)
67          {
68              // Should not happen in production
69              throw new RuntimeException(e);
70          }
71      }
72  
73      /**
74       * Decodes a string into a series of bytes where the string has been encoded with a page-with-offset scheme and
75       * base-64 encoding algorithm as described by {@link #encode(byte[])}.
76       *
77       * Invalid page numbers are ignored, resulting in null (zero-valued) bytes in the output. Invalid offsets use the error
78       * handling of {@link #decodeOffset(char)}.
79       *
80       * @param encoded the string to decode
81       * @return the decoded bytes
82       */
83      static byte[] decode(String encoded)
84      {
85          int offsetsIndex = encoded.length() / 2;
86          char[] encodedPageNumbers = encoded.substring(0, offsetsIndex).toCharArray();
87          char[] encodedOffsets = encoded.substring(offsetsIndex).toCharArray();
88  
89          byte[] bytes = new byte[offsetsIndex];
90          for (int i = 0; i < bytes.length; i++)
91          {
92              int pageNumber = decodePageNumber(encodedPageNumbers[i]);
93              byte offset = decodeOffset(encodedOffsets[i]);
94              if (pageNumber < 0 || pageNumber > 3)
95              {
96                  log.debug("Invalid encoded page number '" + encodedPageNumbers[i] + "', decoded as " + pageNumber + ". Skipping byte.");
97                  continue;
98              }
99              bytes[i] = (byte) ((pageNumber * 64 - 128) + offset);
100         }
101         return bytes;
102     }
103 
104     /**
105      * Decode a character into a byte, based on an unusual base-64 encoding.
106      * <p/>
107      * The characters for digits 0-9 correspond with byte values 0-9, capital letters A-Z are 10-35, small letters A-Z are 36-61,
108      * open angle bracket (&lt;) is 62 and close angle bracket (&gt;) is 63. Characters which are not recognised are returned
109      * as byte value 2 (the same as a digit '2').
110      *
111      * @param encodedOffset the character to decode
112      * @return the decoded value of the character as a byte between 0 and 63.
113      * @see #encodeOffset(int) for the encoder
114      */
115     static byte decodeOffset(char encodedOffset)
116     {
117         if ('0' <= encodedOffset && encodedOffset <= '9')
118             return (byte) (encodedOffset - '0');
119         else if ('A' <= encodedOffset && encodedOffset <= 'Z')
120             return (byte) ((encodedOffset - 'A') + 10);
121         else if ('a' <= encodedOffset && encodedOffset <= 'z')
122             return (byte) ((encodedOffset - 'a') + 36);
123         else if (encodedOffset == '<')
124             return (byte) (62);
125         else if (encodedOffset == '>')
126             return (byte) (63);
127         else
128         {
129             log.debug("Cannot decode invalid encoded offset '" + encodedOffset + "'. Must be one of [A-Za-z0-9<>]. Returning default value '2'.");
130             return 2;
131         }
132     }
133 
134     /**
135      * Encodes a series of byte values in a string using a page-with-offset scheme and a base-64 encoding algorithm.
136      * <p/>
137      * First, the byte value is converted into a page number and offset relative to the starting value of the page.
138      * Page 0 contains values -128 to -65, page 1 has -64 to -1, page 2 has 0 to 63, and page 3 has 64 to 127.
139      * For example, the byte value 96 will be converted into page 3 and offset 32.
140      * <p/>
141      * The offset is encoded using the unusual base-64 encoding implemented by {@link #encodeOffset(int)}. The
142      * page is encoded using {@link #encodePageNumber(int)}.
143      * <p/>
144      * The output string consists of the characters for all encoded page numbers first, followed by the characters
145      * for all the encoded offsets.
146      *
147      * @param plainBytes the binary data to encode
148      * @return an encoded string representing the binary data, where the first half consists of encoded page numbers
149      * and the second half consists of encoded offsets
150      */
151     static String encode(byte[] plainBytes)
152     {
153         StringBuffer encodedPageNumbers = new StringBuffer(plainBytes.length);
154         StringBuffer encodedOffsets = new StringBuffer(plainBytes.length);
155         for (int i = 0; i < plainBytes.length; i++)
156         {
157             byte aByte = plainBytes[i];
158 
159             int pageNumber = ((int) aByte + 128) / 64;
160             encodedPageNumbers.append(encodePageNumber(pageNumber));
161             
162             int offset = ((int) aByte + 128) % 64;
163             encodedOffsets.append(encodeOffset(offset));
164         }
165         return encodedPageNumbers.toString() + encodedOffsets.toString();
166     }
167 
168     /**
169      * Generates a random character which encodes a page number between 0 and 3 inclusive. The character is derived by the following formula:
170      * <ol>
171      * <li>Multiply the page number by 6, add a random integer between 0 and 5.</li>
172      * <li>Convert the resulting value into a character, where a = 0, b = 1, etc.</li>
173      * <li>Select a random Boolean true/false value. Make the character upper-case if this random value is true.</li>
174      * </ol>
175      * For example, if the page number is 0, the resulting character will be between 'a' and 'f' inclusive, and could be either upper or lower case.
176      *
177      * @param pageNumber the page number to encode, which should be between 0 and 3 inclusive
178      * @return a random character between (pageNumber x 6) and (pageNumber x 6 + 5) where a = 0, b = 2, etc. and upper or lower case is randomly selected
179      */
180     static char encodePageNumber(int pageNumber)
181     {
182         int randomChar = (pageNumber * 6) + (int) (Math.random() * 6);
183         boolean upperCase = (int) (Math.random() * 2) < 1;
184         return (char) (randomChar + (int) (upperCase ? 'A' : 'a'));
185     }
186 
187     /**
188      * Decodes a page number generated by {@link #encodePageNumber(int)} by converting to lower case, and dividing the number
189      * value of the letter (where a = 0, b = 1, etc.) by 6.
190      *
191      * @param encodedPageNumber the encoded page number
192      * @return a page number which will be either 0, 1, 2 or 3 if the input is a properly encoded page number
193      */
194     static int decodePageNumber(char encodedPageNumber)
195     {
196         return ((int) Character.toLowerCase(encodedPageNumber) - 'a') / 6;
197     }
198 
199     /**
200      * Encode a base-64 offset into a character, using a quite unusual base-64 encoding.
201      * <p/>
202      * The characters for digits 0-9 are returned for values 0-9, capital letters A-Z represent 10-35, small letters a-z represent 36-61,
203      * open angle bracket (&lt;) is 62 and close angle bracket (&gt;) is 63. Values greater than 63 or less than zero return the null character
204      * (character 0).
205      *
206      * @param offset the offset to encode
207      * @return a character representing the offset in the encoding scheme described above
208      * @see #decodeOffset(char) for the decoder
209      */
210     static char encodeOffset(int offset)
211     {
212         if (offset < 0 || offset > 63)
213         {
214             log.debug("Invalid offset to encode: " + offset + ". Should be between 0 and 63. Returning null character.");
215             return (char) 0;
216         }
217 
218         if (offset < 10)
219             return (char) (offset + '0');
220 
221         if (offset < 36)
222             return (char) (offset - 10 + 'A');
223 
224         if (offset < 62)
225             return (char) (offset - 36 + 'a');
226 
227         if (offset < 63)
228             return '<';
229 
230         return '>';
231     }
232 
233     public void setPassword(String password)
234     {
235         PBEKeySpec pbeKeySpec = new PBEKeySpec(password.toCharArray());
236         try
237         {
238             SecretKeyFactory keyFac = SecretKeyFactory.getInstance("PBEWithMD5AndDES");
239             pbeKey = keyFac.generateSecret(pbeKeySpec);
240         } catch (NoSuchAlgorithmException e)
241         {
242             throw new RuntimeException("Encryption algorithm not found", e);
243         } catch (InvalidKeySpecException e)
244         {
245             throw new RuntimeException("Invalid passphrase", e);
246         }
247 
248 
249     }
250 
251     public static void main(String[] args) throws IOException
252     {
253         if (args.length == 0)
254         {
255             System.err.println("Usage: com.atlassian.payment.encrypted.EncryptionUtils [encrypt|decrypt]");
256             System.exit(1);
257         }
258 
259         EncryptionUtils utils = new EncryptionUtils();
260         BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
261         if ("encrypt".equals(args[0]))
262         {
263             System.out.println("Enter the text to encrypt:");
264             String text = in.readLine();
265             System.out.println("\n"+utils.encrypt(text)+"\n");
266         }
267         else
268         {
269             System.out.println("Paste the encrypted text:");
270             String cdata = in.readLine();
271             System.out.println(utils.decrypt(cdata));
272         }
273     }
274 }