1 /*******************************************************************************
\r
2 * ============LICENSE_START====================================================
\r
4 * * ===========================================================================
\r
5 * * Copyright © 2017 AT&T Intellectual Property. All rights reserved.
\r
6 * * Copyright © 2017 Amdocs
\r
7 * * ===========================================================================
\r
8 * * Licensed under the Apache License, Version 2.0 (the "License");
\r
9 * * you may not use this file except in compliance with the License.
\r
10 * * You may obtain a copy of the License at
\r
12 * * http://www.apache.org/licenses/LICENSE-2.0
\r
14 * * Unless required by applicable law or agreed to in writing, software
\r
15 * * distributed under the License is distributed on an "AS IS" BASIS,
\r
16 * * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
\r
17 * * See the License for the specific language governing permissions and
\r
18 * * limitations under the License.
\r
19 * * ============LICENSE_END====================================================
\r
21 * * ECOMP is a trademark and service mark of AT&T Intellectual Property.
\r
23 ******************************************************************************/
\r
24 package com.att.cadi;
\r
26 import java.io.ByteArrayInputStream;
\r
27 import java.io.ByteArrayOutputStream;
\r
28 import java.io.DataInputStream;
\r
29 import java.io.DataOutputStream;
\r
30 import java.io.File;
\r
31 import java.io.FileInputStream;
\r
32 import java.io.IOException;
\r
33 import java.io.InputStream;
\r
34 import java.io.OutputStream;
\r
35 import java.security.SecureRandom;
\r
36 import java.util.ArrayList;
\r
37 import java.util.Random;
\r
39 import javax.crypto.CipherInputStream;
\r
40 import javax.crypto.CipherOutputStream;
\r
42 import com.att.cadi.Access.Level;
\r
43 import com.att.cadi.config.Config;
\r
46 * Key Conversion, primarily "Base64"
\r
48 * Base64 is required for "Basic Authorization", which is an important part of the overall CADI Package.
\r
50 * Note: This author found that there is not a "standard" library for Base64 conversion within Java.
\r
51 * The source code implementations available elsewhere were surprisingly inefficient, requiring, for
\r
52 * instance, multiple string creation, on a transaction pass. Integrating other packages that might be
\r
53 * efficient enough would put undue Jar File Dependencies given this Framework should have none-but-Java
\r
56 * The essential algorithm is good for a symmetrical key system, as Base64 is really just
\r
57 * a symmetrical key that everyone knows the values.
\r
59 * This code is quite fast, taking about .016 ms for encrypting, decrypting and even .08 for key
\r
60 * generation. The speed quality, especially of key generation makes this a candidate for a short term token
\r
61 * used for identity.
\r
63 * It may be used to easily avoid placing Clear-Text passwords in configurations, etc. and contains
\r
64 * supporting functions such as 2048 keyfile generation (see keygen). This keyfile should, of course,
\r
65 * be set to "400" (Unix) and protected as any other mechanism requires.
\r
67 * However, this algorithm has not been tested against hackers. Until such a time, utilize more tested
\r
68 * packages to protect Data, especially sensitive data at rest (long term).
\r
72 private static final byte[] DOUBLE_EQ = new byte[] {'=','='};
\r
73 public static final String ENC = "enc:";
\r
74 private static final SecureRandom random = new SecureRandom();
\r
76 public final char[] codeset;
\r
77 private final int splitLinesAt;
\r
78 private final String encoding;
\r
79 private final Convert convert;
\r
80 private final boolean endEquals;
\r
81 //Note: AES Encryption is not Thread Safe. It is Synchronized
\r
82 private static AES aes = null; // only initialized from File, and only if needed for Passwords
\r
85 * This is the standard base64 Key Set.
\r
88 public static final Symm base64 = new Symm(
\r
89 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray()
\r
90 ,76, Config.UTF_8,true);
\r
92 public static final Symm base64noSplit = new Symm(
\r
93 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray()
\r
94 ,Integer.MAX_VALUE, Config.UTF_8,true);
\r
97 * This is the standard base64 set suitable for URLs and Filenames
\r
100 public static final Symm base64url = new Symm(
\r
101 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray()
\r
102 ,76, Config.UTF_8,true);
\r
105 * A Password set, using US-ASCII
\r
108 public static final Symm encrypt = new Symm(base64url.codeset,1024, "US-ASCII", false);
\r
111 * A typical set of Password Chars
\r
112 * Note, this is too large to fit into the algorithm. Only use with PassGen
\r
114 private static char passChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+!@#$%^&*(){}[]?:;,.".toCharArray();
\r
119 * Use this to create special case Case Sets and/or Line breaks
\r
121 * If you don't know why you need this, use the Singleton Method
\r
126 public Symm(char[] codeset, int split, String charset, boolean useEndEquals) {
\r
127 this.codeset = codeset;
\r
128 splitLinesAt = split;
\r
129 encoding = charset;
\r
130 endEquals = useEndEquals;
\r
131 char prev = 0, curr=0, first = 0;
\r
132 int offset=Integer.SIZE; // something that's out of range for integer array
\r
134 // There can be time efficiencies gained when the underlying keyset consists mainly of ordered
\r
135 // data (i.e. abcde...). Therefore, we'll quickly analyze the keyset. If it proves to have
\r
136 // too much entropy, the "Unordered" algorithm, which is faster in such cases is used.
\r
137 ArrayList<int[]> la = new ArrayList<int[]>();
\r
138 for(int i=0;i<codeset.length;++i) {
\r
140 if(prev+1==curr) { // is next character in set
\r
143 if(offset!=Integer.SIZE) { // add previous range
\r
144 la.add(new int[]{first,prev,offset});
\r
146 first = prev = curr;
\r
150 la.add(new int[]{first,curr,offset});
\r
151 if(la.size()>codeset.length/3) {
\r
152 convert = new Unordered(codeset);
\r
153 } else { // too random to get speed enhancement from range algorithm
\r
154 int[][] range = new int[la.size()][];
\r
156 convert = new Ordered(range);
\r
160 public Symm copy(int lines) {
\r
161 return new Symm(codeset,lines,encoding,endEquals);
\r
164 // Only used by keygen, which is intentionally randomized. Therefore, always use unordered
\r
165 private Symm(char[] codeset, Symm parent) {
\r
166 this.codeset = codeset;
\r
167 splitLinesAt = parent.splitLinesAt;
\r
168 endEquals = parent.endEquals;
\r
169 encoding = parent.encoding;
\r
170 convert = new Unordered(codeset);
\r
174 * Obtain the base64() behavior of this class, for use in standard BASIC AUTH mechanism, etc.
\r
178 public static final Symm base64() {
\r
183 * Obtain the base64() behavior of this class, for use in standard BASIC AUTH mechanism, etc.
\r
184 * No Line Splitting
\r
188 public static final Symm base64noSplit() {
\r
189 return base64noSplit;
\r
193 * Obtain the base64 "URL" behavior of this class, for use in File Names, etc. (no "/")
\r
196 public static final Symm base64url() {
\r
201 * Obtain a special ASCII version for Scripting, with base set of base64url use in File Names, etc. (no "/")
\r
203 public static final Symm baseCrypt() {
\r
208 * Note: AES Encryption is NOT thread-safe. Must surround entire use with synchronized
\r
210 private synchronized void exec(AESExec exec) throws IOException {
\r
213 byte[] bytes = new byte[AES.AES_KEY_SIZE/8];
\r
214 int offset = (Math.abs(codeset[0])+47)%(codeset.length-bytes.length);
\r
215 for(int i=0;i<bytes.length;++i) {
\r
216 bytes[i] = (byte)codeset[i+offset];
\r
218 aes = new AES(bytes,0,bytes.length);
\r
219 } catch (Exception e) {
\r
220 throw new IOException(e);
\r
226 private static interface AESExec {
\r
227 public void exec(AES aes) throws IOException;
\r
230 public byte[] encode(byte[] toEncrypt) throws IOException {
\r
231 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(toEncrypt.length*1.25));
\r
232 encode(new ByteArrayInputStream(toEncrypt),baos);
\r
233 return baos.toByteArray();
\r
236 public byte[] decode(byte[] encrypted) throws IOException {
\r
237 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(encrypted.length*1.25));
\r
238 decode(new ByteArrayInputStream(encrypted),baos);
\r
239 return baos.toByteArray();
\r
243 * Helper function for String API of "Encode"
\r
244 * use "getBytes" with appropriate char encoding, etc.
\r
248 * @throws IOException
\r
250 public String encode(String str) throws IOException {
\r
253 array = str.getBytes(encoding);
\r
254 } catch (IOException e) {
\r
255 array = str.getBytes(); // take default
\r
257 // Calculate expected size to avoid any buffer expansion copies within the ByteArrayOutput code
\r
258 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(array.length*1.363)); // account for 4 bytes for 3 and a byte or two more
\r
260 encode(new ByteArrayInputStream(array),baos);
\r
261 return baos.toString(encoding);
\r
265 * Helper function for the String API of "Decode"
\r
266 * use "getBytes" with appropriate char encoding, etc.
\r
269 * @throws IOException
\r
271 public String decode(String str) throws IOException {
\r
274 array = str.getBytes(encoding);
\r
275 } catch (IOException e) {
\r
276 array = str.getBytes(); // take default
\r
278 // Calculate expected size to avoid any buffer expansion copies within the ByteArrayOutput code
\r
279 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(array.length*.76)); // Decoding is 3 bytes for 4. Allocate slightly more than 3/4s
\r
280 decode(new ByteArrayInputStream(array), baos);
\r
281 return baos.toString(encoding);
\r
285 * Convenience Function
\r
287 * encode String into InputStream and call encode(InputStream, OutputStream)
\r
291 * @throws IOException
\r
293 public void encode(String string, OutputStream out) throws IOException {
\r
294 encode(new ByteArrayInputStream(string.getBytes()),out);
\r
298 * Convenience Function
\r
300 * encode String into InputStream and call decode(InputStream, OutputStream)
\r
304 * @throws IOException
\r
306 public void decode(String string, OutputStream out) throws IOException {
\r
307 decode(new ByteArrayInputStream(string.getBytes()),out);
\r
310 public void encode(InputStream is, OutputStream os, byte[] prefix) throws IOException {
\r
316 * encode InputStream onto Output Stream
\r
321 * @throws IOException
\r
323 public void encode(InputStream is, OutputStream os) throws IOException {
\r
324 // StringBuilder sb = new StringBuilder((int)(estimate*1.255)); // try to get the right size of StringBuilder from start.. slightly more than 1.25 times
\r
326 int read, idx=0, line=0;
\r
331 if(line>=splitLinesAt) {
\r
335 switch(++idx) { // 1 based reading, slightly faster ++
\r
336 case 1: // ptr is the first 6 bits of read
\r
337 os.write(codeset[read>>2]);
\r
340 case 2: // ptr is the last 2 bits of prev followed by the first 4 bits of read
\r
341 os.write(codeset[((prev & 0x03)<<4) | (read>>4)]);
\r
345 // Char 1 is last 4 bits of prev plus the first 2 bits of read
\r
346 // Char 2 is the last 6 bits of read
\r
347 os.write(codeset[(((prev & 0xF)<<2) | (read>>6))]);
\r
348 if(line==splitLinesAt) { // deal with line splitting for two characters
\r
352 os.write(codeset[(read & 0x3F)]);
\r
358 } else { // deal with any remaining bits from Prev, then pad
\r
360 case 1: // just the last 2 bits of prev
\r
361 os.write(codeset[(prev & 0x03)<<4]);
\r
362 if(endEquals)os.write(DOUBLE_EQ);
\r
364 case 2: // just the last 4 bits of prev
\r
365 os.write(codeset[(prev & 0xF)<<2]);
\r
366 if(endEquals)os.write('=');
\r
375 public void decode(InputStream is, OutputStream os, int skip) throws IOException {
\r
381 * Decode InputStream onto OutputStream
\r
384 * @throws IOException
\r
386 public void decode(InputStream is, OutputStream os) throws IOException {
\r
389 while((read = is.read())>=0) {
\r
390 index = convert.convert(read);
\r
392 switch(++idx) { // 1 based cases, slightly faster ++
\r
393 case 1: // index goes into first 6 bits of prev
\r
396 case 2: // write second 2 bits of into prev, write byte, last 4 bits go into prev
\r
397 os.write((byte)(prev|(index>>4)));
\r
400 case 3: // first 4 bits of index goes into prev, write byte, last 2 bits go into prev
\r
401 os.write((byte)(prev|(index>>2)));
\r
404 default: // (3+) | prev and last six of index
\r
405 os.write((byte)(prev|(index&0x3F)));
\r
414 * Interface to allow this class to choose which algorithm to find index of character in Key
\r
417 private interface Convert {
\r
418 public int convert(int read) throws IOException;
\r
422 * Ordered uses a range of orders to compare against, rather than requiring the investigation
\r
423 * of every character needed.
\r
426 private static final class Ordered implements Convert {
\r
427 private int[][] range;
\r
428 public Ordered(int[][] range) {
\r
429 this.range = range;
\r
431 public int convert(int read) throws IOException {
\r
438 for(int i=0;i<range.length;++i) {
\r
439 if(read >= range[i][0] && read<=range[i][1]) {
\r
440 return read-range[i][2];
\r
443 throw new IOException("Unacceptable Character in Stream");
\r
448 * Unordered, i.e. the key is purposely randomized, simply has to investigate each character
\r
449 * until we find a match.
\r
452 private static final class Unordered implements Convert {
\r
453 private char[] codec;
\r
454 public Unordered(char[] codec) {
\r
455 this.codec = codec;
\r
457 public int convert(int read) throws IOException {
\r
464 for(int i=0;i<codec.length;++i) {
\r
465 if(codec[i]==read)return i;
\r
467 // don't give clue in Encryption mode
\r
468 throw new IOException("Unacceptable Character in Stream");
\r
473 * Generate a 2048 based Key from which we extract our code base
\r
476 * @throws IOException
\r
478 public byte[] keygen() throws IOException {
\r
479 byte inkey[] = new byte[0x600];
\r
480 new SecureRandom().nextBytes(inkey);
\r
481 ByteArrayOutputStream baos = new ByteArrayOutputStream(0x800);
\r
482 base64url.encode(new ByteArrayInputStream(inkey), baos);
\r
483 return baos.toByteArray();
\r
486 // A class allowing us to be less predictable about significant digits (i.e. not picking them up from the
\r
487 // beginning, and not picking them up in an ordered row. Gives a nice 2048 with no visible patterns.
\r
488 private class Obtain {
\r
491 private int length;
\r
492 private byte[] key;
\r
494 private Obtain(Symm b64, byte[] key) {
\r
495 skip = Math.abs(key[key.length-13]%key.length);
\r
496 if((key.length&0x1) == (skip&0x1)) { // if both are odd or both are even
\r
499 length = b64.codeset.length;
\r
500 last = 17+length%59; // never start at beginning
\r
504 private int next() {
\r
505 return Math.abs(key[(++last*skip)%key.length])%length;
\r
510 * Obtain a Symm from "keyfile" (Config.KEYFILE) property
\r
515 public static Symm obtain(Access access) {
\r
516 Symm symm = Symm.baseCrypt();
\r
518 String keyfile = access.getProperty(Config.CADI_KEYFILE,null);
\r
519 if(keyfile!=null) {
\r
520 File file = new File(keyfile);
\r
522 access.log(Level.INIT, Config.CADI_KEYFILE,"points to",file.getCanonicalPath());
\r
523 } catch (IOException e1) {
\r
524 access.log(Level.INIT, Config.CADI_KEYFILE,"points to",file.getAbsolutePath());
\r
526 if(file.exists()) {
\r
528 FileInputStream fis = new FileInputStream(file);
\r
530 symm = Symm.obtain(fis);
\r
534 } catch (IOException e) {
\r
537 } catch (IOException e) {
\r
538 access.log(e, "Cannot load keyfile");
\r
545 * Create a new random key
\r
547 public Symm obtain() throws IOException {
\r
548 byte inkey[] = new byte[0x800];
\r
549 new SecureRandom().nextBytes(inkey);
\r
550 return obtain(inkey);
\r
554 * Obtain a Symm from 2048 key from a String
\r
558 * @throws IOException
\r
560 public static Symm obtain(String key) throws IOException {
\r
561 return obtain(new ByteArrayInputStream(key.getBytes()));
\r
565 * Obtain a Symm from 2048 key from a Stream
\r
569 * @throws IOException
\r
571 public static Symm obtain(InputStream is) throws IOException {
\r
572 ByteArrayOutputStream baos = new ByteArrayOutputStream();
\r
574 base64url.decode(is, baos);
\r
575 } catch (IOException e) {
\r
577 throw new IOException("Invalid Key");
\r
579 byte[] bkey = baos.toByteArray();
\r
580 if(bkey.length<0x88) { // 2048 bit key
\r
581 throw new IOException("Invalid key");
\r
583 return baseCrypt().obtain(bkey);
\r
587 * Convenience for picking up Keyfile
\r
591 * @throws IOException
\r
593 public static Symm obtain(File f) throws IOException {
\r
594 FileInputStream fis = new FileInputStream(f);
\r
596 return obtain(fis);
\r
602 * Decrypt into a String
\r
604 * Convenience method
\r
608 * @throws IOException
\r
610 public String enpass(String password) throws IOException {
\r
611 ByteArrayOutputStream baos = new ByteArrayOutputStream();
\r
612 enpass(password,baos);
\r
613 return new String(baos.toByteArray());
\r
617 * Create an encrypted password, making sure that even short passwords have a minimum length.
\r
621 * @throws IOException
\r
623 public void enpass(final String password, final OutputStream os) throws IOException {
\r
624 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
\r
625 DataOutputStream dos = new DataOutputStream(baos);
\r
626 byte[] bytes = password.getBytes();
\r
627 if(this.getClass().getSimpleName().startsWith("base64")) { // don't expose randomization
\r
631 Random r = new SecureRandom();
\r
634 for(int i=0;i<3;++i) {
\r
635 dos.writeByte(b=(byte)r.nextInt());
\r
636 start+=Math.abs(b);
\r
639 for(int i=0;i<start;++i) {
\r
640 dos.writeByte(r.nextInt());
\r
642 dos.writeInt((int)System.currentTimeMillis());
\r
643 int minlength = Math.min(0x9,bytes.length);
\r
644 dos.writeByte(minlength); // expect truncation
\r
645 if(bytes.length<0x9) {
\r
646 for(int i=0;i<bytes.length;++i) {
\r
647 dos.writeByte(r.nextInt());
\r
648 dos.writeByte(bytes[i]);
\r
650 // make sure it's long enough
\r
651 for(int i=bytes.length;i<0x9;++i) {
\r
652 dos.writeByte(r.nextInt());
\r
659 // 7/21/2016 jg add AES Encryption to the mix
\r
660 exec(new AESExec() {
\r
662 public void exec(AES aes) throws IOException {
\r
663 CipherInputStream cis = aes.inputStream(new ByteArrayInputStream(baos.toByteArray()), true);
\r
672 synchronized(ENC) {
\r
677 * Decrypt a password into a String
\r
679 * Convenience method
\r
683 * @throws IOException
\r
685 public String depass(String password) throws IOException {
\r
686 if(password==null)return null;
\r
687 ByteArrayOutputStream baos = new ByteArrayOutputStream();
\r
688 depass(password,baos);
\r
689 return new String(baos.toByteArray());
\r
693 * Decrypt a password
\r
700 * @throws IOException
\r
702 public long depass(final String password, final OutputStream os) throws IOException {
\r
703 int offset = password.startsWith(ENC)?4:0;
\r
704 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
\r
705 final ByteArrayInputStream bais = new ByteArrayInputStream(password.getBytes(),offset,password.length()-offset);
\r
706 exec(new AESExec() {
\r
708 public void exec(AES aes) throws IOException {
\r
709 CipherOutputStream cos = aes.outputStream(baos, false);
\r
711 cos.close(); // flush
\r
714 byte[] bytes = baos.toByteArray();
\r
715 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes));
\r
717 if(this.getClass().getSimpleName().startsWith("base64")) { // don't expose randomization
\r
722 for(int i=0;i<3;++i) {
\r
723 start+=Math.abs(dis.readByte());
\r
726 for(int i=0;i<start;++i) {
\r
729 time = (dis.readInt() & 0xFFFF)|(System.currentTimeMillis()&0xFFFF0000);
\r
730 int minlength = dis.readByte();
\r
732 DataOutputStream dos = new DataOutputStream(os);
\r
733 for(int i=0;i<minlength;++i) {
\r
735 dos.writeByte(dis.readByte());
\r
738 int pre =((Byte.SIZE*3+Integer.SIZE+Byte.SIZE)/Byte.SIZE)+start;
\r
739 os.write(bytes, pre, bytes.length-pre);
\r
745 public static String randomGen(int numBytes) {
\r
746 return randomGen(passChars,numBytes);
\r
749 public static String randomGen(char[] chars ,int numBytes) {
\r
751 StringBuilder sb = new StringBuilder(numBytes);
\r
752 for(int i=0;i<numBytes;++i) {
\r
753 rint = random.nextInt(chars.length);
\r
754 sb.append(chars[rint]);
\r
756 return sb.toString();
\r
758 // Internal mechanism for helping to randomize placement of characters within a Symm codeset
\r
759 // Based on an incoming data stream (originally created randomly, but can be recreated within
\r
760 // 2048 key), go after a particular place in the new codeset. If that codeset spot is used, then move
\r
761 // right or left (depending on iteration) to find the next available slot. In this way, key generation
\r
762 // is speeded up by only enacting N iterations, but adds a spreading effect of the random number stream, so that keyset is also
\r
763 // shuffled for a good spread. It is, however, repeatable, given the same number set, allowing for
\r
764 // quick recreation when the official stream is actually obtained.
\r
765 public Symm obtain(byte[] key) throws IOException {
\r
767 byte[] bytes = new byte[AES.AES_KEY_SIZE/8];
\r
768 int offset = (Math.abs(key[(47%key.length)])+137)%(key.length-bytes.length);
\r
769 for(int i=0;i<bytes.length;++i) {
\r
770 bytes[i] = key[i+offset];
\r
773 aes = new AES(bytes,0,bytes.length);
\r
774 } catch (Exception e) {
\r
775 throw new IOException(e);
\r
777 int filled = codeset.length;
\r
778 char[] seq = new char[filled];
\r
779 int end = filled--;
\r
781 boolean right = true;
\r
783 Obtain o = new Obtain(this,key);
\r
787 if(index<0 || index>=codeset.length) {
\r
788 System.out.println("uh, oh");
\r
790 if(right) { // alternate going left or right to find the next open slot (keeps it from taking too long to hit something)
\r
791 for(int j=index;j<end;++j) {
\r
793 seq[j]=codeset[filled];
\r
800 for(int j=index;j>=0;--j) {
\r
802 seq[j]=codeset[filled];
\r
810 return new Symm(seq,this);
\r