2 * ============LICENSE_START====================================================
4 * ===========================================================================
5 * Copyright (c) 2018 AT&T Intellectual Property. All rights reserved.
6 * ===========================================================================
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
11 * http://www.apache.org/licenses/LICENSE-2.0
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 * ============LICENSE_END====================================================
22 package org.onap.aaf.cadi;
24 import java.io.ByteArrayInputStream;
25 import java.io.ByteArrayOutputStream;
26 import java.io.DataInputStream;
27 import java.io.DataOutputStream;
29 import java.io.FileInputStream;
30 import java.io.IOException;
31 import java.io.InputStream;
32 import java.io.OutputStream;
33 import java.security.SecureRandom;
34 import java.util.ArrayList;
35 import java.util.Date;
36 import java.util.Random;
38 import javax.crypto.CipherInputStream;
39 import javax.crypto.CipherOutputStream;
41 import org.onap.aaf.cadi.Access.Level;
42 import org.onap.aaf.cadi.config.Config;
45 * Key Conversion, primarily "Base64"
47 * Base64 is required for "Basic Authorization", which is an important part of the overall CADI Package.
49 * Note: This author found that there is not a "standard" library for Base64 conversion within Java.
50 * The source code implementations available elsewhere were surprisingly inefficient, requiring, for
51 * instance, multiple string creation, on a transaction pass. Integrating other packages that might be
52 * efficient enough would put undue Jar File Dependencies given this Framework should have none-but-Java
55 * The essential algorithm is good for a symmetrical key system, as Base64 is really just
56 * a symmetrical key that everyone knows the values.
58 * This code is quite fast, taking about .016 ms for encrypting, decrypting and even .08 for key
59 * generation. The speed quality, especially of key generation makes this a candidate for a short term token
62 * It may be used to easily avoid placing Clear-Text passwords in configurations, etc. and contains
63 * supporting functions such as 2048 keyfile generation (see keygen). This keyfile should, of course,
64 * be set to "400" (Unix) and protected as any other mechanism requires.
66 * AES Encryption is also employed to include standards.
72 private static final byte[] DOUBLE_EQ = new byte[] {'=','='};
73 public static final String ENC = "enc:";
74 private static final Object LOCK = new Object();
75 private static final SecureRandom random = new SecureRandom();
77 public final char[] codeset;
78 private final int splitLinesAt;
79 private final String encoding;
80 private final Convert convert;
81 private final boolean endEquals;
82 private byte[] keyBytes = null;
83 //Note: AES Encryption is not Thread Safe. It is Synchronized
84 //private AES aes = null; // only initialized from File, and only if needed for Passwords
88 * This is the standard base64 Key Set.
91 public static final Symm base64 = new Symm(
92 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray()
93 ,76, Config.UTF_8,true, "Base64");
95 public static final Symm base64noSplit = new Symm(
96 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray()
97 ,Integer.MAX_VALUE, Config.UTF_8,true, "Base64, no Split");
100 * This is the standard base64 set suitable for URLs and Filenames
103 public static final Symm base64url = new Symm(
104 "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_".toCharArray()
105 ,76, Config.UTF_8,true, "Base64 for URL");
108 * A Password set, using US-ASCII
111 public static final Symm encrypt = new Symm(base64url.codeset,1024, "US-ASCII", false, "Base64, 1024 size");
112 private static final byte[] EMPTY = new byte[0];
115 * A typical set of Password Chars
116 * Note, this is too large to fit into the algorithm. Only use with PassGen
118 private static char passChars[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+!@#$%^&*(){}[]?:;,.".toCharArray();
121 private static Symm internalOnly = null;
124 * Use this to create special case Case Sets and/or Line breaks
126 * If you don't know why you need this, use the Singleton Method
131 public Symm(char[] codeset, int split, String charset, boolean useEndEquals, String name) {
132 this.codeset = codeset;
133 splitLinesAt = split;
135 endEquals = useEndEquals;
137 char prev = 0, curr=0, first = 0;
138 int offset=Integer.SIZE; // something that's out of range for integer array
140 // There can be time efficiencies gained when the underlying keyset consists mainly of ordered
141 // data (i.e. abcde...). Therefore, we'll quickly analyze the keyset. If it proves to have
142 // too much entropy, the "Unordered" algorithm, which is faster in such cases is used.
143 ArrayList<int[]> la = new ArrayList<>();
144 for (int i=0;i<codeset.length;++i) {
146 if (prev+1==curr) { // is next character in set
149 if (offset!=Integer.SIZE) { // add previous range
150 la.add(new int[]{first,prev,offset});
156 la.add(new int[]{first,curr,offset});
157 if (la.size()>codeset.length/3) {
158 convert = new Unordered(codeset);
159 } else { // too random to get speed enhancement from range algorithm
160 int[][] range = new int[la.size()][];
162 convert = new Ordered(range);
166 public Symm copy(int lines) {
167 return new Symm(codeset,lines,encoding,endEquals, "Copied " + lines);
170 // Only used by keygen, which is intentionally randomized. Therefore, always use unordered
171 private Symm(char[] codeset, Symm parent) {
172 this.codeset = codeset;
173 splitLinesAt = parent.splitLinesAt;
174 endEquals = parent.endEquals;
175 encoding = parent.encoding;
176 convert = new Unordered(codeset);
180 * Obtain the base64() behavior of this class, for use in standard BASIC AUTH mechanism, etc.
184 public static final Symm base64() {
189 * Obtain the base64() behavior of this class, for use in standard BASIC AUTH mechanism, etc.
194 public static final Symm base64noSplit() {
195 return base64noSplit;
199 * Obtain the base64 "URL" behavior of this class, for use in File Names, etc. (no "/")
202 public static final Symm base64url() {
207 * Obtain a special ASCII version for Scripting, with base set of base64url use in File Names, etc. (no "/")
209 public static final Symm baseCrypt() {
213 public <T> T exec(SyncExec<T> exec) throws Exception {
215 if (keyBytes == null) {
216 keyBytes = new byte[AES.AES_KEY_SIZE/8];
217 int offset = (Math.abs(codeset[0])+47)%(codeset.length-keyBytes.length);
218 for (int i=0;i<keyBytes.length;++i) {
219 keyBytes[i] = (byte)codeset[i+offset];
223 return exec.exec(new AES(keyBytes,0,keyBytes.length));
226 public interface Encryption {
227 public CipherOutputStream outputStream(OutputStream os, boolean encrypt);
228 public CipherInputStream inputStream(InputStream is, boolean encrypt);
231 public static interface SyncExec<T> {
232 public T exec(Encryption enc) throws IOException, Exception;
235 public byte[] encode(byte[] toEncrypt) throws IOException {
236 if (toEncrypt==null) {
239 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(toEncrypt.length*1.25));
240 encode(new ByteArrayInputStream(toEncrypt),baos);
241 return baos.toByteArray();
245 public byte[] decode(byte[] encrypted) throws IOException {
246 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(encrypted.length*1.25));
247 decode(new ByteArrayInputStream(encrypted),baos);
248 return baos.toByteArray();
252 * Helper function for String API of "Encode"
253 * use "getBytes" with appropriate char encoding, etc.
257 * @throws IOException
259 public String encode(String str) throws IOException {
261 boolean useDefaultEncoding = false;
263 array = str.getBytes(encoding);
264 } catch (IOException e) {
265 array = str.getBytes(); // take default
266 useDefaultEncoding = true;
268 // Calculate expected size to avoid any buffer expansion copies within the ByteArrayOutput code
269 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(array.length*1.363)); // account for 4 bytes for 3 and a byte or two more
271 encode(new ByteArrayInputStream(array),baos);
272 if (useDefaultEncoding) {
273 return baos.toString();
275 return baos.toString(encoding);
279 * Helper function for the String API of "Decode"
280 * use "getBytes" with appropriate char encoding, etc.
283 * @throws IOException
285 public String decode(String str) throws IOException {
287 boolean useDefaultEncoding = false;
289 array = str.getBytes(encoding);
290 } catch (IOException e) {
291 array = str.getBytes(); // take default
292 useDefaultEncoding = true;
294 // Calculate expected size to avoid any buffer expansion copies within the ByteArrayOutput code
295 ByteArrayOutputStream baos = new ByteArrayOutputStream((int)(array.length*.76)); // Decoding is 3 bytes for 4. Allocate slightly more than 3/4s
296 decode(new ByteArrayInputStream(array), baos);
297 if (useDefaultEncoding) {
298 return baos.toString();
300 return baos.toString(encoding);
304 * Convenience Function
306 * encode String into InputStream and call encode(InputStream, OutputStream)
310 * @throws IOException
312 public void encode(String string, OutputStream out) throws IOException {
313 encode(new ByteArrayInputStream(string.getBytes()),out);
317 * Convenience Function
319 * encode String into InputStream and call decode(InputStream, OutputStream)
323 * @throws IOException
325 public void decode(String string, OutputStream out) throws IOException {
326 decode(new ByteArrayInputStream(string.getBytes()),out);
329 public void encode(InputStream is, OutputStream os, byte[] prefix) throws IOException {
335 * encode InputStream onto Output Stream
340 * @throws IOException
342 public void encode(InputStream is, OutputStream os) throws IOException {
343 // StringBuilder sb = new StringBuilder((int)(estimate*1.255)); // try to get the right size of StringBuilder from start.. slightly more than 1.25 times
345 int read, idx=0, line=0;
350 if (line>=splitLinesAt) {
354 switch(++idx) { // 1 based reading, slightly faster ++
355 case 1: // ptr is the first 6 bits of read
356 os.write(codeset[read>>2]);
359 case 2: // ptr is the last 2 bits of prev followed by the first 4 bits of read
360 os.write(codeset[((prev & 0x03)<<4) | (read>>4)]);
364 // Char 1 is last 4 bits of prev plus the first 2 bits of read
365 // Char 2 is the last 6 bits of read
366 os.write(codeset[(((prev & 0xF)<<2) | (read>>6))]);
367 if (line==splitLinesAt) { // deal with line splitting for two characters
371 os.write(codeset[(read & 0x3F)]);
377 } else { // deal with any remaining bits from Prev, then pad
379 case 1: // just the last 2 bits of prev
380 os.write(codeset[(prev & 0x03)<<4]);
381 if (endEquals)os.write(DOUBLE_EQ);
383 case 2: // just the last 4 bits of prev
384 os.write(codeset[(prev & 0xF)<<2]);
385 if (endEquals)os.write('=');
394 public void decode(InputStream is, OutputStream os, int skip) throws IOException {
395 if (is.skip(skip)!=skip) {
396 throw new IOException("Error skipping on IOStream in Symm");
402 * Decode InputStream onto OutputStream
405 * @throws IOException
407 public void decode(InputStream is, OutputStream os) throws IOException {
410 while ((read = is.read())>=0) {
411 index = convert.convert(read);
413 switch(++idx) { // 1 based cases, slightly faster ++
414 case 1: // index goes into first 6 bits of prev
417 case 2: // write second 2 bits of into prev, write byte, last 4 bits go into prev
418 os.write((byte)(prev|(index>>4)));
421 case 3: // first 4 bits of index goes into prev, write byte, last 2 bits go into prev
422 os.write((byte)(prev|(index>>2)));
425 default: // (3+) | prev and last six of index
426 os.write((byte)(prev|(index&0x3F)));
435 * Interface to allow this class to choose which algorithm to find index of character in Key
439 private interface Convert {
440 public int convert(int read) throws IOException;
444 * Ordered uses a range of orders to compare against, rather than requiring the investigation
445 * of every character needed.
449 private static final class Ordered implements Convert {
450 private int[][] range;
451 public Ordered(int[][] range) {
454 public int convert(int read) throws IOException {
455 // System.out.print((char)read);
464 for (int i=0;i<range.length;++i) {
465 if (read >= range[i][0] && read<=range[i][1]) {
466 return read-range[i][2];
469 throw new IOException("Unacceptable Character in Stream");
474 * Unordered, i.e. the key is purposely randomized, simply has to investigate each character
475 * until we find a match.
479 private static final class Unordered implements Convert {
480 private char[] codec;
481 public Unordered(char[] codec) {
484 public int convert(int read) throws IOException {
492 for (int i=0;i<codec.length;++i) {
493 if (codec[i]==read)return i;
495 // don't give clue in Encryption mode
496 throw new IOException("Unacceptable Character in Stream");
501 * Generate a 2048 based Key from which we extract our code base
504 * @throws IOException
506 public static byte[] keygen() throws IOException {
507 byte inkey[] = new byte[0x600];
508 new SecureRandom().nextBytes(inkey);
509 ByteArrayOutputStream baos = new ByteArrayOutputStream(0x800);
510 base64url.encode(new ByteArrayInputStream(inkey), baos);
511 return baos.toByteArray();
514 // A class allowing us to be less predictable about significant digits (i.e. not picking them up from the
515 // beginning, and not picking them up in an ordered row. Gives a nice 2048 with no visible patterns.
516 private class Obtain {
522 private Obtain(Symm b64, byte[] key) {
523 skip = Math.abs(key[key.length-13]%key.length);
524 if ((key.length&0x1) == (skip&0x1)) { // if both are odd or both are even
527 length = b64.codeset.length;
528 last = 17+length%59; // never start at beginning
533 return Math.abs(key[(++last*skip)%key.length])%length;
538 * Obtain a Symm from "keyfile" (Config.KEYFILE) property
542 * @throws IOException
543 * @throws CadiException
545 public static Symm obtain(Access access) throws CadiException {
546 String keyfile = access.getProperty(Config.CADI_KEYFILE,null);
548 Symm symm = Symm.baseCrypt();
550 File file = new File(keyfile);
552 access.log(Level.INIT, Config.CADI_KEYFILE,"points to",file.getCanonicalPath());
553 } catch (IOException e1) {
554 access.log(Level.INIT, Config.CADI_KEYFILE,"points to",file.getAbsolutePath());
558 FileInputStream fis = new FileInputStream(file);
560 symm = Symm.obtain(fis);
564 } catch (IOException e) {
567 } catch (IOException e) {
568 access.log(e, "Cannot load keyfile");
573 filename = file.getCanonicalPath();
574 } catch (IOException e) {
575 filename = file.getAbsolutePath();
577 throw new CadiException("ERROR: " + filename + " does not exist!");
582 return internalOnly();
583 } catch (IOException e) {
584 throw new CadiException(e);
589 * Create a new random key
591 public Symm obtain() throws IOException {
592 byte inkey[] = new byte[0x800];
593 new SecureRandom().nextBytes(inkey);
594 Symm s = obtain(inkey);
595 s.name = "from Random";
600 * Obtain a Symm from 2048 key from a String
604 * @throws IOException
606 public static Symm obtain(String key) throws IOException {
607 Symm s = obtain(new ByteArrayInputStream(key.getBytes()));
608 s.name = "from String";
613 * Obtain a Symm from 2048 key from a Stream
617 * @throws IOException
619 public static Symm obtain(InputStream is) throws IOException {
620 ByteArrayOutputStream baos = new ByteArrayOutputStream();
622 base64url.decode(is, baos);
623 } catch (IOException e) {
625 throw new IOException("Invalid Key");
627 byte[] bkey = baos.toByteArray();
628 if (bkey.length<0x88) { // 2048 bit key
629 throw new IOException("Invalid key");
631 Symm s = baseCrypt().obtain(bkey);
632 s.name = "from InputStream";
637 * Convenience for picking up Keyfile
641 * @throws IOException
643 public static Symm obtain(File f) throws IOException {
644 FileInputStream fis = new FileInputStream(f);
646 Symm s = obtain(fis);
647 s.name = "From " + f.getCanonicalPath() + " dated " + new Date(f.lastModified());
654 * Decrypt into a String
660 * @throws IOException
662 public String enpass(String password) throws IOException {
663 ByteArrayOutputStream baos = new ByteArrayOutputStream();
664 enpass(password,baos);
665 return new String(baos.toByteArray());
669 * Create an encrypted password, making sure that even short passwords have a minimum length.
673 * @throws IOException
675 public void enpass(final String password, final OutputStream os) throws IOException {
676 if (password==null) {
677 throw new IOException("Invalid password passed");
679 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
680 DataOutputStream dos = new DataOutputStream(baos);
681 byte[] bytes = password.getBytes();
682 if (this.getClass().getSimpleName().startsWith("base64")) { // don't expose randomization
686 Random r = new SecureRandom();
689 for (int i=0;i<3;++i) {
690 dos.writeByte(b=(byte)r.nextInt());
694 for (int i=0;i<start;++i) {
695 dos.writeByte(r.nextInt());
697 dos.writeInt((int)System.currentTimeMillis());
698 int minlength = Math.min(0x9,bytes.length);
699 dos.writeByte(minlength); // expect truncation
700 if (bytes.length<0x9) {
701 for (int i=0;i<bytes.length;++i) {
702 dos.writeByte(r.nextInt());
703 dos.writeByte(bytes[i]);
705 // make sure it's long enough
706 for (int i=bytes.length;i<0x9;++i) {
707 dos.writeByte(r.nextInt());
714 // 7/21/2016 Jonathan add AES Encryption to the mix
716 exec(new SyncExec<Void>() {
718 public Void exec(Encryption enc) throws Exception {
719 CipherInputStream cis = enc.inputStream(new ByteArrayInputStream(baos.toByteArray()), true);
729 } catch (IOException e) {
731 } catch (Exception e) {
732 throw new IOException(e);
737 * Decrypt a password into a String
743 * @throws IOException
745 public String depass(String password) throws IOException {
746 if (password==null)return null;
747 ByteArrayOutputStream baos = new ByteArrayOutputStream();
748 depass(password,baos);
749 return new String(baos.toByteArray());
760 * @throws IOException
762 public long depass(final String password, final OutputStream os) throws IOException {
763 int offset = password.startsWith(ENC)?4:0;
764 final ByteArrayOutputStream baos = new ByteArrayOutputStream();
765 final ByteArrayInputStream bais = new ByteArrayInputStream(password.getBytes(),offset,password.length()-offset);
767 exec(new SyncExec<Void>() {
769 public Void exec(Encryption enc) throws IOException {
770 CipherOutputStream cos = enc.outputStream(baos, false);
772 cos.close(); // flush
776 } catch (IOException e) {
778 } catch (Exception e) {
779 throw new IOException(e);
782 byte[] bytes = baos.toByteArray();
783 DataInputStream dis = new DataInputStream(new ByteArrayInputStream(bytes));
785 if (this.getClass().getSimpleName().startsWith("base64")) { // don't expose randomization
790 for (int i=0;i<3;++i) {
791 start+=Math.abs(dis.readByte());
794 for (int i=0;i<start;++i) {
797 time = (dis.readInt() & 0xFFFF)|(System.currentTimeMillis()&0xFFFF0000);
798 int minlength = dis.readByte();
800 DataOutputStream dos = new DataOutputStream(os);
801 for (int i=0;i<minlength;++i) {
803 dos.writeByte(dis.readByte());
806 int pre =((Byte.SIZE*3+Integer.SIZE+Byte.SIZE)/Byte.SIZE)+start;
807 os.write(bytes, pre, bytes.length-pre);
813 public static String randomGen(int numBytes) {
814 return randomGen(passChars,numBytes);
817 public static String randomGen(char[] chars ,int numBytes) {
819 StringBuilder sb = new StringBuilder(numBytes);
820 for (int i=0;i<numBytes;++i) {
821 rint = random.nextInt(chars.length);
822 sb.append(chars[rint]);
824 return sb.toString();
826 // Internal mechanism for helping to randomize placement of characters within a Symm codeset
827 // Based on an incoming data stream (originally created randomly, but can be recreated within
828 // 2048 key), go after a particular place in the new codeset. If that codeset spot is used, then move
829 // right or left (depending on iteration) to find the next available slot. In this way, key generation
830 // is speeded up by only enacting N iterations, but adds a spreading effect of the random number stream, so that keyset is also
831 // shuffled for a good spread. It is, however, repeatable, given the same number set, allowing for
832 // quick recreation when the official stream is actually obtained.
833 public Symm obtain(byte[] key) throws IOException {
834 int filled = codeset.length;
835 char[] seq = new char[filled];
838 boolean right = true;
840 Obtain o = new Obtain(this,key);
844 if (index<0 || index>=codeset.length) {
845 System.out.println("uh, oh");
847 if (right) { // alternate going left or right to find the next open slot (keeps it from taking too long to hit something)
848 for (int j=index;j<end;++j) {
850 seq[j]=codeset[filled];
857 for (int j=index;j>=0;--j) {
859 seq[j]=codeset[filled];
867 Symm newSymm = new Symm(seq,this);
868 newSymm.name = "from bytes";
871 newSymm.keyBytes = new byte[AES.AES_KEY_SIZE/8];
872 int offset = (Math.abs(key[(47%key.length)])+137)%(key.length-newSymm.keyBytes.length);
873 for (int i=0;i<newSymm.keyBytes.length;++i) {
874 newSymm.keyBytes[i] = key[i+offset];
876 } catch (Exception e) {
877 throw new IOException(e);
884 * This Symm is generated for internal JVM use. It has no external keyfile, but can be used
885 * for securing Memory, as it remains the same ONLY of the current JVM
887 * @throws IOException
889 public static synchronized Symm internalOnly() throws IOException {
890 if (internalOnly==null) {
891 ByteArrayInputStream baos = new ByteArrayInputStream(keygen());
893 internalOnly = Symm.obtain(baos);
902 public String toString() {