From 28e192c519c59d97d70aea6fabe18927e1aff42a Mon Sep 17 00:00:00 2001 From: AsamK Date: Mon, 11 May 2015 12:55:18 +0200 Subject: [PATCH] First commit --- .gitignore | 7 + build.gradle | 24 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 51018 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 164 ++ gradlew.bat | 90 + settings.gradle | 18 + src/main/java/cli/Base64.java | 2135 ++++++++++++++++++ src/main/java/cli/JsonAxolotlStore.java | 135 ++ src/main/java/cli/JsonIdentityKeyStore.java | 74 + src/main/java/cli/JsonPreKeyStore.java | 67 + src/main/java/cli/JsonSessionStore.java | 94 + src/main/java/cli/JsonSignedPreKeyStore.java | 84 + src/main/java/cli/Main.java | 148 ++ src/main/java/cli/Manager.java | 181 ++ src/main/java/cli/Util.java | 25 + src/main/java/cli/WhisperTrustStore.java | 20 + src/main/resources/cli/whisper.store | Bin 0 -> 1107 bytes 18 files changed, 3272 insertions(+) create mode 100644 .gitignore create mode 100644 build.gradle create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100755 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle create mode 100644 src/main/java/cli/Base64.java create mode 100644 src/main/java/cli/JsonAxolotlStore.java create mode 100644 src/main/java/cli/JsonIdentityKeyStore.java create mode 100644 src/main/java/cli/JsonPreKeyStore.java create mode 100644 src/main/java/cli/JsonSessionStore.java create mode 100644 src/main/java/cli/JsonSignedPreKeyStore.java create mode 100644 src/main/java/cli/Main.java create mode 100644 src/main/java/cli/Manager.java create mode 100644 src/main/java/cli/Util.java create mode 100644 src/main/java/cli/WhisperTrustStore.java create mode 100644 src/main/resources/cli/whisper.store diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..e98305a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.gradle/ +.idea/ +build/ +*~ +*.swp +*.iml +local.properties diff --git a/build.gradle b/build.gradle new file mode 100644 index 00000000..f9d89548 --- /dev/null +++ b/build.gradle @@ -0,0 +1,24 @@ +apply plugin: 'java' +apply plugin: 'application' + +mainClassName = 'cli.Main' + +repositories { + mavenCentral() +} + +dependencies { + compile 'org.whispersystems:textsecure-java:1.3.0' + compile 'com.madgag.spongycastle:prov:1.51.0.0' + compile 'org.json:json:20141113' + compile 'commons-io:commons-io:2.4' + compile 'net.sourceforge.argparse4j:argparse4j:0.5.0' +} + +jar { + baseName = 'textsecure-cli' + version = '0.0.1' + manifest { + attributes 'Main-Class': 'cli.Main' + } +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..c97a8bdb9088d370da7e88784a7a093b971aa23a GIT binary patch literal 51018 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

!ZEHpm+1dHJ=ON*&7AKm=FZEuky+kzQ95XO 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 TY# \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >&- +APP_HOME="`pwd -P`" +cd "$SAVED" >&- + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules +function splitJvmOpts() { + JVM_OPTS=("$@") +} +eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS +JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME" + +exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 00000000..aec99730 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,90 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windowz variants + +if not "%OS%" == "Windows_NT" goto win9xME_args +if "%@eval[2+2]" == "4" goto 4NT_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* +goto execute + +:4NT_args +@rem Get arguments from the 4NT Shell from JP Software +set CMD_LINE_ARGS=%$ + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 00000000..a2c308d3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,18 @@ +/* + * This settings file was auto generated by the Gradle buildInit task + * + * The settings file is used to specify which projects to include in your build. + * In a single project build this file can be empty or even removed. + * + * Detailed information about configuring a multi-project build in Gradle can be found + * in the user guide at http://gradle.org/docs/2.2.1/userguide/multi_project_builds.html + */ + +/* +// To declare projects as part of a multi-project build use the 'include' method +include 'shared' +include 'api' +include 'services:webservice' +*/ + +rootProject.name = 'textsecure-cli' diff --git a/src/main/java/cli/Base64.java b/src/main/java/cli/Base64.java new file mode 100644 index 00000000..061932ab --- /dev/null +++ b/src/main/java/cli/Base64.java @@ -0,0 +1,2135 @@ +package cli; + +/** + *

Encodes and decodes to and from Base64 notation.

+ *

Homepage: http://iharder.net/base64.

+ *

+ *

Example:

+ *

+ * String encoded = Base64.encode( myByteArray ); + *
+ * byte[] myByteArray = Base64.decode( encoded ); + *

+ *

The options parameter, which appears in a few places, is used to pass + * several pieces of information to the encoder. In the "higher level" methods such as + * encodeBytes( bytes, options ) the options parameter can be used to indicate such + * things as first gzipping the bytes before encoding them, not inserting linefeeds, + * and encoding using the URL-safe and Ordered dialects.

+ *

+ *

Note, according to RFC3548, + * Section 2.1, implementations should not add line feeds unless explicitly told + * to do so. I've got Base64 set to this behavior now, although earlier versions + * broke lines by default.

+ *

+ *

The constants defined in Base64 can be OR-ed together to combine options, so you + * might make a call like this:

+ *

+ * String encoded = Base64.encodeBytes( mybytes, Base64.GZIP | Base64.DO_BREAK_LINES ); + *

to compress the data before encoding it and then making the output have newline characters.

+ *

Also...

+ * String encoded = Base64.encodeBytes( crazyString.getBytes() ); + *

+ *

+ *

+ *

+ * Change Log: + *

+ *
    + *
  • v2.3.4 - Fixed bug when working with gzipped streams whereby flushing + * the Base64.OutputStream closed the Base64 encoding (by padding with equals + * signs) too soon. Also added an option to suppress the automatic decoding + * of gzipped streams. Also added experimental support for specifying a + * class loader when using the + * {@link #decodeToObject(java.lang.String, int, java.lang.ClassLoader)} + * method.
  • + *
  • v2.3.3 - Changed default char encoding to US-ASCII which reduces the internal Java + * footprint with its CharEncoders and so forth. Fixed some javadocs that were + * inconsistent. Removed imports and specified things like java.io.IOException + * explicitly inline.
  • + *
  • v2.3.2 - Reduced memory footprint! Finally refined the "guessing" of how big the + * final encoded data will be so that the code doesn't have to create two output + * arrays: an oversized initial one and then a final, exact-sized one. Big win + * when using the {@link #encodeBytesToBytes(byte[])} family of methods (and not + * using the gzip options which uses a different mechanism with streams and stuff).
  • + *
  • v2.3.1 - Added {@link #encodeBytesToBytes(byte[], int, int, int)} and some + * similar helper methods to be more efficient with memory by not returning a + * String but just a byte array.
  • + *
  • v2.3 - This is not a drop-in replacement! This is two years of comments + * and bug fixes queued up and finally executed. Thanks to everyone who sent + * me stuff, and I'm sorry I wasn't able to distribute your fixes to everyone else. + * Much bad coding was cleaned up including throwing exceptions where necessary + * instead of returning null values or something similar. Here are some changes + * that may affect you: + *
      + *
    • Does not break lines, by default. This is to keep in compliance with + * RFC3548.
    • + *
    • Throws exceptions instead of returning null values. Because some operations + * (especially those that may permit the GZIP option) use IO streams, there + * is a possiblity of an java.io.IOException being thrown. After some discussion and + * thought, I've changed the behavior of the methods to throw java.io.IOExceptions + * rather than return null if ever there's an error. I think this is more + * appropriate, though it will require some changes to your code. Sorry, + * it should have been done this way to begin with.
    • + *
    • Removed all references to System.out, System.err, and the like. + * Shame on me. All I can say is sorry they were ever there.
    • + *
    • Throws NullPointerExceptions and IllegalArgumentExceptions as needed + * such as when passed arrays are null or offsets are invalid.
    • + *
    • Cleaned up as much javadoc as I could to avoid any javadoc warnings. + * This was especially annoying before for people who were thorough in their + * own projects and then had gobs of javadoc warnings on this file.
    • + *
    + *
  • v2.2.1 - Fixed bug using URL_SAFE and ORDERED encodings. Fixed bug + * when using very small files (~< 40 bytes).
  • + *
  • v2.2 - Added some helper methods for encoding/decoding directly from + * one file to the next. Also added a main() method to support command line + * encoding/decoding from one file to the next. Also added these Base64 dialects: + *
      + *
    1. The default is RFC3548 format.
    2. + *
    3. Calling Base64.setFormat(Base64.BASE64_FORMAT.URLSAFE_FORMAT) generates + * URL and file name friendly format as described in Section 4 of RFC3548. + * http://www.faqs.org/rfcs/rfc3548.html
    4. + *
    5. Calling Base64.setFormat(Base64.BASE64_FORMAT.ORDERED_FORMAT) generates + * URL and file name friendly format that preserves lexical ordering as described + * in http://www.faqs.org/qa/rfcc-1940.html
    6. + *
    + * Special thanks to Jim Kellerman at http://www.powerset.com/ + * for contributing the new Base64 dialects. + *
  • + *

    + *

  • v2.1 - Cleaned up javadoc comments and unused variables and methods. Added + * some convenience methods for reading and writing to and from files.
  • + *
  • v2.0.2 - Now specifies UTF-8 encoding in places where the code fails on systems + * with other encodings (like EBCDIC).
  • + *
  • v2.0.1 - Fixed an error when decoding a single byte, that is, when the + * encoded data was a single byte.
  • + *
  • v2.0 - I got rid of methods that used booleans to set options. + * Now everything is more consolidated and cleaner. The code now detects + * when data that's being decoded is gzip-compressed and will decompress it + * automatically. Generally things are cleaner. You'll probably have to + * change some method calls that you were making to support the new + * options format (ints that you "OR" together).
  • + *
  • v1.5.1 - Fixed bug when decompressing and decoding to a + * byte[] using decode( String s, boolean gzipCompressed ). + * Added the ability to "suspend" encoding in the Output Stream so + * you can turn on and off the encoding if you need to embed base64 + * data in an otherwise "normal" stream (like an XML file).
  • + *
  • v1.5 - Output stream pases on flush() command but doesn't do anything itself. + * This helps when using GZIP streams. + * Added the ability to GZip-compress objects before encoding them.
  • + *
  • v1.4 - Added helper methods to read/write files.
  • + *
  • v1.3.6 - Fixed OutputStream.flush() so that 'position' is reset.
  • + *
  • v1.3.5 - Added flag to turn on and off line breaks. Fixed bug in input stream + * where last buffer being read, if not completely full, was not returned.
  • + *
  • v1.3.4 - Fixed when "improperly padded stream" error was thrown at the wrong time.
  • + *
  • v1.3.3 - Fixed I/O streams which were totally messed up.
  • + *
+ *

+ *

+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *

+ * + * @author Robert Harder + * @author rob@iharder.net + * @version 2.3.3 + */ +public class Base64 { + +/* ******** P U B L I C F I E L D S ******** */ + + + /** + * No options specified. Value is zero. + */ + public final static int NO_OPTIONS = 0; + + /** + * Specify encoding in first bit. Value is one. + */ + public final static int ENCODE = 1; + + + /** + * Specify decoding in first bit. Value is zero. + */ + public final static int DECODE = 0; + + + /** + * Specify that data should be gzip-compressed in second bit. Value is two. + */ + public final static int GZIP = 2; + + /** + * Specify that gzipped data should not be automatically gunzipped. + */ + public final static int DONT_GUNZIP = 4; + + + /** + * Do break lines when encoding. Value is 8. + */ + public final static int DO_BREAK_LINES = 8; + + /** + * Encode using Base64-like encoding that is URL- and Filename-safe as described + * in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * It is important to note that data encoded this way is not officially valid Base64, + * or at the very least should not be called Base64 without also specifying that is + * was encoded using the URL- and Filename-safe dialect. + */ + public final static int URL_SAFE = 16; + + + /** + * Encode using the special "ordered" dialect of Base64 described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + public final static int ORDERED = 32; + + +/* ******** P R I V A T E F I E L D S ******** */ + + + /** + * Maximum line length (76) of Base64 output. + */ + private final static int MAX_LINE_LENGTH = 76; + + + /** + * The equals sign (=) as a byte. + */ + private final static byte EQUALS_SIGN = (byte) '='; + + + /** + * The new line character (\n) as a byte. + */ + private final static byte NEW_LINE = (byte) '\n'; + + + /** + * Preferred encoding. + */ + private final static String PREFERRED_ENCODING = "US-ASCII"; + + + private final static byte WHITE_SPACE_ENC = -5; // Indicates white space in encoding + private final static byte EQUALS_SIGN_ENC = -1; // Indicates equals sign in encoding + + +/* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */ + + /** + * The 64 valid Base64 values. + */ + /* Host platform me be something funny like EBCDIC, so we hardcode these values. */ + private final static byte[] _STANDARD_ALPHABET = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/' + }; + + + /** + * Translates a Base64 value to either its 6-bit reconstruction value + * or a negative number indicating some other meaning. + */ + private final static byte[] _STANDARD_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + 62, // Plus sign at decimal 43 + -9, -9, -9, // Decimal 44 - 46 + 63, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, -9, -9, // Decimal 91 - 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + +/* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */ + + /** + * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: + * http://www.faqs.org/rfcs/rfc3548.html. + * Notice that the last two bytes become "hyphen" and "underscore" instead of "plus" and "slash." + */ + private final static byte[] _URL_SAFE_ALPHABET = { + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z', + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', + (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_' + }; + + /** + * Used in decoding URL- and Filename-safe dialects of Base64. + */ + private final static byte[] _URL_SAFE_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 62, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N' + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 63, // Underscore at decimal 95 + -9, // Decimal 96 + 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm' + 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + + +/* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */ + + /** + * I don't get the point of this technique, but someone requested it, + * and it is described here: + * http://www.faqs.org/qa/rfcc-1940.html. + */ + private final static byte[] _ORDERED_ALPHABET = { + (byte) '-', + (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', + (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', + (byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', + (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N', + (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', + (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', + (byte) '_', + (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', + (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', + (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', + (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z' + }; + + /** + * Used in decoding the "ordered" dialect of Base64. + */ + private final static byte[] _ORDERED_DECODABET = { + -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 0 - 8 + -5, -5, // Whitespace: Tab and Linefeed + -9, -9, // Decimal 11 - 12 + -5, // Whitespace: Carriage Return + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26 + -9, -9, -9, -9, -9, // Decimal 27 - 31 + -5, // Whitespace: Space + -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42 + -9, // Plus sign at decimal 43 + -9, // Decimal 44 + 0, // Minus sign at decimal 45 + -9, // Decimal 46 + -9, // Slash at decimal 47 + 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine + -9, -9, -9, // Decimal 58 - 60 + -1, // Equals sign at decimal 61 + -9, -9, -9, // Decimal 62 - 64 + 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M' + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z' + -9, -9, -9, -9, // Decimal 91 - 94 + 37, // Underscore at decimal 95 + -9, // Decimal 96 + 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm' + 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z' + -9, -9, -9, -9 // Decimal 123 - 126 + /*,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243 + -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255 */ + }; + + +/* ******** D E T E R M I N E W H I C H A L H A B E T ******** */ + + + /** + * Returns one of the _SOMETHING_ALPHABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URLSAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getAlphabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_ALPHABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_ALPHABET; + } else { + return _STANDARD_ALPHABET; + } + } // end getAlphabet + + + /** + * Returns one of the _SOMETHING_DECODABET byte arrays depending on + * the options specified. + * It's possible, though silly, to specify ORDERED and URL_SAFE + * in which case one of them will be picked, though there is + * no guarantee as to which one will be picked. + */ + private final static byte[] getDecodabet(int options) { + if ((options & URL_SAFE) == URL_SAFE) { + return _URL_SAFE_DECODABET; + } else if ((options & ORDERED) == ORDERED) { + return _ORDERED_DECODABET; + } else { + return _STANDARD_DECODABET; + } + } // end getAlphabet + + + /** + * Defeats instantiation. + */ + private Base64() { + } + + + public static int getEncodedLengthWithoutPadding(int unencodedLength) { + int remainderBytes = unencodedLength % 3; + int paddingBytes = 0; + + if (remainderBytes != 0) + paddingBytes = 3 - remainderBytes; + + return (((int) ((unencodedLength + 2) / 3)) * 4) - paddingBytes; + } + + public static int getEncodedBytesForTarget(int targetSize) { + return ((int) (targetSize * 3)) / 4; + } + + +/* ******** E N C O D I N G M E T H O D S ******** */ + + + /** + * Encodes up to the first three bytes of array threeBytes + * and returns a four-byte array in Base64 notation. + * The actual number of significant bytes in your array is + * given by numSigBytes. + * The array threeBytes needs only be as big as + * numSigBytes. + * Code can reuse a byte array by passing a four-byte array as b4. + * + * @param b4 A reusable byte array to reduce array instantiation + * @param threeBytes the array to convert + * @param numSigBytes the number of significant bytes in your array + * @return four byte array in Base64 notation. + * @since 1.5.1 + */ + private static byte[] encode3to4(byte[] b4, byte[] threeBytes, int numSigBytes, int options) { + encode3to4(threeBytes, 0, numSigBytes, b4, 0, options); + return b4; + } // end encode3to4 + + + /** + *

Encodes up to three bytes of the array source + * and writes the resulting four Base64 bytes to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 3 for + * the source array or destOffset + 4 for + * the destination array. + * The actual number of significant bytes in your array is + * given by numSigBytes.

+ *

This is the lowest level of the encoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param numSigBytes the number of significant bytes in your array + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @return the destination array + * @since 1.3 + */ + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset, int options) { + + byte[] ALPHABET = getAlphabet(options); + + // 1 2 3 + // 01234567890123456789012345678901 Bit position + // --------000000001111111122222222 Array position from threeBytes + // --------| || || || | Six bit groups to index ALPHABET + // >>18 >>12 >> 6 >> 0 Right shift necessary + // 0x3f 0x3f 0x3f Additional AND + + // Create buffer with zero-padding if there are only one or two + // significant bytes passed in the array. + // We have to shift left 24 in order to flush out the 1's that appear + // when Java treats a value as negative that is cast from a byte to an int. + int inBuff = (numSigBytes > 0 ? ((source[srcOffset] << 24) >>> 8) : 0) + | (numSigBytes > 1 ? ((source[srcOffset + 1] << 24) >>> 16) : 0) + | (numSigBytes > 2 ? ((source[srcOffset + 2] << 24) >>> 24) : 0); + + switch (numSigBytes) { + case 3: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = ALPHABET[(inBuff) & 0x3f]; + return destination; + + case 2: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = ALPHABET[(inBuff >>> 6) & 0x3f]; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + case 1: + destination[destOffset] = ALPHABET[(inBuff >>> 18)]; + destination[destOffset + 1] = ALPHABET[(inBuff >>> 12) & 0x3f]; + destination[destOffset + 2] = EQUALS_SIGN; + destination[destOffset + 3] = EQUALS_SIGN; + return destination; + + default: + return destination; + } // end switch + } // end encode3to4 + + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded ByteBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode(java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while (raw.hasRemaining()) { + int rem = Math.min(3, raw.remaining()); + raw.get(raw3, 0, rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + encoded.put(enc4); + } // end input remaining + } + + + /** + * Performs Base64 encoding on the raw ByteBuffer, + * writing it to the encoded CharBuffer. + * This is an experimental feature. Currently it does not + * pass along any options (such as {@link #DO_BREAK_LINES} + * or {@link #GZIP}. + * + * @param raw input buffer + * @param encoded output buffer + * @since 2.3 + */ + public static void encode(java.nio.ByteBuffer raw, java.nio.CharBuffer encoded) { + byte[] raw3 = new byte[3]; + byte[] enc4 = new byte[4]; + + while (raw.hasRemaining()) { + int rem = Math.min(3, raw.remaining()); + raw.get(raw3, 0, rem); + Base64.encode3to4(enc4, raw3, rem, Base64.NO_OPTIONS); + for (int i = 0; i < 4; i++) { + encoded.put((char) (enc4[i] & 0xFF)); + } + } // end input remaining + } + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + *

+ *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ *

+ * The object is not GZip-compressed before being encoded. + * + * @param serializableObject The object to encode + * @return The Base64-encoded object + * @throws java.io.IOException if there is an error + * @throws NullPointerException if serializedObject is null + * @since 1.4 + */ + public static String encodeObject(java.io.Serializable serializableObject) + throws java.io.IOException { + return encodeObject(serializableObject, NO_OPTIONS); + } // end encodeObject + + + /** + * Serializes an object and returns the Base64-encoded + * version of that serialized object. + *

+ *

As of v 2.3, if the object + * cannot be serialized or there is another error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ *

+ * The object is not GZip-compressed before being encoded. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     * 
+ *

+ * Example: encodeObject( myObj, Base64.GZIP ) or + *

+ * Example: encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES ) + * + * @param serializableObject The object to encode + * @param options Specified options + * @return The Base64-encoded object + * @throws java.io.IOException if there is an error + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeObject(java.io.Serializable serializableObject, int options) + throws java.io.IOException { + + if (serializableObject == null) { + throw new NullPointerException("Cannot serialize a null object."); + } // end if: null + + // Streams + java.io.ByteArrayOutputStream baos = null; + java.io.OutputStream b64os = null; + java.util.zip.GZIPOutputStream gzos = null; + java.io.ObjectOutputStream oos = null; + + + try { + // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + if ((options & GZIP) != 0) { + // Gzip + gzos = new java.util.zip.GZIPOutputStream(b64os); + oos = new java.io.ObjectOutputStream(gzos); + } else { + // Not gzipped + oos = new java.io.ObjectOutputStream(b64os); + } + oos.writeObject(serializableObject); + } // end try + catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try { + oos.close(); + } catch (Exception e) { + } + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + // Return value according to relevant encoding. + try { + return new String(baos.toByteArray(), PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + // Fall back to some Java default + return new String(baos.toByteArray()); + } // end catch + + } // end encode + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + * + * @param source The data to convert + * @return The data in Base64-encoded form + * @throws NullPointerException if source array is null + * @since 1.4 + */ + public static String encodeBytes(byte[] source) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, 0, source.length, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + public static String encodeBytesWithoutPadding(byte[] source, int offset, int length) { + String encoded = null; + + try { + encoded = encodeBytes(source, offset, length, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } + + assert encoded != null; + + if (encoded.charAt(encoded.length() - 2) == '=') + return encoded.substring(0, encoded.length() - 2); + else if (encoded.charAt(encoded.length() - 1) == '=') + return encoded.substring(0, encoded.length() - 1); + else return encoded; + + } + + public static String encodeBytesWithoutPadding(byte[] source) { + return encodeBytesWithoutPadding(source, 0, source.length); + } + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + *

+ *

+ *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int options) throws java.io.IOException { + return encodeBytes(source, 0, source.length, options); + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + * Does not GZip-compress data. + *

+ *

As of v 2.3, if there is an error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @return The Base64-encoded data as a String + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @since 1.4 + */ + public static String encodeBytes(byte[] source, int off, int len) { + // Since we're not going to have the GZIP encoding turned on, + // we're not going to have an java.io.IOException thrown, so + // we should not force the user to have to catch it. + String encoded = null; + try { + encoded = encodeBytes(source, off, len, NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : ex.getMessage(); + } // end catch + assert encoded != null; + return encoded; + } // end encodeBytes + + + /** + * Encodes a byte array into Base64 notation. + *

+ * Example options:

+     *   GZIP: gzip-compresses object before encoding it.
+     *   DO_BREAK_LINES: break lines at 76 characters
+     *     Note: Technically, this makes your encoding non-compliant.
+     * 
+ *

+ * Example: encodeBytes( myData, Base64.GZIP ) or + *

+ * Example: encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES ) + *

+ *

+ *

As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public static String encodeBytes(byte[] source, int off, int len, int options) throws java.io.IOException { + byte[] encoded = encodeBytesToBytes(source, off, len, options); + + // Return value according to relevant encoding. + try { + return new String(encoded, PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uue) { + return new String(encoded); + } // end catch + + } // end encodeBytes + + + /** + * Similar to {@link #encodeBytes(byte[])} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * @param source The data to convert + * @return The Base64-encoded data as a byte[] (of ASCII characters) + * @throws NullPointerException if source array is null + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source) { + byte[] encoded = null; + try { + encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return encoded; + } + + + /** + * Similar to {@link #encodeBytes(byte[], int, int, int)} but returns + * a byte array instead of instantiating a String. This is more efficient + * if you're working with I/O streams and have large data sets to encode. + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert + * @param options Specified options + * @return The Base64-encoded data as a String + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @throws IllegalArgumentException if source array, offset, or length are invalid + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @since 2.3.1 + */ + public static byte[] encodeBytesToBytes(byte[] source, int off, int len, int options) throws java.io.IOException { + + if (source == null) { + throw new NullPointerException("Cannot serialize a null array."); + } // end if: null + + if (off < 0) { + throw new IllegalArgumentException("Cannot have negative offset: " + off); + } // end if: off < 0 + + if (len < 0) { + throw new IllegalArgumentException("Cannot have length offset: " + len); + } // end if: len < 0 + + if (off + len > source.length) { + throw new IllegalArgumentException( + String.format("Cannot have offset of %d and length of %d with array of length %d", off, len, source.length)); + } // end if: off < 0 + + + // Compress? + if ((options & GZIP) != 0) { + java.io.ByteArrayOutputStream baos = null; + java.util.zip.GZIPOutputStream gzos = null; + Base64.OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new Base64.OutputStream(baos, ENCODE | options); + gzos = new java.util.zip.GZIPOutputStream(b64os); + + gzos.write(source, off, len); + gzos.close(); + } // end try + catch (java.io.IOException e) { + // Catch it and then throw it immediately so that + // the finally{} block is called for cleanup. + throw e; + } // end catch + finally { + try { + gzos.close(); + } catch (Exception e) { + } + try { + b64os.close(); + } catch (Exception e) { + } + try { + baos.close(); + } catch (Exception e) { + } + } // end finally + + return baos.toByteArray(); + } // end if: compress + + // Else, don't compress. Better not to use streams at all then. + else { + boolean breakLines = (options & DO_BREAK_LINES) > 0; + + //int len43 = len * 4 / 3; + //byte[] outBuff = new byte[ ( len43 ) // Main 4:3 + // + ( (len % 3) > 0 ? 4 : 0 ) // Account for padding + // + (breakLines ? ( len43 / MAX_LINE_LENGTH ) : 0) ]; // New lines + // Try to determine more precisely how big the array needs to be. + // If we get it right, we don't have to do an array copy, and + // we save a bunch of memory. + int encLen = (len / 3) * 4 + (len % 3 > 0 ? 4 : 0); // Bytes needed for actual encoding + if (breakLines) { + encLen += encLen / MAX_LINE_LENGTH; // Plus extra newline characters + } + byte[] outBuff = new byte[encLen]; + + + int d = 0; + int e = 0; + int len2 = len - 2; + int lineLength = 0; + for (; d < len2; d += 3, e += 4) { + encode3to4(source, d + off, 3, outBuff, e, options); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + outBuff[e + 4] = NEW_LINE; + e++; + lineLength = 0; + } // end if: end of line + } // en dfor: each piece of array + + if (d < len) { + encode3to4(source, d + off, len - d, outBuff, e, options); + e += 4; + } // end if: some padding needed + + + // Only resize array if we didn't guess it right. + if (e < outBuff.length - 1) { + byte[] finalOut = new byte[e]; + System.arraycopy(outBuff, 0, finalOut, 0, e); + //System.err.println("Having to resize array from " + outBuff.length + " to " + e ); + return finalOut; + } else { + //System.err.println("No need to resize array."); + return outBuff; + } + + } // end else: don't compress + + } // end encodeBytesToBytes + + + + + +/* ******** D E C O D I N G M E T H O D S ******** */ + + + /** + * Decodes four bytes from array source + * and writes the resulting bytes (up to three of them) + * to destination. + * The source and destination arrays can be manipulated + * anywhere along their length by specifying + * srcOffset and destOffset. + * This method does not check to make sure your arrays + * are large enough to accomodate srcOffset + 4 for + * the source array or destOffset + 3 for + * the destination array. + * This method returns the actual number of bytes that + * were converted from the Base64 encoding. + *

This is the lowest level of the decoding methods with + * all possible parameters.

+ * + * @param source the array to convert + * @param srcOffset the index where conversion begins + * @param destination the array to hold the conversion + * @param destOffset the index where output will be put + * @param options alphabet type is pulled from this (standard, url-safe, ordered) + * @return the number of decoded bytes converted + * @throws NullPointerException if source or destination arrays are null + * @throws IllegalArgumentException if srcOffset or destOffset are invalid + * or there is not enough room in the array. + * @since 1.3 + */ + private static int decode4to3( + byte[] source, int srcOffset, + byte[] destination, int destOffset, int options) { + + // Lots of error checking and exception throwing + if (source == null) { + throw new NullPointerException("Source array was null."); + } // end if + if (destination == null) { + throw new NullPointerException("Destination array was null."); + } // end if + if (srcOffset < 0 || srcOffset + 3 >= source.length) { + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and still process four bytes.", source.length, srcOffset)); + } // end if + if (destOffset < 0 || destOffset + 2 >= destination.length) { + throw new IllegalArgumentException(String.format( + "Destination array with length %d cannot have offset of %d and still store three bytes.", destination.length, destOffset)); + } // end if + + + byte[] DECODABET = getDecodabet(options); + + // Example: Dk== + if (source[srcOffset + 2] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1] ] << 24 ) >>> 12 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12); + + destination[destOffset] = (byte) (outBuff >>> 16); + return 1; + } + + // Example: DkL= + else if (source[srcOffset + 3] == EQUALS_SIGN) { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6); + + destination[destOffset] = (byte) (outBuff >>> 16); + destination[destOffset + 1] = (byte) (outBuff >>> 8); + return 2; + } + + // Example: DkLE + else { + // Two ways to do the same thing. Don't know which way I like best. + //int outBuff = ( ( DECODABET[ source[ srcOffset ] ] << 24 ) >>> 6 ) + // | ( ( DECODABET[ source[ srcOffset + 1 ] ] << 24 ) >>> 12 ) + // | ( ( DECODABET[ source[ srcOffset + 2 ] ] << 24 ) >>> 18 ) + // | ( ( DECODABET[ source[ srcOffset + 3 ] ] << 24 ) >>> 24 ); + int outBuff = ((DECODABET[source[srcOffset]] & 0xFF) << 18) + | ((DECODABET[source[srcOffset + 1]] & 0xFF) << 12) + | ((DECODABET[source[srcOffset + 2]] & 0xFF) << 6) + | ((DECODABET[source[srcOffset + 3]] & 0xFF)); + + + destination[destOffset] = (byte) (outBuff >> 16); + destination[destOffset + 1] = (byte) (outBuff >> 8); + destination[destOffset + 2] = (byte) (outBuff); + + return 3; + } + } // end decodeToBytes + + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @return decoded data + * @since 2.3.1 + */ + public static byte[] decode(byte[] source) { + byte[] decoded = null; + try { + decoded = decode(source, 0, source.length, Base64.NO_OPTIONS); + } catch (java.io.IOException ex) { + assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage(); + } + return decoded; + } + + + /** + * Low-level access to decoding ASCII characters in + * the form of a byte array. Ignores GUNZIP option, if + * it's set. This is not generally a recommended method, + * although it is used internally as part of the decoding process. + * Special case: if len = 0, an empty array is returned. Still, + * if you need more speed and reduced memory footprint (and aren't + * gzipping), consider this method. + * + * @param source The Base64 encoded data + * @param off The offset of where to begin decoding + * @param len The length of characters to decode + * @param options Can specify options such as alphabet type to use + * @return decoded data + * @throws java.io.IOException If bogus characters exist in source data + * @since 1.3 + */ + public static byte[] decode(byte[] source, int off, int len, int options) + throws java.io.IOException { + + // Lots of error checking and exception throwing + if (source == null) { + throw new NullPointerException("Cannot decode null source array."); + } // end if + if (off < 0 || off + len > source.length) { + throw new IllegalArgumentException(String.format( + "Source array with length %d cannot have offset of %d and process %d bytes.", source.length, off, len)); + } // end if + + if (len == 0) { + return new byte[0]; + } else if (len < 4) { + throw new IllegalArgumentException( + "Base64-encoded string must have at least four characters, but length specified was " + len); + } // end if + + byte[] DECODABET = getDecodabet(options); + + int len34 = len * 3 / 4; // Estimate on array size + byte[] outBuff = new byte[len34]; // Upper limit on size of output + int outBuffPosn = 0; // Keep track of where we're writing + + byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space + int b4Posn = 0; // Keep track of four byte input buffer + int i = 0; // Source array counter + byte sbiCrop = 0; // Low seven bits (ASCII) of input + byte sbiDecode = 0; // Special value from DECODABET + + for (i = off; i < off + len; i++) { // Loop through source + + sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits + sbiDecode = DECODABET[sbiCrop]; // Special value + + // White space, Equals sign, or legit Base64 character + // Note the values such as -5 and -9 in the + // DECODABETs at the top of the file. + if (sbiDecode >= WHITE_SPACE_ENC) { + if (sbiDecode >= EQUALS_SIGN_ENC) { + b4[b4Posn++] = sbiCrop; // Save non-whitespace + if (b4Posn > 3) { // Time to decode? + outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); + b4Posn = 0; + + // If that was the equals sign, break out of 'for' loop + if (sbiCrop == EQUALS_SIGN) { + break; + } // end if: equals sign + } // end if: quartet built + } // end if: equals sign or better + } // end if: white space, equals sign or better + else { + // There's a bad input character in the Base64 stream. + throw new java.io.IOException(String.format( + "Bad Base64 input character '%c' in array position %d", source[i], i)); + } // end else: + } // each input character + + byte[] out = new byte[outBuffPosn]; + System.arraycopy(outBuff, 0, out, 0, outBuffPosn); + return out; + } // end decode + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @return the decoded data + * @throws java.io.IOException If there is a problem + * @since 1.4 + */ + public static byte[] decode(String s) throws java.io.IOException { + return decode(s, NO_OPTIONS); + } + + + public static byte[] decodeWithoutPadding(String source) throws java.io.IOException { + int padding = source.length() % 4; + + if (padding == 1) source = source + "="; + else if (padding == 2) source = source + "=="; + else if (padding == 3) source = source + "="; + + return decode(source); + } + + + /** + * Decodes data from Base64 notation, automatically + * detecting gzip-compressed data and decompressing it. + * + * @param s the string to decode + * @param options encode options such as URL_SAFE + * @return the decoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if s is null + * @since 1.4 + */ + public static byte[] decode(String s, int options) throws java.io.IOException { + + if (s == null) { + throw new NullPointerException("Input string was null."); + } // end if + + byte[] bytes; + try { + bytes = s.getBytes(PREFERRED_ENCODING); + } // end try + catch (java.io.UnsupportedEncodingException uee) { + bytes = s.getBytes(); + } // end catch + // + + // Decode + bytes = decode(bytes, 0, bytes.length, options); + + // Check to see if it's gzip-compressed + // GZIP Magic Two-Byte Number: 0x8b1f (35615) + boolean dontGunzip = (options & DONT_GUNZIP) != 0; + if ((bytes != null) && (bytes.length >= 4) && (!dontGunzip)) { + + int head = ((int) bytes[0] & 0xff) | ((bytes[1] << 8) & 0xff00); + if (java.util.zip.GZIPInputStream.GZIP_MAGIC == head) { + java.io.ByteArrayInputStream bais = null; + java.util.zip.GZIPInputStream gzis = null; + java.io.ByteArrayOutputStream baos = null; + byte[] buffer = new byte[2048]; + int length = 0; + + try { + baos = new java.io.ByteArrayOutputStream(); + bais = new java.io.ByteArrayInputStream(bytes); + gzis = new java.util.zip.GZIPInputStream(bais); + + while ((length = gzis.read(buffer)) >= 0) { + baos.write(buffer, 0, length); + } // end while: reading input + + // No error? Get new bytes. + bytes = baos.toByteArray(); + + } // end try + catch (java.io.IOException e) { + e.printStackTrace(); + // Just return originally-decoded bytes + } // end catch + finally { + try { + baos.close(); + } catch (Exception e) { + } + try { + gzis.close(); + } catch (Exception e) { + } + try { + bais.close(); + } catch (Exception e) { + } + } // end finally + + } // end if: gzipped + } // end if: bytes.length >= 2 + + return bytes; + } // end decode + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * + * @param encodedObject The Base64 data to decode + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 1.5 + */ + public static Object decodeToObject(String encodedObject) + throws java.io.IOException, java.lang.ClassNotFoundException { + return decodeToObject(encodedObject, NO_OPTIONS, null); + } + + + /** + * Attempts to decode Base64 data and deserialize a Java + * Object within. Returns null if there was an error. + * If loader is not null, it will be the class loader + * used when deserializing. + * + * @param encodedObject The Base64 data to decode + * @param options Various parameters related to decoding + * @param loader Optional class loader to use in deserializing classes. + * @return The decoded and deserialized object + * @throws NullPointerException if encodedObject is null + * @throws java.io.IOException if there is a general error + * @throws ClassNotFoundException if the decoded object is of a + * class that cannot be found by the JVM + * @since 2.3.4 + */ + public static Object decodeToObject( + String encodedObject, int options, final ClassLoader loader) + throws java.io.IOException, java.lang.ClassNotFoundException { + + // Decode and gunzip if necessary + byte[] objBytes = decode(encodedObject, options); + + java.io.ByteArrayInputStream bais = null; + java.io.ObjectInputStream ois = null; + Object obj = null; + + try { + bais = new java.io.ByteArrayInputStream(objBytes); + + // If no custom class loader is provided, use Java's builtin OIS. + if (loader == null) { + ois = new java.io.ObjectInputStream(bais); + } // end if: no loader provided + + // Else make a customized object input stream that uses + // the provided class loader. + else { + ois = new java.io.ObjectInputStream(bais) { + @Override + public Class resolveClass(java.io.ObjectStreamClass streamClass) + throws java.io.IOException, ClassNotFoundException { + Class c = Class.forName(streamClass.getName(), false, loader); + if (c == null) { + return super.resolveClass(streamClass); + } else { + return c; // Class loader knows of this class. + } // end else: not null + } // end resolveClass + }; // end ois + } // end else: no custom class loader + + obj = ois.readObject(); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + catch (java.lang.ClassNotFoundException e) { + throw e; // Catch and throw in order to execute finally{} + } // end catch + finally { + try { + bais.close(); + } catch (Exception e) { + } + try { + ois.close(); + } catch (Exception e) { + } + } // end finally + + return obj; + } // end decodeObject + + + /** + * Convenience method for encoding data to a file. + *

+ *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToEncode byte array of data to encode in base64 form + * @param filename Filename for saving encoded data + * @throws java.io.IOException if there is an error + * @throws NullPointerException if dataToEncode is null + * @since 2.1 + */ + public static void encodeToFile(byte[] dataToEncode, String filename) + throws java.io.IOException { + + if (dataToEncode == null) { + throw new NullPointerException("Data to encode was null."); + } // end iff + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.ENCODE); + bos.write(dataToEncode); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + } // end encodeToFile + + + /** + * Convenience method for decoding data to a file. + *

+ *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param dataToDecode Base64-encoded data as a string + * @param filename Filename for saving decoded data + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static void decodeToFile(String dataToDecode, String filename) + throws java.io.IOException { + + Base64.OutputStream bos = null; + try { + bos = new Base64.OutputStream( + new java.io.FileOutputStream(filename), Base64.DECODE); + bos.write(dataToDecode.getBytes(PREFERRED_ENCODING)); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and throw to execute finally{} block + } // end catch: java.io.IOException + finally { + try { + bos.close(); + } catch (Exception e) { + } + } // end finally + + } // end decodeToFile + + + /** + * Convenience method for reading a base64-encoded + * file and decoding it. + *

+ *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading encoded data + * @return decoded byte array + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static byte[] decodeFromFile(String filename) + throws java.io.IOException { + + byte[] decodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = null; + int length = 0; + int numBytes = 0; + + // Check for size of file + if (file.length() > Integer.MAX_VALUE) { + throw new java.io.IOException("File is too big for this convenience method (" + file.length() + " bytes)."); + } // end if: file too big for int index + buffer = new byte[(int) file.length()]; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.DECODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } // end while + + // Save in a variable to return + decodedData = new byte[length]; + System.arraycopy(buffer, 0, decodedData, 0, length); + + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return decodedData; + } // end decodeFromFile + + + /** + * Convenience method for reading a binary file + * and base64-encoding it. + *

+ *

As of v 2.3, if there is a error, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned false, but + * in retrospect that's a pretty poor way to handle it.

+ * + * @param filename Filename for reading binary data + * @return base64-encoded string + * @throws java.io.IOException if there is an error + * @since 2.1 + */ + public static String encodeFromFile(String filename) + throws java.io.IOException { + + String encodedData = null; + Base64.InputStream bis = null; + try { + // Set up some useful variables + java.io.File file = new java.io.File(filename); + byte[] buffer = new byte[Math.max((int) (file.length() * 1.4), 40)]; // Need max() for math on small files (v2.2.1) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new Base64.InputStream( + new java.io.BufferedInputStream( + new java.io.FileInputStream(file)), Base64.ENCODE); + + // Read until done + while ((numBytes = bis.read(buffer, length, 4096)) >= 0) { + length += numBytes; + } // end while + + // Save in a variable to return + encodedData = new String(buffer, 0, length, Base64.PREFERRED_ENCODING); + + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch: java.io.IOException + finally { + try { + bis.close(); + } catch (Exception e) { + } + } // end finally + + return encodedData; + } // end encodeFromFile + + /** + * Reads infile and encodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void encodeFileToFile(String infile, String outfile) + throws java.io.IOException { + + String encoded = Base64.encodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new java.io.BufferedOutputStream( + new java.io.FileOutputStream(outfile)); + out.write(encoded.getBytes("US-ASCII")); // Strict, 7-bit output. + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { + out.close(); + } catch (Exception ex) { + } + } // end finally + } // end encodeFileToFile + + + /** + * Reads infile and decodes it to outfile. + * + * @param infile Input file + * @param outfile Output file + * @throws java.io.IOException if there is an error + * @since 2.2 + */ + public static void decodeFileToFile(String infile, String outfile) + throws java.io.IOException { + + byte[] decoded = Base64.decodeFromFile(infile); + java.io.OutputStream out = null; + try { + out = new java.io.BufferedOutputStream( + new java.io.FileOutputStream(outfile)); + out.write(decoded); + } // end try + catch (java.io.IOException e) { + throw e; // Catch and release to execute finally{} + } // end catch + finally { + try { + out.close(); + } catch (Exception ex) { + } + } // end finally + } // end decodeFileToFile + + + /* ******** I N N E R C L A S S I N P U T S T R E A M ******** */ + + + /** + * A {@link Base64.InputStream} will read data from another + * java.io.InputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class InputStream extends java.io.FilterInputStream { + + private boolean encode; // Encoding or decoding + private int position; // Current position in the buffer + private byte[] buffer; // Small buffer holding converted data + private int bufferLength; // Length of buffer (3 or 4) + private int numSigBytes; // Number of meaningful bytes in the buffer + private int lineLength; + private boolean breakLines; // Break lines at less than 80 characters + private int options; // Record options used to create the stream. + private byte[] decodabet; // Local copies to avoid extra method calls + + + /** + * Constructs a {@link Base64.InputStream} in DECODE mode. + * + * @param in the java.io.InputStream from which to read data. + * @since 1.3 + */ + public InputStream(java.io.InputStream in) { + this(in, DECODE); + } // end constructor + + + /** + * Constructs a {@link Base64.InputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.InputStream( in, Base64.DECODE ) + * + * @param in the java.io.InputStream from which to read data. + * @param options Specified options + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 2.0 + */ + public InputStream(java.io.InputStream in, int options) { + + super(in); + this.options = options; // Record for later + this.breakLines = (options & DO_BREAK_LINES) > 0; + this.encode = (options & ENCODE) > 0; + this.bufferLength = encode ? 4 : 3; + this.buffer = new byte[bufferLength]; + this.position = -1; + this.lineLength = 0; + this.decodabet = getDecodabet(options); + } // end constructor + + /** + * Reads enough of the input stream to convert + * to/from Base64 and returns the next byte. + * + * @return next byte + * @since 1.3 + */ + @Override + public int read() throws java.io.IOException { + + // Do we need to get data? + if (position < 0) { + if (encode) { + byte[] b3 = new byte[3]; + int numBinaryBytes = 0; + for (int i = 0; i < 3; i++) { + int b = in.read(); + + // If end of stream, b is -1. + if (b >= 0) { + b3[i] = (byte) b; + numBinaryBytes++; + } else { + break; // out of for loop + } // end else: end of stream + + } // end for: each needed input byte + + if (numBinaryBytes > 0) { + encode3to4(b3, 0, numBinaryBytes, buffer, 0, options); + position = 0; + numSigBytes = 4; + } // end if: got data + else { + return -1; // Must be end of stream + } // end else + } // end if: encoding + + // Else decoding + else { + byte[] b4 = new byte[4]; + int i = 0; + for (i = 0; i < 4; i++) { + // Read four "meaningful" bytes: + int b = 0; + do { + b = in.read(); + } + while (b >= 0 && decodabet[b & 0x7f] <= WHITE_SPACE_ENC); + + if (b < 0) { + break; // Reads a -1 if end of stream + } // end if: end of stream + + b4[i] = (byte) b; + } // end for: each needed input byte + + if (i == 4) { + numSigBytes = decode4to3(b4, 0, buffer, 0, options); + position = 0; + } // end if: got four characters + else if (i == 0) { + return -1; + } // end else if: also padded correctly + else { + // Must have broken out from above. + throw new java.io.IOException("Improperly padded Base64 input."); + } // end + + } // end else: decode + } // end else: get data + + // Got data? + if (position >= 0) { + // End of relevant data? + if ( /*!encode &&*/ position >= numSigBytes) { + return -1; + } // end if: got data + + if (encode && breakLines && lineLength >= MAX_LINE_LENGTH) { + lineLength = 0; + return '\n'; + } // end if + else { + lineLength++; // This isn't important when decoding + // but throwing an extra "if" seems + // just as wasteful. + + int b = buffer[position++]; + + if (position >= bufferLength) { + position = -1; + } // end if: end + + return b & 0xFF; // This is how you "cast" a byte that's + // intended to be unsigned. + } // end else + } // end if: position >= 0 + + // Else error + else { + throw new java.io.IOException("Error in Base64 code reading stream."); + } // end else + } // end read + + + /** + * Calls {@link #read()} repeatedly until the end of stream + * is reached or len bytes are read. + * Returns number of bytes read into array or -1 if + * end of stream is encountered. + * + * @param dest array to hold values + * @param off offset for array + * @param len max number of bytes to read into array + * @return bytes read into array or -1 if end of stream is encountered. + * @since 1.3 + */ + @Override + public int read(byte[] dest, int off, int len) + throws java.io.IOException { + int i; + int b; + for (i = 0; i < len; i++) { + b = read(); + + if (b >= 0) { + dest[off + i] = (byte) b; + } else if (i == 0) { + return -1; + } else { + break; // Out of 'for' loop + } // Out of 'for' loop + } // end for: each byte read + return i; + } // end read + + } // end inner class InputStream + + + + + + + /* ******** I N N E R C L A S S O U T P U T S T R E A M ******** */ + + + /** + * A {@link Base64.OutputStream} will write data to another + * java.io.OutputStream, given in the constructor, + * and encode/decode to/from Base64 notation on the fly. + * + * @see Base64 + * @since 1.3 + */ + public static class OutputStream extends java.io.FilterOutputStream { + + private boolean encode; + private int position; + private byte[] buffer; + private int bufferLength; + private int lineLength; + private boolean breakLines; + private byte[] b4; // Scratch used in a few places + private boolean suspendEncoding; + private int options; // Record for later + private byte[] decodabet; // Local copies to avoid extra method calls + + /** + * Constructs a {@link Base64.OutputStream} in ENCODE mode. + * + * @param out the java.io.OutputStream to which data will be written. + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out) { + this(out, ENCODE); + } // end constructor + + + /** + * Constructs a {@link Base64.OutputStream} in + * either ENCODE or DECODE mode. + *

+ * Valid options:

+         *   ENCODE or DECODE: Encode or Decode as data is read.
+         *   DO_BREAK_LINES: don't break lines at 76 characters
+         *     (only meaningful when encoding)
+         * 
+ *

+ * Example: new Base64.OutputStream( out, Base64.ENCODE ) + * + * @param out the java.io.OutputStream to which data will be written. + * @param options Specified options. + * @see Base64#ENCODE + * @see Base64#DECODE + * @see Base64#DO_BREAK_LINES + * @since 1.3 + */ + public OutputStream(java.io.OutputStream out, int options) { + super(out); + this.breakLines = (options & DO_BREAK_LINES) != 0; + this.encode = (options & ENCODE) != 0; + this.bufferLength = encode ? 3 : 4; + this.buffer = new byte[bufferLength]; + this.position = 0; + this.lineLength = 0; + this.suspendEncoding = false; + this.b4 = new byte[4]; + this.options = options; + this.decodabet = getDecodabet(options); + } // end constructor + + + /** + * Writes the byte to the output stream after + * converting to/from Base64 notation. + * When encoding, bytes are buffered three + * at a time before the output stream actually + * gets a write() call. + * When decoding, bytes are buffered four + * at a time. + * + * @param theByte the byte to write + * @since 1.3 + */ + @Override + public void write(int theByte) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theByte); + return; + } // end if: supsended + + // Encode? + if (encode) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to encode. + + this.out.write(encode3to4(b4, buffer, bufferLength, options)); + + lineLength += 4; + if (breakLines && lineLength >= MAX_LINE_LENGTH) { + this.out.write(NEW_LINE); + lineLength = 0; + } // end if: end of line + + position = 0; + } // end if: enough to output + } // end if: encoding + + // Else, Decoding + else { + // Meaningful Base64 character? + if (decodabet[theByte & 0x7f] > WHITE_SPACE_ENC) { + buffer[position++] = (byte) theByte; + if (position >= bufferLength) { // Enough to output. + + int len = Base64.decode4to3(buffer, 0, b4, 0, options); + out.write(b4, 0, len); + position = 0; + } // end if: enough to output + } // end if: meaningful base64 character + else if (decodabet[theByte & 0x7f] != WHITE_SPACE_ENC) { + throw new java.io.IOException("Invalid character in Base64 data."); + } // end else: not white space either + } // end else: decoding + } // end write + + + /** + * Calls {@link #write(int)} repeatedly until len + * bytes are written. + * + * @param theBytes array from which to read bytes + * @param off offset for array + * @param len max number of bytes to read into array + * @since 1.3 + */ + @Override + public void write(byte[] theBytes, int off, int len) + throws java.io.IOException { + // Encoding suspended? + if (suspendEncoding) { + this.out.write(theBytes, off, len); + return; + } // end if: supsended + + for (int i = 0; i < len; i++) { + write(theBytes[off + i]); + } // end for: each byte written + + } // end write + + + /** + * Method added by PHIL. [Thanks, PHIL. -Rob] + * This pads the buffer without closing the stream. + * + * @throws java.io.IOException if there's an error. + */ + public void flushBase64() throws java.io.IOException { + if (position > 0) { + if (encode) { + out.write(encode3to4(b4, buffer, position, options)); + position = 0; + } // end if: encoding + else { + throw new java.io.IOException("Base64 input not properly padded."); + } // end else: decoding + } // end if: buffer partially full + + } // end flush + + + /** + * Flushes and closes (I think, in the superclass) the stream. + * + * @since 1.3 + */ + @Override + public void close() throws java.io.IOException { + // 1. Ensure that pending characters are written + flushBase64(); + + // 2. Actually close the stream + // Base class both flushes and closes. + super.close(); + + buffer = null; + out = null; + } // end close + + + /** + * Suspends encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @throws java.io.IOException if there's an error flushing + * @since 1.5.1 + */ + public void suspendEncoding() throws java.io.IOException { + flushBase64(); + this.suspendEncoding = true; + } // end suspendEncoding + + + /** + * Resumes encoding of the stream. + * May be helpful if you need to embed a piece of + * base64-encoded data in a stream. + * + * @since 1.5.1 + */ + public void resumeEncoding() { + this.suspendEncoding = false; + } // end resumeEncoding + + + } // end inner class OutputStream + + +} // end class Base64 diff --git a/src/main/java/cli/JsonAxolotlStore.java b/src/main/java/cli/JsonAxolotlStore.java new file mode 100644 index 00000000..d260b6b0 --- /dev/null +++ b/src/main/java/cli/JsonAxolotlStore.java @@ -0,0 +1,135 @@ +package cli; + +import org.json.JSONObject; +import org.whispersystems.libaxolotl.*; +import org.whispersystems.libaxolotl.state.AxolotlStore; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; + +import java.io.IOException; +import java.util.List; + +public class JsonAxolotlStore implements AxolotlStore { + private final JsonPreKeyStore preKeyStore; + private final JsonSessionStore sessionStore; + private final JsonSignedPreKeyStore signedPreKeyStore; + + private final JsonIdentityKeyStore identityKeyStore; + + public JsonAxolotlStore(JSONObject jsonAxolotl) throws IOException, InvalidKeyException { + this.preKeyStore = new JsonPreKeyStore(jsonAxolotl.getJSONArray("preKeys")); + this.sessionStore = new JsonSessionStore(jsonAxolotl.getJSONArray("sessionStore")); + this.signedPreKeyStore = new JsonSignedPreKeyStore(jsonAxolotl.getJSONArray("signedPreKeyStore")); + this.identityKeyStore = new JsonIdentityKeyStore(jsonAxolotl.getJSONObject("identityKeyStore")); + } + + public JsonAxolotlStore(IdentityKeyPair identityKeyPair, int registrationId) { + preKeyStore = new JsonPreKeyStore(); + sessionStore = new JsonSessionStore(); + signedPreKeyStore = new JsonSignedPreKeyStore(); + this.identityKeyStore = new JsonIdentityKeyStore(identityKeyPair, registrationId); + } + + public JSONObject getJson() { + return new JSONObject().put("preKeys", preKeyStore.getJson()) + .put("sessionStore", sessionStore.getJson()) + .put("signedPreKeyStore", signedPreKeyStore.getJson()) + .put("identityKeyStore", identityKeyStore.getJson()); + } + + @Override + public IdentityKeyPair getIdentityKeyPair() { + return identityKeyStore.getIdentityKeyPair(); + } + + @Override + public int getLocalRegistrationId() { + return identityKeyStore.getLocalRegistrationId(); + } + + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + identityKeyStore.saveIdentity(name, identityKey); + } + + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + return identityKeyStore.isTrustedIdentity(name, identityKey); + } + + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + return preKeyStore.loadPreKey(preKeyId); + } + + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + preKeyStore.storePreKey(preKeyId, record); + } + + @Override + public boolean containsPreKey(int preKeyId) { + return preKeyStore.containsPreKey(preKeyId); + } + + @Override + public void removePreKey(int preKeyId) { + preKeyStore.removePreKey(preKeyId); + } + + @Override + public SessionRecord loadSession(AxolotlAddress address) { + return sessionStore.loadSession(address); + } + + @Override + public List getSubDeviceSessions(String name) { + return sessionStore.getSubDeviceSessions(name); + } + + @Override + public void storeSession(AxolotlAddress address, SessionRecord record) { + sessionStore.storeSession(address, record); + } + + @Override + public boolean containsSession(AxolotlAddress address) { + return sessionStore.containsSession(address); + } + + @Override + public void deleteSession(AxolotlAddress address) { + sessionStore.deleteSession(address); + } + + @Override + public void deleteAllSessions(String name) { + sessionStore.deleteAllSessions(name); + } + + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + return signedPreKeyStore.loadSignedPreKey(signedPreKeyId); + } + + @Override + public List loadSignedPreKeys() { + return signedPreKeyStore.loadSignedPreKeys(); + } + + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + signedPreKeyStore.storeSignedPreKey(signedPreKeyId, record); + } + + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return signedPreKeyStore.containsSignedPreKey(signedPreKeyId); + } + + @Override + public void removeSignedPreKey(int signedPreKeyId) { + signedPreKeyStore.removeSignedPreKey(signedPreKeyId); + } +} diff --git a/src/main/java/cli/JsonIdentityKeyStore.java b/src/main/java/cli/JsonIdentityKeyStore.java new file mode 100644 index 00000000..75e6ba06 --- /dev/null +++ b/src/main/java/cli/JsonIdentityKeyStore.java @@ -0,0 +1,74 @@ +package cli; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.whispersystems.libaxolotl.IdentityKey; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.state.IdentityKeyStore; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class JsonIdentityKeyStore implements IdentityKeyStore { + + private final Map trustedKeys = new HashMap<>(); + + private final IdentityKeyPair identityKeyPair; + private final int localRegistrationId; + + public JsonIdentityKeyStore(JSONObject jsonAxolotl) throws IOException, InvalidKeyException { + localRegistrationId = jsonAxolotl.getInt("registrationId"); + identityKeyPair = new IdentityKeyPair(Base64.decode(jsonAxolotl.getString("identityKey"))); + + JSONArray list = jsonAxolotl.getJSONArray("trustedKeys"); + for (int i = 0; i < list.length(); i++) { + JSONObject k = list.getJSONObject(i); + try { + trustedKeys.put(k.getString("name"), new IdentityKey(Base64.decode(k.getString("identityKey")), 0)); + } catch (InvalidKeyException | IOException e) { + System.out.println("Error while decoding key for: " + k.getString("name")); + } + } + } + + public JsonIdentityKeyStore(IdentityKeyPair identityKeyPair, int localRegistrationId) { + this.identityKeyPair = identityKeyPair; + this.localRegistrationId = localRegistrationId; + } + + public JSONObject getJson() { + JSONArray list = new JSONArray(); + for (String name : trustedKeys.keySet()) { + list.put(new JSONObject().put("name", name).put("identityKey", Base64.encodeBytes(trustedKeys.get(name).serialize()))); + } + + JSONObject result = new JSONObject(); + result.put("registrationId", localRegistrationId); + result.put("identityKey", Base64.encodeBytes(identityKeyPair.serialize())); + result.put("trustedKeys", list); + return result; + } + + @Override + public IdentityKeyPair getIdentityKeyPair() { + return identityKeyPair; + } + + @Override + public int getLocalRegistrationId() { + return localRegistrationId; + } + + @Override + public void saveIdentity(String name, IdentityKey identityKey) { + trustedKeys.put(name, identityKey); + } + + @Override + public boolean isTrustedIdentity(String name, IdentityKey identityKey) { + IdentityKey trusted = trustedKeys.get(name); + return (trusted == null || trusted.equals(identityKey)); + } +} diff --git a/src/main/java/cli/JsonPreKeyStore.java b/src/main/java/cli/JsonPreKeyStore.java new file mode 100644 index 00000000..63e7ea77 --- /dev/null +++ b/src/main/java/cli/JsonPreKeyStore.java @@ -0,0 +1,67 @@ +package cli; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.PreKeyStore; + +import java.io.IOException; +import java.util.HashMap; +import java.util.Map; + +public class JsonPreKeyStore implements PreKeyStore { + + private final Map store = new HashMap<>(); + + public JsonPreKeyStore() { + + } + + public JsonPreKeyStore(JSONArray list) throws IOException { + for (int i = 0; i < list.length(); i++) { + JSONObject k = list.getJSONObject(i); + try { + store.put(k.getInt("id"), Base64.decode(k.getString("record"))); + } catch (IOException e) { + System.out.println("Error while decoding prekey for: " + k.getString("name")); + } + } + } + + public JSONArray getJson() { + JSONArray result = new JSONArray(); + for (Integer id : store.keySet()) { + result.put(new JSONObject().put("id", id.toString()).put("record", Base64.encodeBytes(store.get(id)))); + } + return result; + } + + @Override + public PreKeyRecord loadPreKey(int preKeyId) throws InvalidKeyIdException { + try { + if (!store.containsKey(preKeyId)) { + throw new InvalidKeyIdException("No such prekeyrecord!"); + } + + return new PreKeyRecord(store.get(preKeyId)); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public void storePreKey(int preKeyId, PreKeyRecord record) { + store.put(preKeyId, record.serialize()); + } + + @Override + public boolean containsPreKey(int preKeyId) { + return store.containsKey(preKeyId); + } + + @Override + public void removePreKey(int preKeyId) { + store.remove(preKeyId); + } +} diff --git a/src/main/java/cli/JsonSessionStore.java b/src/main/java/cli/JsonSessionStore.java new file mode 100644 index 00000000..b070334b --- /dev/null +++ b/src/main/java/cli/JsonSessionStore.java @@ -0,0 +1,94 @@ +package cli; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.whispersystems.libaxolotl.AxolotlAddress; +import org.whispersystems.libaxolotl.state.SessionRecord; +import org.whispersystems.libaxolotl.state.SessionStore; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class JsonSessionStore implements SessionStore { + + private Map sessions = new HashMap<>(); + + public JsonSessionStore() { + + } + + public JsonSessionStore(JSONArray list) throws IOException { + for (int i = 0; i < list.length(); i++) { + JSONObject k = list.getJSONObject(i); + try { + sessions.put(new AxolotlAddress(k.getString("name"), k.getInt("deviceId")), Base64.decode(k.getString("record"))); + } catch (IOException e) { + System.out.println("Error while decoding prekey for: " + k.getString("name")); + } + } + } + + public JSONArray getJson() { + JSONArray result = new JSONArray(); + for (AxolotlAddress address : sessions.keySet()) { + result.put(new JSONObject().put("name", address.getName()). + put("deviceId", address.getDeviceId()). + put("record", Base64.encodeBytes(sessions.get(address)))); + } + return result; + } + + @Override + public synchronized SessionRecord loadSession(AxolotlAddress remoteAddress) { + try { + if (containsSession(remoteAddress)) { + return new SessionRecord(sessions.get(remoteAddress)); + } else { + return new SessionRecord(); + } + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public synchronized List getSubDeviceSessions(String name) { + List deviceIds = new LinkedList<>(); + + for (AxolotlAddress key : sessions.keySet()) { + if (key.getName().equals(name) && + key.getDeviceId() != 1) { + deviceIds.add(key.getDeviceId()); + } + } + + return deviceIds; + } + + @Override + public synchronized void storeSession(AxolotlAddress address, SessionRecord record) { + sessions.put(address, record.serialize()); + } + + @Override + public synchronized boolean containsSession(AxolotlAddress address) { + return sessions.containsKey(address); + } + + @Override + public synchronized void deleteSession(AxolotlAddress address) { + sessions.remove(address); + } + + @Override + public synchronized void deleteAllSessions(String name) { + for (AxolotlAddress key : sessions.keySet()) { + if (key.getName().equals(name)) { + sessions.remove(key); + } + } + } +} diff --git a/src/main/java/cli/JsonSignedPreKeyStore.java b/src/main/java/cli/JsonSignedPreKeyStore.java new file mode 100644 index 00000000..000a0aa4 --- /dev/null +++ b/src/main/java/cli/JsonSignedPreKeyStore.java @@ -0,0 +1,84 @@ +package cli; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.whispersystems.libaxolotl.InvalidKeyIdException; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyStore; + +import java.io.IOException; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +public class JsonSignedPreKeyStore implements SignedPreKeyStore { + + private final Map store = new HashMap<>(); + + public JsonSignedPreKeyStore() { + + } + + public JsonSignedPreKeyStore(JSONArray list) throws IOException { + for (int i = 0; i < list.length(); i++) { + JSONObject k = list.getJSONObject(i); + try { + store.put(k.getInt("id"), Base64.decode(k.getString("record"))); + } catch (IOException e) { + System.out.println("Error while decoding prekey for: " + k.getString("name")); + } + } + } + + public JSONArray getJson() { + JSONArray result = new JSONArray(); + for (Integer id : store.keySet()) { + result.put(new JSONObject().put("id", id.toString()).put("record", store.get(id))); + } + return result; + } + + @Override + public SignedPreKeyRecord loadSignedPreKey(int signedPreKeyId) throws InvalidKeyIdException { + try { + if (!store.containsKey(signedPreKeyId)) { + throw new InvalidKeyIdException("No such signedprekeyrecord! " + signedPreKeyId); + } + + return new SignedPreKeyRecord(store.get(signedPreKeyId)); + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public List loadSignedPreKeys() { + try { + List results = new LinkedList<>(); + + for (byte[] serialized : store.values()) { + results.add(new SignedPreKeyRecord(serialized)); + } + + return results; + } catch (IOException e) { + throw new AssertionError(e); + } + } + + @Override + public void storeSignedPreKey(int signedPreKeyId, SignedPreKeyRecord record) { + store.put(signedPreKeyId, record.serialize()); + } + + @Override + public boolean containsSignedPreKey(int signedPreKeyId) { + return store.containsKey(signedPreKeyId); + } + + @Override + public void removeSignedPreKey(int signedPreKeyId) { + store.remove(signedPreKeyId); + } +} diff --git a/src/main/java/cli/Main.java b/src/main/java/cli/Main.java new file mode 100644 index 00000000..34f07a55 --- /dev/null +++ b/src/main/java/cli/Main.java @@ -0,0 +1,148 @@ +/** + * Copyright (C) 2015 AsamK + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cli; + +import net.sourceforge.argparse4j.ArgumentParsers; +import net.sourceforge.argparse4j.inf.*; +import org.apache.commons.io.IOUtils; +import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.textsecure.api.TextSecureMessageSender; +import org.whispersystems.textsecure.api.crypto.UntrustedIdentityException; +import org.whispersystems.textsecure.api.messages.TextSecureMessage; +import org.whispersystems.textsecure.api.push.TextSecureAddress; + +import java.io.IOException; +import java.security.Security; + +public class Main { + + public static void main(String[] args) { + // Workaround for BKS truststore + Security.insertProviderAt(new org.spongycastle.jce.provider.BouncyCastleProvider(), 1); + + ArgumentParser parser = ArgumentParsers.newArgumentParser("textsecure-cli") + .defaultHelp(true) + .description("Commandline interface for TextSecure."); + Subparsers subparsers = parser.addSubparsers() + .title("subcommands") + .dest("command") + .description("valid subcommands") + .help("additional help"); + Subparser parserRegister = subparsers.addParser("register"); + Subparser parserVerify = subparsers.addParser("verify"); + parserVerify.addArgument("verificationCode") + .help("The verification code you received via sms."); + Subparser parserSend = subparsers.addParser("send"); + parserSend.addArgument("recipient") + .help("Specify the recipients' phone number.") + .nargs("*"); + parserSend.addArgument("-m", "--message") + .help("Specify the message, if missing standard input is used."); + Subparser parserReceive = subparsers.addParser("receive"); + parser.addArgument("-u", "--username") + .required(true) + .help("Specify your phone number, that will be used for verification."); + Namespace ns = null; + try { + ns = parser.parseArgs(args); + } catch (ArgumentParserException e) { + parser.handleError(e); + System.exit(1); + } + + String username = ns.getString("username"); + Manager m = new Manager(username); + if (m.userExists()) { + try { + m.load(); + } catch (Exception e) { + System.out.println("Loading file error: " + e.getMessage()); + System.exit(2); + } + } + switch (ns.getString("command")) { + case "register": + if (!m.userHasKeys()) { + m.createNewIdentity(); + } + try { + m.register(); + } catch (IOException e) { + System.out.println("Request verify error: " + e.getMessage()); + System.exit(3); + } + break; + case "verify": + if (!m.userHasKeys()) { + System.out.println("User has no keys, first call register."); + System.exit(1); + } + if (m.isRegistered()) { + System.out.println("User registration is already verified"); + System.exit(1); + } + try { + m.verifyAccount(ns.getString("verificationCode")); + } catch (IOException e) { + System.out.println("Verify error: " + e.getMessage()); + System.exit(3); + } + break; + case "send": + if (!m.isRegistered()) { + System.out.println("User is not registered."); + System.exit(1); + } + TextSecureMessageSender messageSender = m.getMessageSender(); + String messageText = ns.getString("message"); + if (messageText == null) { + try { + messageText = IOUtils.toString(System.in); + } catch (IOException e) { + System.out.println("Failed to read message from stdin: " + e.getMessage()); + System.exit(1); + } + } + TextSecureMessage message = TextSecureMessage.newBuilder().withBody(messageText).build(); + for (String recipient : ns.getList("recipient")) { + try { + messageSender.sendMessage(new TextSecureAddress(recipient), message); + } catch (UntrustedIdentityException | IOException e) { + System.out.println("Send message: " + e.getMessage()); + } + } + break; + case "receive": + if (!m.isRegistered()) { + System.out.println("User is not registered."); + System.exit(1); + } + try { + message = m.receiveMessage(); + if (message == null) { + System.exit(0); + } else { + System.out.println("Received message: " + message.getBody().get()); + } + } catch (IOException | InvalidVersionException e) { + System.out.println("Receive message: " + e.getMessage()); + } + break; + } + m.save(); + } +} diff --git a/src/main/java/cli/Manager.java b/src/main/java/cli/Manager.java new file mode 100644 index 00000000..97070c97 --- /dev/null +++ b/src/main/java/cli/Manager.java @@ -0,0 +1,181 @@ +/** + * Copyright (C) 2015 AsamK + *

+ * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + *

+ * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + *

+ * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package cli; + +import org.apache.commons.io.IOUtils; +import org.json.JSONObject; +import org.whispersystems.libaxolotl.IdentityKeyPair; +import org.whispersystems.libaxolotl.InvalidKeyException; +import org.whispersystems.libaxolotl.InvalidVersionException; +import org.whispersystems.libaxolotl.state.PreKeyRecord; +import org.whispersystems.libaxolotl.state.SignedPreKeyRecord; +import org.whispersystems.libaxolotl.util.KeyHelper; +import org.whispersystems.libaxolotl.util.guava.Optional; +import org.whispersystems.textsecure.api.TextSecureAccountManager; +import org.whispersystems.textsecure.api.TextSecureMessagePipe; +import org.whispersystems.textsecure.api.TextSecureMessageReceiver; +import org.whispersystems.textsecure.api.TextSecureMessageSender; +import org.whispersystems.textsecure.api.crypto.TextSecureCipher; +import org.whispersystems.textsecure.api.messages.TextSecureEnvelope; +import org.whispersystems.textsecure.api.messages.TextSecureMessage; +import org.whispersystems.textsecure.api.push.TrustStore; + +import java.io.*; +import java.nio.file.Path; +import java.util.List; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +public class Manager { + private final static String URL = "https://textsecure-service.whispersystems.org"; + private final static TrustStore TRUST_STORE = new WhisperTrustStore(); + + private final static String settingsPath = System.getProperty("user.home") + "/.config/textsecure"; + + private String username; + private String password; + private String signalingKey; + + private boolean registered = false; + + private JsonAxolotlStore axolotlStore; + TextSecureAccountManager accountManager; + + public Manager(String username) { + this.username = username; + } + + private String getFileName() { + String path = settingsPath + "/data"; + new File(path).mkdirs(); + return path + "/" + username; + } + + public boolean userExists() { + File f = new File(getFileName()); + if (!f.exists() || f.isDirectory()) { + return false; + } + return true; + } + + public boolean userHasKeys() { + return axolotlStore != null; + } + + public void load() throws IOException, InvalidKeyException { + JSONObject in = new JSONObject(IOUtils.toString(new FileInputStream(getFileName()))); + username = in.getString("username"); + password = in.getString("password"); + signalingKey = in.getString("signalingKey"); + axolotlStore = new JsonAxolotlStore(in.getJSONObject("axolotlStore")); + registered = in.getBoolean("registered"); + accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password); + } + + public void save() { + String out = new JSONObject().put("username", username) + .put("password", password) + .put("signalingKey", signalingKey) + .put("axolotlStore", axolotlStore.getJson()) + .put("registered", registered).toString(); + try { + OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(getFileName())); + writer.write(out); + writer.flush(); + writer.close(); + } catch (Exception e) { + System.out.println("Saving file error: " + e.getMessage()); + return; + } + } + + public void createNewIdentity() { + IdentityKeyPair identityKey = KeyHelper.generateIdentityKeyPair(); + int registrationId = KeyHelper.generateRegistrationId(false); + axolotlStore = new JsonAxolotlStore(identityKey, registrationId); + registered = false; + } + + public boolean isRegistered() { + return registered; + } + + public void register() throws IOException { + password = Util.getSecret(18); + + accountManager = new TextSecureAccountManager(URL, TRUST_STORE, username, password); + + accountManager.requestSmsVerificationCode(); + registered = false; + } + + public void verifyAccount(String verificationCode) throws IOException { + verificationCode = verificationCode.replace("-", ""); + signalingKey = Util.getSecret(52); + accountManager.verifyAccount(verificationCode, signalingKey, false, axolotlStore.getLocalRegistrationId()); + + //accountManager.setGcmId(Optional.of(GoogleCloudMessaging.getInstance(this).register(REGISTRATION_ID))); + registered = true; + int start = 0; + List oneTimePreKeys = KeyHelper.generatePreKeys(start, 100); + PreKeyRecord lastResortKey = KeyHelper.generateLastResortPreKey(); + int signedPreKeyId = 0; + SignedPreKeyRecord signedPreKeyRecord; + try { + signedPreKeyRecord = KeyHelper.generateSignedPreKey(axolotlStore.getIdentityKeyPair(), signedPreKeyId); + } catch (InvalidKeyException e) { + // Should really not happen + System.out.println("invalid key"); + return; + } + accountManager.setPreKeys(axolotlStore.getIdentityKeyPair().getPublicKey(), lastResortKey, signedPreKeyRecord, oneTimePreKeys); + } + + public TextSecureMessageSender getMessageSender() { + return new TextSecureMessageSender(URL, TRUST_STORE, username, password, + axolotlStore, Optional.absent()); + } + + public TextSecureMessage receiveMessage() throws IOException, InvalidVersionException { + TextSecureMessageReceiver messageReceiver = new TextSecureMessageReceiver(URL, TRUST_STORE, username, password, signalingKey); + TextSecureMessagePipe messagePipe = null; + + try { + messagePipe = messageReceiver.createMessagePipe(); + + TextSecureEnvelope envelope; + try { + envelope = messagePipe.read(5, TimeUnit.SECONDS); + } catch (TimeoutException e) { + return null; + } + TextSecureCipher cipher = new TextSecureCipher(axolotlStore); + TextSecureMessage message = null; + try { + message = cipher.decrypt(envelope); + } catch (Exception e) { + // TODO handle all exceptions + e.printStackTrace(); + } + return message; + } finally { + if (messagePipe != null) + messagePipe.shutdown(); + } + } +} diff --git a/src/main/java/cli/Util.java b/src/main/java/cli/Util.java new file mode 100644 index 00000000..36921de2 --- /dev/null +++ b/src/main/java/cli/Util.java @@ -0,0 +1,25 @@ +package cli; + +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +public class Util { + public static String getSecret(int size) { + byte[] secret = getSecretBytes(size); + return Base64.encodeBytes(secret); + } + + public static byte[] getSecretBytes(int size) { + byte[] secret = new byte[size]; + getSecureRandom().nextBytes(secret); + return secret; + } + + public static SecureRandom getSecureRandom() { + try { + return SecureRandom.getInstance("SHA1PRNG"); + } catch (NoSuchAlgorithmException e) { + throw new AssertionError(e); + } + } +} diff --git a/src/main/java/cli/WhisperTrustStore.java b/src/main/java/cli/WhisperTrustStore.java new file mode 100644 index 00000000..77770946 --- /dev/null +++ b/src/main/java/cli/WhisperTrustStore.java @@ -0,0 +1,20 @@ +package cli; + +import org.whispersystems.textsecure.api.push.TrustStore; + +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.InputStream; + +public class WhisperTrustStore implements TrustStore { + + @Override + public InputStream getKeyStoreInputStream() { + return cli.WhisperTrustStore.class.getResourceAsStream("whisper.store"); + } + + @Override + public String getKeyStorePassword() { + return "whisper"; + } +} diff --git a/src/main/resources/cli/whisper.store b/src/main/resources/cli/whisper.store new file mode 100644 index 0000000000000000000000000000000000000000..664ca9563ce30963c1481225f08a7e8438a917c2 GIT binary patch literal 1107 zcmZQzU|?imU=VpInV_H0#dv$!H|7tD`ioaY?=z@jU|`?Q$RJpfT2WG*np|3xs+*pi ztDBt2z`$s`cw_!b1`uG4&@(l#WME+aY|zB~-k^!;`T}MqMkXdshR$8OtG%9lPBP$S z$Ok~52w^9^MUq(S0bJR<%Dsd)l4pXwx!#r?PB$J(72_)s`)F!xuZ|FX8yi! z*Kc)CuW8*{FMh$7>~$@NO=J3*o`@RpRQ0|6cGlIY(_wpM%i7o}mwwE98NTMX#;>L= z^N%wzGak>;N;1g~nZP+S`z~*8L4gXx!oZ`=Q3n`4Ej#Dnp#-exM-o48^ zIo(5k6s(AO(tqWPjQ-Mjb1t+qF*7nSE)Fp8GmvFt4wdC&5n~Zy?3Vaw|Jzd1C9kz@ z-;`9NO8kyb$Y#W`|}K!gM;_>0*S!DD+U(oQd=*zOusR=S5@{Nwb;<3G^N(vzasL`C zb{(@+-*kSm(01LNGiqf&1)7{^N`I0+RpidP;3em-Rf??{ed_v0L>_G0dOUsk1HBjhEM!iBdvzfUeMcy~b+0PQK5Z2$lO literal 0 HcmV?d00001 -- 2.50.1