From ef1032ae26bb1c1254459b9d87658fb9ec6239d3 Mon Sep 17 00:00:00 2001 From: fly Date: Wed, 14 Jul 2021 16:31:33 +0800 Subject: [PATCH 1/5] fly build --- .gitignore | 15 + app/.gitignore | 1 + app/build.gradle | 48 + app/proguard-rules.pro | 21 + app/src/androidTest/AndroidManifest.xml | 12 + app/src/androidTest/res/drawable/icon.png | Bin 0 -> 3180 bytes app/src/androidTest/res/layout/main.xml | 12 + app/src/androidTest/res/values/strings.xml | 5 + app/src/main/AndroidManifest.xml | 53 + app/src/main/assets/fonts/fa-brands-400.ttf | Bin 0 -> 134572 bytes app/src/main/assets/fonts/fa-regular-400.ttf | Bin 0 -> 34092 bytes app/src/main/assets/fonts/fa-solid-900.ttf | Bin 0 -> 204580 bytes .../co/bitethebullet/android/token/About.java | 33 + .../android/token/PinChange.java | 135 ++ .../android/token/PinManager.java | 91 + .../android/token/PinRemove.java | 102 + .../android/token/QRCodeActivity.java | 58 + .../android/token/SettingActivity.java | 21 + .../android/token/SettingsFragment.java | 124 + .../bitethebullet/android/token/TokenAdd.java | 550 +++++ .../android/token/TokenCountDownView.java | 42 + .../android/token/TokenList.java | 681 ++++++ .../android/token/adapters/TokenAdapter.java | 162 ++ .../token/datalayer/TokenDbAdapter.java | 239 ++ .../token/dialogs/DeleteTokenDialog.java | 58 + .../dialogs/DeleteTokenPickerDialog.java | 72 + .../token/dialogs/PinDefintionDialog.java | 62 + .../token/parse/OtpAuthUriException.java | 24 + .../android/token/parse/UrlParser.java | 82 + .../android/token/tokens/HotpToken.java | 298 +++ .../android/token/tokens/IToken.java | 45 + .../android/token/tokens/ITokenMeta.java | 18 + .../android/token/tokens/IconSuggestor.java | 94 + .../android/token/tokens/TokenFactory.java | 85 + .../android/token/tokens/TokenHelper.java | 43 + .../android/token/tokens/TokenMetaData.java | 63 + .../android/token/tokens/TotpToken.java | 99 + .../android/token/util/Base32.java | 235 ++ .../android/token/util/Base64.java | 2068 +++++++++++++++++ .../android/token/util/FontManager.java | 15 + .../android/token/util/SeedConvertor.java | 61 + .../android/token/zxing/IntentIntegrator.java | 332 +++ .../android/token/zxing/IntentResult.java | 33 + app/src/main/res/drawable/add.png | Bin 0 -> 1613 bytes app/src/main/res/drawable/android50.png | Bin 0 -> 3074 bytes app/src/main/res/drawable/androidtoken.png | Bin 0 -> 5445 bytes .../res/drawable/baseline_add_black_24dp.png | Bin 0 -> 95 bytes app/src/main/res/drawable/icon.png | Bin 0 -> 3180 bytes app/src/main/res/drawable/xclock.png | Bin 0 -> 2499 bytes app/src/main/res/layout/about.xml | 46 + app/src/main/res/layout/activity_q_r_code.xml | 29 + app/src/main/res/layout/activity_setting.xml | 16 + app/src/main/res/layout/main.xml | 84 + app/src/main/res/layout/otpdialog.xml | 21 + app/src/main/res/layout/pinchange.xml | 65 + .../main/res/layout/pindefinitiondialog.xml | 17 + app/src/main/res/layout/pinremove.xml | 26 + app/src/main/res/layout/token_add.xml | 211 ++ app/src/main/res/layout/token_list_row.xml | 83 + app/src/main/res/menu/token_item_menu.xml | 11 + app/src/main/res/navigation/nav_graph.xml | 28 + app/src/main/res/values/arrays.xml | 44 + app/src/main/res/values/dimens.xml | 3 + app/src/main/res/values/strings.xml | 147 ++ app/src/main/res/values/styles.xml | 11 + app/src/main/res/xml/root_preferences.xml | 23 + .../android/token/HotpTokenTests.java | 67 + .../android/token/ParseUrlTests.java | 182 ++ .../android/token/SeedConvertorTests.java | 112 + .../android/token/TotpTokenTests.java | 104 + build.gradle | 24 + gradle.properties | 19 + gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 54329 bytes gradle/wrapper/gradle-wrapper.properties | 6 + gradlew | 172 ++ gradlew.bat | 84 + settings.gradle | 2 + 77 files changed, 7829 insertions(+) create mode 100644 .gitignore create mode 100644 app/.gitignore create mode 100644 app/build.gradle create mode 100644 app/proguard-rules.pro create mode 100644 app/src/androidTest/AndroidManifest.xml create mode 100644 app/src/androidTest/res/drawable/icon.png create mode 100644 app/src/androidTest/res/layout/main.xml create mode 100644 app/src/androidTest/res/values/strings.xml create mode 100644 app/src/main/AndroidManifest.xml create mode 100644 app/src/main/assets/fonts/fa-brands-400.ttf create mode 100644 app/src/main/assets/fonts/fa-regular-400.ttf create mode 100644 app/src/main/assets/fonts/fa-solid-900.ttf create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/About.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/PinChange.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/PinManager.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/PinRemove.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/QRCodeActivity.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/SettingActivity.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/SettingsFragment.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/TokenAdd.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/TokenCountDownView.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/TokenList.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/adapters/TokenAdapter.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/datalayer/TokenDbAdapter.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenDialog.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenPickerDialog.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/dialogs/PinDefintionDialog.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/parse/OtpAuthUriException.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/parse/UrlParser.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/HotpToken.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/IToken.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/ITokenMeta.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/IconSuggestor.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenFactory.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenHelper.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenMetaData.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/tokens/TotpToken.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/util/Base32.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/util/Base64.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/util/FontManager.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/util/SeedConvertor.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentIntegrator.java create mode 100644 app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentResult.java create mode 100644 app/src/main/res/drawable/add.png create mode 100644 app/src/main/res/drawable/android50.png create mode 100644 app/src/main/res/drawable/androidtoken.png create mode 100644 app/src/main/res/drawable/baseline_add_black_24dp.png create mode 100644 app/src/main/res/drawable/icon.png create mode 100644 app/src/main/res/drawable/xclock.png create mode 100644 app/src/main/res/layout/about.xml create mode 100644 app/src/main/res/layout/activity_q_r_code.xml create mode 100644 app/src/main/res/layout/activity_setting.xml create mode 100644 app/src/main/res/layout/main.xml create mode 100644 app/src/main/res/layout/otpdialog.xml create mode 100644 app/src/main/res/layout/pinchange.xml create mode 100644 app/src/main/res/layout/pindefinitiondialog.xml create mode 100644 app/src/main/res/layout/pinremove.xml create mode 100644 app/src/main/res/layout/token_add.xml create mode 100644 app/src/main/res/layout/token_list_row.xml create mode 100644 app/src/main/res/menu/token_item_menu.xml create mode 100644 app/src/main/res/navigation/nav_graph.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/values/dimens.xml create mode 100644 app/src/main/res/values/strings.xml create mode 100644 app/src/main/res/values/styles.xml create mode 100644 app/src/main/res/xml/root_preferences.xml create mode 100644 app/src/test/java/uk/co/bitethebullet/android/token/HotpTokenTests.java create mode 100644 app/src/test/java/uk/co/bitethebullet/android/token/ParseUrlTests.java create mode 100644 app/src/test/java/uk/co/bitethebullet/android/token/SeedConvertorTests.java create mode 100644 app/src/test/java/uk/co/bitethebullet/android/token/TotpTokenTests.java create mode 100644 build.gradle create mode 100644 gradle.properties create mode 100644 gradle/wrapper/gradle-wrapper.jar create mode 100644 gradle/wrapper/gradle-wrapper.properties create mode 100644 gradlew create mode 100644 gradlew.bat create mode 100644 settings.gradle diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..aa724b7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,15 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/app/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..d2d8189 --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,48 @@ +plugins { + id 'com.android.application' +} + +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + applicationId "com.fly.otp" + minSdkVersion 16 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + } + + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation "androidx.preference:preference:1.1.0" + implementation 'androidx.appcompat:appcompat:1.1.0' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'androidx.legacy:legacy-support-v4:1.0.0' + implementation 'com.google.android.material:material:1.1.0' + implementation 'androidx.biometric:biometric:1.0.1' + implementation 'androidx.navigation:navigation-fragment:2.3.0' + implementation 'androidx.navigation:navigation-ui:2.3.0' + implementation 'androidmads.library.qrgenearator:QRGenearator:1.0.4' + implementation 'com.google.zxing:core:3.3.2' + + testImplementation 'junit:junit:4.13' + testImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + testImplementation 'com.google.truth:truth:1.0.1' + +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/app/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/app/src/androidTest/AndroidManifest.xml b/app/src/androidTest/AndroidManifest.xml new file mode 100644 index 0000000..c355df4 --- /dev/null +++ b/app/src/androidTest/AndroidManifest.xml @@ -0,0 +1,12 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/androidTest/res/drawable/icon.png b/app/src/androidTest/res/drawable/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..75024841d327c4fbaefef7c8e9c8d0e892895f93 GIT binary patch literal 3180 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 S`hb-H0000 + + + diff --git a/app/src/androidTest/res/values/strings.xml b/app/src/androidTest/res/values/strings.xml new file mode 100644 index 0000000..ab55c80 --- /dev/null +++ b/app/src/androidTest/res/values/strings.xml @@ -0,0 +1,5 @@ + + + Hello World! + TokenListTest + diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..95e7464 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/assets/fonts/fa-brands-400.ttf b/app/src/main/assets/fonts/fa-brands-400.ttf new file mode 100644 index 0000000000000000000000000000000000000000..ef792f4244b8ca1d096741f5b43a74c1bd36df8c GIT binary patch literal 134572 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%0A!*q|pB=^x9NFeid%?3b(xc+iHsc8`yF_^%j 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

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.7 - Fixed subtle bug when base 64 input stream contained the + * value 01111111, which is an invalid base 64 character but should not + * throw an ArrayIndexOutOfBoundsException either. Led to discovery of + * mishandling (or potential for better handling) of other bad input + * characters. You should now get an IOException if you try decoding + * something that has bad characters in it.
  • + *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded + * string ended in the last column; the buffer was not properly shrunk and + * contained an extra (null) byte that made it into the string.
  • + *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size + * was wrong for files of size 31, 34, and 37 bytes.
  • + *
  • 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.7 + */ + +package uk.co.bitethebullet.android.token.util; + + + +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,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 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,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 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,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 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(){} + + + + +/* ******** 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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @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 + + + + /** + * 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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @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 + * @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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @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 + * @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 ){ + // If breaking lines and the last byte falls right at + // the line length (76 bytes per line), there will be + // one extra byte, and the array will need to be resized. + // Not too bad of an estimate on array size, I'd say. + 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 ) + throws java.io.IOException { + 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 sbiDecode = 0; // Special value from DECODABET + + for( i = off; i < off+len; i++ ) { // Loop through source + + sbiDecode = DECODABET[ source[i]&0xFF ]; + + // 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++ ] = source[i]; // 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( source[i] == 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 decimal %d in array position %d", ((int)source[i])&0xFF, 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 ); + } + + + + /** + * 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+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5) + 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/app/src/main/java/uk/co/bitethebullet/android/token/util/FontManager.java b/app/src/main/java/uk/co/bitethebullet/android/token/util/FontManager.java new file mode 100644 index 0000000..ee5c7ee --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/util/FontManager.java @@ -0,0 +1,15 @@ +package uk.co.bitethebullet.android.token.util; + +import android.content.Context; +import android.graphics.Typeface; + +public class FontManager { + public static final String ROOT = "fonts/", + FONTAWESOME = ROOT + "fa-regular-400.ttf", + FONTAWESOME_BRANDS = ROOT + "fa-brands-400.ttf", + FONTAWESOME_SOLID = ROOT + "fa-solid-900.ttf"; + + public static Typeface getTypeface(Context context, String font) { + return Typeface.createFromAsset(context.getAssets(), font); + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/util/SeedConvertor.java b/app/src/main/java/uk/co/bitethebullet/android/token/util/SeedConvertor.java new file mode 100644 index 0000000..8b9df84 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/util/SeedConvertor.java @@ -0,0 +1,61 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2011 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token.util; + +import java.io.IOException; +import uk.co.bitethebullet.android.token.tokens.HotpToken; + +public class SeedConvertor { + + public static final int HEX_FORMAT = 0; + public static final int BASE32_FORMAT = 1; + public static final int BASE64_FORMAT = 2; + + public static byte[] ConvertFromEncodingToBA(String input, int currentFormat) throws IOException{ + + if(currentFormat == 0){ + //hex + return HotpToken.stringToHex(input); + }else if(currentFormat == 1){ + //base 32 + Base32 base32 = new Base32(); + return base32.decodeBytes(input.toUpperCase()); + }else if(currentFormat == 2){ + //base64 + return Base64.decode(input); + }else + return null; + } + + public static String ConvertFromBA(byte[] input, int targetFormat){ + if(targetFormat == 0){ + //hex + return HotpToken.byteArrayToHexString(input); + }else if(targetFormat == 1){ + //base 32 + Base32 base32 = new Base32(); + return base32.encodeBytes(input); + }else if(targetFormat == 2){ + //base64 + return Base64.encodeBytes(input); + }else + return null; + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentIntegrator.java b/app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentIntegrator.java new file mode 100644 index 0000000..95dc0a8 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentIntegrator.java @@ -0,0 +1,332 @@ +/* + * Copyright 2009 ZXing authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package uk.co.bitethebullet.android.token.zxing; + +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import android.app.Activity; +import android.app.AlertDialog; +import android.content.ActivityNotFoundException; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.content.pm.ResolveInfo; +import android.net.Uri; +import android.util.Log; + +/** + *

A utility class which helps ease integration with Barcode Scanner via {@link Intent}s. This is a simple + * way to invoke barcode scanning and receive the result, without any need to integrate, modify, or learn the + * project's source code.

+ * + *

Initiating a barcode scan

+ * + *

To integrate, create an instance of {@code IntentIntegrator} and call {@link #initiateScan()} and wait + * for the result in your app.

+ * + *

It does require that the Barcode Scanner (or work-alike) application is installed. The + * {@link #initiateScan()} method will prompt the user to download the application, if needed.

+ * + *

There are a few steps to using this integration. First, your {@link Activity} must implement + * the method {@link Activity#onActivityResult(int, int, Intent)} and include a line of code like this:

+ * + *
{@code
+ * public void onActivityResult(int requestCode, int resultCode, Intent intent) {
+ *   IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, intent);
+ *   if (scanResult != null) {
+ *     // handle scan result
+ *   }
+ *   // else continue with any other code you need in the method
+ *   ...
+ * }
+ * }
+ * + *

This is where you will handle a scan result.

+ * + *

Second, just call this in response to a user action somewhere to begin the scan process:

+ * + *
{@code
+ * IntentIntegrator integrator = new IntentIntegrator(yourActivity);
+ * integrator.initiateScan();
+ * }
+ * + *

Note that {@link #initiateScan()} returns an {@link AlertDialog} which is non-null if the + * user was prompted to download the application. This lets the calling app potentially manage the dialog. + * In particular, ideally, the app dismisses the dialog if it's still active in its {@link Activity#onPause()} + * method.

+ * + *

You can use {@link #setTitle(String)} to customize the title of this download prompt dialog (or, use + * {@link #setTitleByID(int)} to set the title by string resource ID.) Likewise, the prompt message, and + * yes/no button labels can be changed.

+ * + *

By default, this will only allow applications that are known to respond to this intent correctly + * do so. The apps that are allowed to response can be set with {@link #setTargetApplications(Collection)}. + * For example, set to {@link #TARGET_BARCODE_SCANNER_ONLY} to only target the Barcode Scanner app itself.

+ * + *

Sharing text via barcode

+ * + *

To share text, encoded as a QR Code on-screen, similarly, see {@link #shareText(CharSequence)}.

+ * + *

Some code, particularly download integration, was contributed from the Anobiit application.

+ * + * @author Sean Owen + * @author Fred Lin + * @author Isaac Potoczny-Jones + * @author Brad Drehmer + * @author gcstang + */ +public final class IntentIntegrator { + + public static final int REQUEST_CODE = 0x0000c0de; // Only use bottom 16 bits + private static final String TAG = IntentIntegrator.class.getSimpleName(); + + public static final String DEFAULT_TITLE = "Install Barcode Scanner?"; + public static final String DEFAULT_MESSAGE = + "This application requires Barcode Scanner. Would you like to install it?"; + public static final String DEFAULT_YES = "Yes"; + public static final String DEFAULT_NO = "No"; + + private static final String BS_PACKAGE = "com.google.zxing.client.android"; + + // supported barcode formats + public static final Collection PRODUCT_CODE_TYPES = list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "RSS_14"); + public static final Collection ONE_D_CODE_TYPES = + list("UPC_A", "UPC_E", "EAN_8", "EAN_13", "CODE_39", "CODE_93", "CODE_128", + "ITF", "RSS_14", "RSS_EXPANDED"); + public static final Collection QR_CODE_TYPES = Collections.singleton("QR_CODE"); + public static final Collection DATA_MATRIX_TYPES = Collections.singleton("DATA_MATRIX"); + + public static final Collection ALL_CODE_TYPES = null; + + public static final Collection TARGET_BARCODE_SCANNER_ONLY = Collections.singleton(BS_PACKAGE); + public static final Collection TARGET_ALL_KNOWN = list( + BS_PACKAGE, // Barcode Scanner + "com.srowen.bs.android", // Barcode Scanner+ + "com.srowen.bs.android.simple" // Barcode Scanner+ Simple + // TODO add more -- what else supports this intent? + ); + + private final Activity activity; + private String title; + private String message; + private String buttonYes; + private String buttonNo; + private Collection targetApplications; + + public IntentIntegrator(Activity activity) { + this.activity = activity; + title = DEFAULT_TITLE; + message = DEFAULT_MESSAGE; + buttonYes = DEFAULT_YES; + buttonNo = DEFAULT_NO; + targetApplications = TARGET_ALL_KNOWN; + } + + public String getTitle() { + return title; + } + + public void setTitle(String title) { + this.title = title; + } + + public void setTitleByID(int titleID) { + title = activity.getString(titleID); + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } + + public void setMessageByID(int messageID) { + message = activity.getString(messageID); + } + + public String getButtonYes() { + return buttonYes; + } + + public void setButtonYes(String buttonYes) { + this.buttonYes = buttonYes; + } + + public void setButtonYesByID(int buttonYesID) { + buttonYes = activity.getString(buttonYesID); + } + + public String getButtonNo() { + return buttonNo; + } + + public void setButtonNo(String buttonNo) { + this.buttonNo = buttonNo; + } + + public void setButtonNoByID(int buttonNoID) { + buttonNo = activity.getString(buttonNoID); + } + + public Collection getTargetApplications() { + return targetApplications; + } + + public void setTargetApplications(Collection targetApplications) { + this.targetApplications = targetApplications; + } + + public void setSingleTargetApplication(String targetApplication) { + this.targetApplications = Collections.singleton(targetApplication); + } + + /** + * Initiates a scan for all known barcode types. + */ + public AlertDialog initiateScan() { + return initiateScan(ALL_CODE_TYPES); + } + + /** + * Initiates a scan only for a certain set of barcode types, given as strings corresponding + * to their names in ZXing's {@code BarcodeFormat} class like "UPC_A". You can supply constants + * like {@link #PRODUCT_CODE_TYPES} for example. + */ + public AlertDialog initiateScan(Collection desiredBarcodeFormats) { + Intent intentScan = new Intent(BS_PACKAGE + ".SCAN"); + intentScan.addCategory(Intent.CATEGORY_DEFAULT); + + // check which types of codes to scan for + if (desiredBarcodeFormats != null) { + // set the desired barcode types + StringBuilder joinedByComma = new StringBuilder(); + for (String format : desiredBarcodeFormats) { + if (joinedByComma.length() > 0) { + joinedByComma.append(','); + } + joinedByComma.append(format); + } + intentScan.putExtra("SCAN_FORMATS", joinedByComma.toString()); + } + + String targetAppPackage = findTargetAppPackage(intentScan); + if (targetAppPackage == null) { + return showDownloadDialog(); + } + intentScan.setPackage(targetAppPackage); + intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intentScan.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + activity.startActivityForResult(intentScan, REQUEST_CODE); + return null; + } + + private String findTargetAppPackage(Intent intent) { + PackageManager pm = activity.getPackageManager(); + List availableApps = pm.queryIntentActivities(intent, PackageManager.MATCH_DEFAULT_ONLY); + if (availableApps != null) { + for (ResolveInfo availableApp : availableApps) { + String packageName = availableApp.activityInfo.packageName; + if (targetApplications.contains(packageName)) { + return packageName; + } + } + } + return null; + } + + private AlertDialog showDownloadDialog() { + AlertDialog.Builder downloadDialog = new AlertDialog.Builder(activity); + downloadDialog.setTitle(title); + downloadDialog.setMessage(message); + downloadDialog.setPositiveButton(buttonYes, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialogInterface, int i) { + Uri uri = Uri.parse("market://details?id=" + BS_PACKAGE); + Intent intent = new Intent(Intent.ACTION_VIEW, uri); + try { + activity.startActivity(intent); + } catch (ActivityNotFoundException anfe) { + // Hmm, market is not installed + Log.w(TAG, "Android Market is not installed; cannot install Barcode Scanner"); + } + } + }); + downloadDialog.setNegativeButton(buttonNo, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialogInterface, int i) {} + }); + return downloadDialog.show(); + } + + + /** + *

Call this from your {@link Activity}'s + * {@link Activity#onActivityResult(int, int, Intent)} method.

+ * + * @return null if the event handled here was not related to this class, or + * else an {@link IntentResult} containing the result of the scan. If the user cancelled scanning, + * the fields will be null. + */ + public static IntentResult parseActivityResult(int requestCode, int resultCode, Intent intent) { + if (requestCode == REQUEST_CODE) { + if (resultCode == Activity.RESULT_OK) { + String contents = intent.getStringExtra("SCAN_RESULT"); + String formatName = intent.getStringExtra("SCAN_RESULT_FORMAT"); + byte[] rawBytes = intent.getByteArrayExtra("SCAN_RESULT_BYTES"); + int intentOrientation = intent.getIntExtra("SCAN_RESULT_ORIENTATION", Integer.MIN_VALUE); + Integer orientation = intentOrientation == Integer.MIN_VALUE ? null : intentOrientation; + String errorCorrectionLevel = intent.getStringExtra("SCAN_RESULT_ERROR_CORRECTION_LEVEL"); + return new IntentResult(contents, + formatName); + } + return null; + } + return null; + } + + + /** + * Shares the given text by encoding it as a barcode, such that another user can + * scan the text off the screen of the device. + * + * @param text the text string to encode as a barcode + */ + public void shareText(CharSequence text) { + Intent intent = new Intent(); + intent.addCategory(Intent.CATEGORY_DEFAULT); + intent.setAction(BS_PACKAGE + ".ENCODE"); + intent.putExtra("ENCODE_TYPE", "TEXT_TYPE"); + intent.putExtra("ENCODE_DATA", text); + String targetAppPackage = findTargetAppPackage(intent); + if (targetAppPackage == null) { + showDownloadDialog(); + } else { + intent.setPackage(targetAppPackage); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP); + intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_WHEN_TASK_RESET); + activity.startActivity(intent); + } + } + + private static Collection list(String... values) { + return Collections.unmodifiableCollection(Arrays.asList(values)); + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentResult.java b/app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentResult.java new file mode 100644 index 0000000..d9e7ddc --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/zxing/IntentResult.java @@ -0,0 +1,33 @@ + +package uk.co.bitethebullet.android.token.zxing; + +/** + * <p>Encapsulates the result of a barcode scan invoked through {@link IntentIntegrator}.</p> + * + * @author Sean Owen + */ +public final class IntentResult { + + private final String contents; + private final String formatName; + + IntentResult(String contents, String formatName) { + this.contents = contents; + this.formatName = formatName; + } + + /** + * @return raw content of barcode + */ + public String getContents() { + return contents; + } + + /** + * @return name of format, like "QR_CODE", "UPC_A". See <code>BarcodeFormat</code> for more format names. + */ + public String getFormatName() { + return formatName; + } + +} \ No newline at end of file diff --git a/app/src/main/res/drawable/add.png b/app/src/main/res/drawable/add.png new file mode 100644 index 0000000000000000000000000000000000000000..c2b44a648f8baa649d11f89825da3cdc3ca8c15a GIT binary patch literal 1613 zcmV-T2D15yP)+07)3O-8Z?Log#)1{CWMeePQnI4LL~{Aqy$2V z_^^83d*3~0@71^V+2`JK?tRy1PCVRm?qUC}Z>{yMZ|}n*LU;3u^>N_Gn*+^E%=+|i z{$DS}iuMPL;YSS9gLq$~!_fQ}30=VddHi2wQ_Ss6WIqpGb!Xq-*en1ncBx@wXh)Km z={V+RqsZKc1x?R@UJk>YT(MTZx6aML)^Xo@I}sRd8ORN7NmKJdlA32IGTw(QfRet0 z#1VcG-y0<~9gCT^nLBH9_YTIKyfXko2Ht+$XvF&^Vwy)2yz?$qjZ+1LK8!Ak|2Q#E zVg3o1xzDYWeQ56f9{|e{?wMpP+A)t8M<%AtoS6MBSnVL$3mNaAaqrrQ zo*a*;ln^bk-{u^BawVsfd1y|qDFiu|M!Y+*@+B;OQv=|Nl}r?ygKfOFo%7x-lP2v^diO`sRCeO-qXQ|HUQ*qG~3mP_B-_|$c%+qy9fXdk&f|} z#=IMbk7;(|AY$tr({pVLuWTr}O3tg>MG+x~F#r-eVksibe?< zF+-c0h95%Hxd4D3?Fy|`74_E1zDNOlT5|SEJH}!H%*tIGf>h zCFF1C>PFvrE(m+#TIRVDb2*%;12hTo`W^=`ZjCOg69xhNi-arTtt`A(^h&Kl$};C! zAhg#L?$7B)S^_dr0U(U@`3YGCKy1hpF0&&5EF#GYDz}B2N{9gVGrwLY6&*kduBw4t zTn#`HqR3H@fE`g?nYmTh@+#L=a7(crv=mPU%aS==Z)32%SUwpTN0R>p8C&`|xtB zrU|MO@2+1;TPGX6GHifxms^(JyEq5F)Rn-OVb~AUII>OHlx`sHKT)U(mC#Epa$3(; zDlTQ-G;~owS-sV6?xc2$7n$L)+Nb2!HLVva+*Z!yxt#As003Z?4+G&Y4Zezuy@eFp zpoY2Y?~b0VBlMd?noxQaDS9i1^XguplY#6+O7t_t=W!JlHR47A2)bU{b5h7`20xg1 zNXC8cfj~tnwQ>pmepQNg0ibXt2iFcXnZ9kI&mX~@3@zqE3PZ&$ z_)|JyMtly;;^?$(m6Jmg0I>`S)I&$H(|M4gQu?>^QIdja#qo<)Vfd=d9!+z%6%+ql z@St*wZ<6`Q$s0hwa|%MGKvn>VE<6?@Q@yoK`!s43@OsM0h4CzZEq3lztTlIMUq%Iz z<6}-vrX(+c8`Jns*o33@RF1&AYME)sg|4U0ok^X{;NWo-qSwYrYvd{E_5c*o$gto{ zDsnUtc^EbP7_@yo08Dzq3C-e6s%m-;_I~01X1DmRdAC2${}W&U_!x|1-wd#i00000 LNkvXXu0mjf;(PqF literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/android50.png b/app/src/main/res/drawable/android50.png new file mode 100644 index 0000000000000000000000000000000000000000..68b893812fbb6bf60caed3b6b94a8d06b7036def GIT binary patch literal 3074 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/androidtoken.png b/app/src/main/res/drawable/androidtoken.png new file mode 100644 index 0000000000000000000000000000000000000000..869efa5e519210d0f109e0beca0e80fdbccb1640 GIT binary patch literal 5445 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 U<2Uu==b(hOK^s=xU4dl%AGk05l>h($ literal 0 HcmV?d00001 diff --git a/app/src/main/res/drawable/baseline_add_black_24dp.png b/app/src/main/res/drawable/baseline_add_black_24dp.png new file mode 100644 index 0000000000000000000000000000000000000000..818b26d62fc45092264ad73e63904d28d1915d86 GIT binary patch literal 95 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 S`hb-H0000lBAekf@ zP)G<{1_B{_8tBeS7HE?0q_cLC-nXRpExo4q?$@uE-@LvxM59rnr|PSF?|Ju~bKbe< zy?1XGfCaI5&HkGiHZERwp!`=GN(DKQm=E?gdW z!Q_g7PNviC;-Gy)kw&|GOQU@8+5-iQtwlc}0OEZ4l83(LK7B~7AJ#=Y7H=$F#kZog z%Z7GE6uKF}5eDqO1hmdL3_d{Y04_}gQQB=qgTRJqADzx*X8nGJcFDts1wWLH`JCUq zY-9PvziKRIgF8+)3{Inz>%@c3C7{_Q{-Bd286aWO1kW5 zV=^PN#;7NXr?gDXNEzE}ek=g8-q|_j69sKfv&a}p9&R(DO_PAg7=qOsXPzpKnQ#(R z0vVWo0!}|YXKeORWX^;kgv&|_Wt}FB>LbbLj~UcVu}Ya`os)?8xOJ+8~suaIU6t&NYF026`6-qdfwiA%#M=76NTJ zv87C}zjeb=!IJg)x6Bv7zUN9Nr?|>csEDJ5%n6u%Nfh7l~-6WxoPwpQ~0V?k0wzdcvqfGbL#^|Mc4h#7TwCN zELqL4UsueY!ZlqAwm=$6dmN4sW~t|NLdR5JOLsqHdM8@12rwv`L>FH;J4a>mqMbW} z?QdMdsKg7oog(_nDhj8#Rvc(`%73@DNw)OCFK(L+V99+2$NpJqGMweP6Q_9&h^-W) zHVXC-W)Z5)sBi9ufN{v_7c)i=(W|f|_Ap^07lxli3@N zoXPkm@B1Cw>ZK?7GwQ;N8fZMgm?45|3O~LqIR#q~IQrjebnt|ve`6xX`=AwUM z5}o`p^r`|F(S;#(rtv||B%UcV53GE(S(^RuSBo+MJofBCvFs$@>#ORuz~WCs!9;5h z0(I<8m}e5GzR-?_<|`Q;!8K(>Bfup+`tyAw;s~~08T*jn+Is`&j-x!2S6NaTaSbT|Dy;q?^ zI{$5W)zVqr;nQDv=tlO=kO;~YNGu7=zk zCZAoSJC0G20tH`wg&+l3oH0~ZiIBIa6R&P4ZK$afK_Yb_Or?pwrBw~La2%9gLB{i;HQF#Hl9Rs4Ly@+CHHF;TY2KQ*JH%PH z=alxo*X-*5`0N(NW1&I2HP7Pyj5>B4QDN3Uc*m1oboM& z{qQmAa`-4DQy>fs4x_ETgYOI*l2|zIHl>f1J_c&3e1^u9xa6 z$cJcmy;{7Z&S>itTA9gd#NtV0q_wpb<>lqLv7~|COW)Hf6be*VSCf8}vB{W5*Mtl2 zw|QK-6*}3SFVsFwKE+S&=1BKn_3}Qi9L-E3k;+YeXfzrK1OhBb6hgqJo}(HZ97IV; z2^8i4oPLTtT;T;B@;W^Hr}hsr8E9BUen#03eR1sQkXQX$*+jfg?tsb?K!Tb4Z3za0 z5Q#*fY5E6}kjVlY^9ZpuX$zr8HiNuo8~stcOSS5C&Y5rihU@Z|Z`s!&J3JKZ-BmV# zcp^o%S&3x&8zYGXE?l^PWHN~#6!|C`4j&+yyDs$qZG~b<iBpm5*0agXxTR5s@xW`A^A=KMK|C3(&=7~Hlo^HuL(8s3}np$kpyCN3Px89 z-<7JWDsVWQIXZo7BecRH_`-?V!ebA4aft5?mW>3IkM8aftjcZs-je!!XFS}A2|pVRc=sZsRFJxqs{M?L{K1{Pf(PE=E&ADq)7x{(v+sQF+=+je3S>PBw^-|^W#$<8 z));y%6#A?T#u#`^#`)u(gses3mwZsEQarMYckceTdlz&#T@WiboxS-Fo6m2&=jFQc z%^!9Mb{y;%?yV3>_MQ_<-a0ZO-11r9!2LTqE7orB+O&Sh)de5V{|BmO;a(aWr;`8x N002ovPDHLkV1i7hxxxSd literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/about.xml b/app/src/main/res/layout/about.xml new file mode 100644 index 0000000..b13b551 --- /dev/null +++ b/app/src/main/res/layout/about.xml @@ -0,0 +1,46 @@ + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/otpdialog.xml b/app/src/main/res/layout/otpdialog.xml new file mode 100644 index 0000000..0c4c269 --- /dev/null +++ b/app/src/main/res/layout/otpdialog.xml @@ -0,0 +1,21 @@ + + + + + + diff --git a/app/src/main/res/layout/pinchange.xml b/app/src/main/res/layout/pinchange.xml new file mode 100644 index 0000000..061b44b --- /dev/null +++ b/app/src/main/res/layout/pinchange.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/pindefinitiondialog.xml b/app/src/main/res/layout/pindefinitiondialog.xml new file mode 100644 index 0000000..81e4537 --- /dev/null +++ b/app/src/main/res/layout/pindefinitiondialog.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/pinremove.xml b/app/src/main/res/layout/pinremove.xml new file mode 100644 index 0000000..1cfd452 --- /dev/null +++ b/app/src/main/res/layout/pinremove.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/app/src/main/res/layout/token_add.xml b/app/src/main/res/layout/token_add.xml new file mode 100644 index 0000000..1967198 --- /dev/null +++ b/app/src/main/res/layout/token_add.xml @@ -0,0 +1,211 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/token_list_row.xml b/app/src/main/res/layout/token_list_row.xml new file mode 100644 index 0000000..f8c59f7 --- /dev/null +++ b/app/src/main/res/layout/token_list_row.xml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/token_item_menu.xml b/app/src/main/res/menu/token_item_menu.xml new file mode 100644 index 0000000..85ab721 --- /dev/null +++ b/app/src/main/res/menu/token_item_menu.xml @@ -0,0 +1,11 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml new file mode 100644 index 0000000..283446f --- /dev/null +++ b/app/src/main/res/navigation/nav_graph.xml @@ -0,0 +1,28 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..b4f7650 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,44 @@ + + + + Event Token + Time Token + + + 6 + 7 + 8 + + + 30 seconds + 60 seconds + + + Hex + Base 32 + Base 64 + + + + No Security + PIN Number + Biometric + + + 0 + 1 + 2 + + + + + + Reply + Reply to all + + + + reply + reply_all + + diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..125df87 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,3 @@ + + 16dp + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml new file mode 100644 index 0000000..152e0b8 --- /dev/null +++ b/app/src/main/res/values/strings.xml @@ -0,0 +1,147 @@ + + + No Tokens Defined + Auth Token + Auth Token - Add Token + Auth Token - Change Pin + Auth Token - About + Add Token + Change PIN + Remove Pin + Delete Token + Token Type + Name + Organisation + Serial No + OTP Length + Time Step + Next > + Select Token Type + Select OTP Length + Select Time Step + Token Seed Method + Direct Seed Entry + Generate Random Seed + Seed from Password + Token Seed Value + Complete + OK + Cancel + Token Name is Required + Token Serial is Required + Token Seed is Required + Token Seed is weak, the recommended minimum is 128 bits. The seed enter contains %s bits. Create the token anyway? + Token Seed is too short + Token Seed is Invalid, only enter a-f, A-F or 0-9 characters. + S/N: + Enter PIN + New PIN + Confirm PIN + Save + Existing PIN is invalid + The new PINs are not the same + New PIN is required + Enter Existing PIN + Remove + Login + One Time Password + Input Format + Scan QR + Settings + About + otpauth url is not valid + Invalid token type + The counter parameter is required when creating an HOTP token + Cancel + Install + Install Barcode Scanner? + To read barcode you need need install the ZXING barcode scanner app. + Are you sure you want to delete this token? + Token Deleted + Token Added + Invalid QR code + Unable to convert the supplied value to a hex seed + Unable to switch input format, value entered is invalid + https://github.com/markmcavoy/androidtoken + Auth Token is a free and open source TOTP and HOTP application from + Zalabria Limited.\n\nIf you would like us to help you with your Android/iOS projects please get in + touch. Zalabria + + Messages + Sync + Security + Display Preferences + + + Your signature + Default reply action + + + Sync email periodically + Download incoming attachments + Automatically download attachments for incoming emails + + Only download attachments when manually requested + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Change Icon + Delete Token + Copy Seed to Clipboard + Generate QR Code + Token seed copied + Scan QR + + Delete + Delete Token + Are you sure you want to delete the token, [name]? + Token as been deleted + Security Protection + Group token into two digit blocks + Invalid PIN + Token seed copied + + HOTP Token + Dismiss + Ok + Set PIN + + About + Close + + First Fragment + Second Fragment + Next + Previous + + Hello first fragment + Hello second fragment. Arg: %1$s + + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..85f1be1 --- /dev/null +++ b/app/src/main/res/values/styles.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/demo/src/main/res/values/colors.xml b/demo/src/main/res/values/colors.xml new file mode 100644 index 0000000..f8c6127 --- /dev/null +++ b/demo/src/main/res/values/colors.xml @@ -0,0 +1,10 @@ + + + #FFBB86FC + #FF6200EE + #FF3700B3 + #FF03DAC5 + #FF018786 + #FF000000 + #FFFFFFFF + \ No newline at end of file diff --git a/demo/src/main/res/values/strings.xml b/demo/src/main/res/values/strings.xml new file mode 100644 index 0000000..aee84b4 --- /dev/null +++ b/demo/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + Demo + \ No newline at end of file diff --git a/demo/src/main/res/values/themes.xml b/demo/src/main/res/values/themes.xml new file mode 100644 index 0000000..af5f0ab --- /dev/null +++ b/demo/src/main/res/values/themes.xml @@ -0,0 +1,16 @@ + + + + \ No newline at end of file diff --git a/demo/src/test/java/com/fly/demo/ExampleUnitTest.kt b/demo/src/test/java/com/fly/demo/ExampleUnitTest.kt new file mode 100644 index 0000000..8c0a09b --- /dev/null +++ b/demo/src/test/java/com/fly/demo/ExampleUnitTest.kt @@ -0,0 +1,17 @@ +package com.fly.demo + +import org.junit.Test + +import org.junit.Assert.* + +/** + * Example local unit test, which will execute on the development machine (host). + * + * See [testing documentation](http://d.android.com/tools/testing). + */ +class ExampleUnitTest { + @Test + fun addition_isCorrect() { + assertEquals(4, 2 + 2) + } +} \ No newline at end of file diff --git a/optlib/.gitignore b/optlib/.gitignore new file mode 100644 index 0000000..42afabf --- /dev/null +++ b/optlib/.gitignore @@ -0,0 +1 @@ +/build \ No newline at end of file diff --git a/optlib/build.gradle b/optlib/build.gradle new file mode 100644 index 0000000..bd4ff32 --- /dev/null +++ b/optlib/build.gradle @@ -0,0 +1,41 @@ +plugins { + id 'com.android.library' +} +apply from: 'push.gradle' +android { + compileSdkVersion 30 + buildToolsVersion "30.0.3" + + defaultConfig { + minSdkVersion 16 + targetSdkVersion 30 + versionCode 1 + versionName "1.0" + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles "consumer-rules.pro" + } + + testOptions { + unitTests.returnDefaultValues = true + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } +} + +dependencies { + + implementation 'androidx.appcompat:appcompat:1.3.0' + implementation 'com.google.android.material:material:1.4.0' + testImplementation 'junit:junit:4.+' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} \ No newline at end of file diff --git a/optlib/consumer-rules.pro b/optlib/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/optlib/proguard-rules.pro b/optlib/proguard-rules.pro new file mode 100644 index 0000000..481bb43 --- /dev/null +++ b/optlib/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile \ No newline at end of file diff --git a/optlib/push.gradle b/optlib/push.gradle new file mode 100644 index 0000000..540267a --- /dev/null +++ b/optlib/push.gradle @@ -0,0 +1,22 @@ +apply plugin: "maven" +uploadArchives { + repositories { + mavenDeployer { + repository(url: "$release_url") { + authentication( + userName: "$maven_name", + password: "$maven_password" + ) + } + snapshotRepository(url: "$snapshot_url") { + authentication( + userName: "$maven_name", + password: "$maven_password" + ) + } + pom.version = "$cfgvs.optlib" + pom.artifactId = "optlib" + pom.groupId = "$groupId" + } + } +} \ No newline at end of file diff --git a/optlib/src/androidTest/java/com/fly/optlib/ExampleInstrumentedTest.java b/optlib/src/androidTest/java/com/fly/optlib/ExampleInstrumentedTest.java new file mode 100644 index 0000000..ac41686 --- /dev/null +++ b/optlib/src/androidTest/java/com/fly/optlib/ExampleInstrumentedTest.java @@ -0,0 +1,26 @@ +package com.fly.optlib; + +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import static org.junit.Assert.*; + +/** + * Instrumented test, which will execute on an Android device. + * + * @see Testing documentation + */ +@RunWith(AndroidJUnit4.class) +public class ExampleInstrumentedTest { + @Test + public void useAppContext() { + // Context of the app under test. + Context appContext = InstrumentationRegistry.getInstrumentation().getTargetContext(); + assertEquals("com.fly.optlib.test", appContext.getPackageName()); + } +} \ No newline at end of file diff --git a/optlib/src/main/AndroidManifest.xml b/optlib/src/main/AndroidManifest.xml new file mode 100644 index 0000000..6afce7f --- /dev/null +++ b/optlib/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + \ No newline at end of file diff --git a/optlib/src/main/java/com/fly/optlib/parse/OtpAuthUriException.java b/optlib/src/main/java/com/fly/optlib/parse/OtpAuthUriException.java new file mode 100644 index 0000000..7b3b2fc --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/parse/OtpAuthUriException.java @@ -0,0 +1,24 @@ +package com.fly.optlib.parse; + +public class OtpAuthUriException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + String err; + + public OtpAuthUriException() + { + super(); // call superclass constructor + err = "unknown"; + } + + //----------------------------------------------- + // Constructor receives some kind of message that is saved in an instance variable. + public OtpAuthUriException(String err) + { + super(err); // call super class constructor + this.err = err; // save message + } +} diff --git a/optlib/src/main/java/com/fly/optlib/parse/UrlParser.java b/optlib/src/main/java/com/fly/optlib/parse/UrlParser.java new file mode 100644 index 0000000..47cdea7 --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/parse/UrlParser.java @@ -0,0 +1,82 @@ +package com.fly.optlib.parse; + + +import com.fly.optlib.tokens.ITokenMeta; +import com.fly.optlib.tokens.TokenMetaData; + +public class UrlParser { + + public static ITokenMeta parseOtpAuthUrl(String url) throws OtpAuthUriException { + + int tokenType; + String tokenName; + String organisation = null; + String secret = null; + int digits = 6; + int counter = 0; + int period = 30; + boolean hasCounterParameter = false; + + if(!url.startsWith("otpauth://")){ + //not a valid otpauth url + //throw new OtpAuthUriException(context.getString(R.string.otpAuthUrlInvalid)); + throw new OtpAuthUriException(); + } + + String tokenTypeString = url.substring(10, url.indexOf("/", 10)); + + if(tokenTypeString.equals("hotp")){ + tokenType = TokenMetaData.HOTP_TOKEN; + }else if(tokenTypeString.equals("totp")){ + tokenType = TokenMetaData.TOTP_TOKEN; + }else{ + //the token type parameter is not valid + //throw new OtpAuthUriException(context.getString(R.string.otpAuthTokenTypeInvalid)); + throw new OtpAuthUriException(); + } + + tokenName = url.substring(url.indexOf("/", 10) + 1, url.indexOf("?", 10)); + + //decode url + tokenName = java.net.URLDecoder.decode(tokenName); + + //check to see if we have an organisation prefix for the token name + if(tokenName.contains(":")){ + organisation = tokenName.substring(0, tokenName.indexOf(":")).trim(); + tokenName = tokenName.substring(tokenName.indexOf(":") + 1).trim(); + } + String[] parameters = url.substring(url.indexOf("?") + 1).split("&"); + + for(int i = 0; i < parameters.length; i++){ + String[] paraDetail = new String[2]; + + //get the key + paraDetail[0] = parameters[i].substring(0, parameters[i].indexOf("=")); + + //get the value + paraDetail[1] = parameters[i].substring(parameters[i].indexOf("=") + 1, parameters[i].length()); + + //read the parameter and work out if its + //a valid parameter, if not just ignore + if(paraDetail[0].equals("secret")){ + secret = paraDetail[1]; + }else if(paraDetail[0].equals("digits")){ + digits = Integer.parseInt(paraDetail[1]); + }else if(paraDetail[0].equals("counter")){ + counter = Integer.parseInt(paraDetail[1]); + hasCounterParameter = true; + }else if(paraDetail[0].equals("period")){ + period = Integer.parseInt(paraDetail[1]); + } + } + + if(tokenType == TokenMetaData.HOTP_TOKEN && !hasCounterParameter){ + //when the token is a hotp token it must have the counter + //parameter supplied otherwise we should throw an error + //throw new OtpAuthUriException(context.getString(R.string.otpAuthMissingCounterParameter)); + throw new OtpAuthUriException(); + } + + return new TokenMetaData(tokenName, tokenType, secret, digits, period, counter, organisation); + } +} diff --git a/optlib/src/main/java/com/fly/optlib/tokens/HotpToken.java b/optlib/src/main/java/com/fly/optlib/tokens/HotpToken.java new file mode 100644 index 0000000..317586b --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/tokens/HotpToken.java @@ -0,0 +1,308 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package com.fly.optlib.tokens; + +import com.fly.optlib.utils.Constant; +import com.fly.optlib.utils.SeedConvertor; + +import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.TimeZone; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + + + + +/** + * Hotp Token + * + * This is an event based OATH token, for further details + * see the RFC http://tools.ietf.org/html/rfc4226 + * + */ +public class HotpToken implements IToken { + + private String mName; + private String mOrganisation; + private String mSerial; + private String mSeed; + private long mEventCount; + private int mOtpLength; + private long id; + + + private static final int[] DIGITS_POWER + // 0 1 2 3 4 5 6 7 8 + = {1,10,100,1000,10000,100000,1000000,10000000,100000000}; + + /** + * + * @param name + * @param serial + * @param seed + * @param eventCount 这个每次生成时 有变动!! + * @param otpLength + * @param organisation + */ + public HotpToken(String name, String serial, String seed, + long eventCount, int otpLength, String organisation){ + mName = name; + mSerial = serial; + mSeed = seed; + mEventCount = eventCount; + mOtpLength = otpLength; + mOrganisation = organisation; + } + + public int getTimeStep(){ + return 0; + } + + public long getId(){ + return this.id; + } + + public void setId(long id){ + this.id = id; + } + + public int getTokenType(){ + return Constant.TOKEN_TYPE_EVENT; + } + + public String getName() { + return mName; + } + + public String getOrganisation(){ return mOrganisation; } + + public void setName(String name) { + this.mName = name; + } + + + public String getSerialNumber() { + return mSerial; + } + + + public void setSerialNumber(String serial) { + this.mSerial = serial; + } + + + public String getSeed() { + return mSeed; + } + + + protected void setSeed(String seed) { + this.mSeed = seed; + } + + + public long getEventCount() { + return mEventCount; + } + + + protected void setEventCount(long eventCount) { + this.mEventCount = eventCount; + } + + + public int getOtpLength() { + return mOtpLength; + } + + + public void setOtpLength(int otpLength) { + this.mOtpLength = otpLength; + } + + + public String getUrl() { + //create the uri for the token that can be used to generate the QR code + //otpauth://hotp/organisation:alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=10" + try { + //convert the seed from hex to base32 + String base32Secret = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(getSeed(), SeedConvertor.HEX_FORMAT), + SeedConvertor.BASE32_FORMAT); + + StringBuilder buffer = new StringBuilder(); + buffer.append("otpauth://hotp/"); + + if (getOrganisation() != null && getOrganisation().length() > 0) { + buffer.append(java.net.URLEncoder.encode(getOrganisation())); + buffer.append(":"); + } + + buffer.append(java.net.URLEncoder.encode(getName())); + buffer.append("?secret="); + buffer.append(base32Secret); + buffer.append("&counter="); + buffer.append(getEventCount()); + + return buffer.toString(); + }catch(IOException ex){ + return null; + } + } + + public String getFullName(){ + StringBuilder sb = new StringBuilder(); + + if(this.getOrganisation() != null){ + sb.append(this.getOrganisation()); + sb.append("/"); + } + + sb.append(this.getName()); + + return sb.toString(); + } + + + public String generateOtp() { + + byte[] counter = new byte[8]; + long movingFactor = mEventCount; + + for(int i = counter.length - 1; i >= 0; i--){ + counter[i] = (byte)(movingFactor & 0xff); + movingFactor >>= 8; + } + + byte[] hash = hmacSha(stringToHex(mSeed), counter); + int offset = hash[hash.length - 1] & 0xf; + + int otpBinary = ((hash[offset] & 0x7f) << 24) + |((hash[offset + 1] & 0xff) << 16) + |((hash[offset + 2] & 0xff) << 8) + |(hash[offset + 3] & 0xff); + + int otp = otpBinary % DIGITS_POWER[mOtpLength]; + String result = Integer.toString(otp); + + + while(result.length() < mOtpLength){ + result = "0" + result; + } + + return result; + } + + public static byte[] stringToHex(String hexInputString){ + + byte[] bts = new byte[hexInputString.length() / 2]; + + for (int i = 0; i < bts.length; i++) { + bts[i] = (byte) Integer.parseInt(hexInputString.substring(2*i, 2*i+2), 16); + } + + return bts; + } + + private byte[] hmacSha(byte[] seed, byte[] counter) { + + try{ + Mac hmacSha1; + + try{ + hmacSha1 = Mac.getInstance("HmacSHA1"); + }catch(NoSuchAlgorithmException ex){ + hmacSha1 = Mac.getInstance("HMAC-SHA-1"); + } + + SecretKeySpec macKey = new SecretKeySpec(seed, "RAW"); + hmacSha1.init(macKey); + + return hmacSha1.doFinal(counter); + + }catch(GeneralSecurityException ex){ + throw new UndeclaredThrowableException(ex); + } + } + + /** + * Generates a new seed value for a token + * the returned string will contain a randomly generated + * hex value + * @param length - defines the length of the new seed this should be either 128 or 160 + * @return + */ + public static String generateNewSeed(int length){ + + String salt = ""; + long ticks = Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTimeInMillis(); + salt = salt + ticks; + + byte[] byteToHash = salt.getBytes(); + + MessageDigest md; + + try{ + if(length == 128){ + //128 long + md = MessageDigest.getInstance("MD5"); + }else{ + //160 long + md = MessageDigest.getInstance("SHA1"); + } + + md.reset(); + md.update(byteToHash); + + byte[] digest = md.digest(); + + //convert to hex string + + return byteArrayToHexString(digest); + + }catch(NoSuchAlgorithmException ex){ + return null; + } + } + + + public static String byteArrayToHexString(byte[] digest) { + + StringBuffer buffer = new StringBuffer(); + + for(int i =0; i < digest.length; i++){ + String hex = Integer.toHexString(0xff & digest[i]); + + if(hex.length() == 1) + buffer.append("0"); + + buffer.append(hex); + + } + + return buffer.toString(); + } + +} diff --git a/optlib/src/main/java/com/fly/optlib/tokens/IToken.java b/optlib/src/main/java/com/fly/optlib/tokens/IToken.java new file mode 100644 index 0000000..50c0f31 --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/tokens/IToken.java @@ -0,0 +1,45 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package com.fly.optlib.tokens; + +public interface IToken { + + public String getName(); + + public String getSerialNumber(); + + public int getTokenType(); + + public String generateOtp(); + + public long getId(); + + public void setId(long id); + + public int getTimeStep(); + + public String getOrganisation(); + + public String getSeed(); + + public String getFullName(); + + public String getUrl(); +} diff --git a/optlib/src/main/java/com/fly/optlib/tokens/ITokenMeta.java b/optlib/src/main/java/com/fly/optlib/tokens/ITokenMeta.java new file mode 100644 index 0000000..dee4e75 --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/tokens/ITokenMeta.java @@ -0,0 +1,18 @@ +package com.fly.optlib.tokens; + +public interface ITokenMeta { + + public String getName(); + + public String getOrganisation(); + + public int getTokenType(); + + public String getSecretBase32(); + + public int getDigits(); + + public int getTimeStep(); + + public int getCounter(); +} diff --git a/optlib/src/main/java/com/fly/optlib/tokens/TokenMetaData.java b/optlib/src/main/java/com/fly/optlib/tokens/TokenMetaData.java new file mode 100644 index 0000000..b08108c --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/tokens/TokenMetaData.java @@ -0,0 +1,61 @@ +package com.fly.optlib.tokens; + +public class TokenMetaData implements ITokenMeta { + + public static final int HOTP_TOKEN = 0; + public static final int TOTP_TOKEN = 1; + + String tokenName; + String organisation; + int tokenType; + String secretBase32; + int digits; + int timeStep; + int counter; + + public TokenMetaData(String tokenName, int tokenType, String secret, + int digits, int timeStep, int counter){ + + this(tokenName, tokenType, secret, digits, timeStep, counter, null); + } + + public TokenMetaData(String tokenName, int tokenType, String secret, + int digits, int timeStep, int counter, + String organisation) + { + this.tokenName = tokenName; + this.tokenType = tokenType; + this.secretBase32 = secret; + this.digits = digits; + this.timeStep = timeStep; + this.counter = counter; + this.organisation = organisation; + } + + public String getName() { + return tokenName; + } + + public int getTokenType() { + return tokenType; + } + + public String getSecretBase32() { + return secretBase32; + } + + public int getDigits() { + return digits; + } + + public int getTimeStep() { + return timeStep; + } + + public int getCounter() { + return counter; + } + + public String getOrganisation(){ return organisation; } + +} diff --git a/optlib/src/main/java/com/fly/optlib/tokens/TotpToken.java b/optlib/src/main/java/com/fly/optlib/tokens/TotpToken.java new file mode 100644 index 0000000..76e7d1c --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/tokens/TotpToken.java @@ -0,0 +1,101 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package com.fly.optlib.tokens; + +import com.fly.optlib.utils.Constant; +import com.fly.optlib.utils.SeedConvertor; + +import java.io.IOException; +import java.util.Calendar; +import java.util.TimeZone; + + + + +/** + * TOTP Token + * + * Generates an OTP based on the time, for more information see + * http://tools.ietf.org/html/draft-mraihi-totp-timebased-00 + * + */ +public class TotpToken extends HotpToken { + + private int mTimeStep; + + public TotpToken(String name, String serial, String seed, int timeStep, + int otpLength, String organisation){ + super(name, serial, seed, 0, otpLength, organisation); + + mTimeStep = timeStep; + } + + @Override + public int getTimeStep(){ + return mTimeStep; + } + + @Override + public int getTokenType(){ + return Constant.TOKEN_TYPE_TIME; + } + + @Override + public String generateOtp() { + + //calculate the moving counter using the time + return generateOtp(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); + } + + @Override + public String getUrl() { + //otpauth://totp/organisation:alice@google.com?secret=JBSWY3DPEHPK3PXP" + try { + //convert the seed from hex to base32 + String base32Secret = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(getSeed(), SeedConvertor.HEX_FORMAT), + SeedConvertor.BASE32_FORMAT); + + StringBuilder buffer = new StringBuilder(); + buffer.append("otpauth://totp/"); + + if (getOrganisation() != null && getOrganisation().length() > 0) { + buffer.append(java.net.URLEncoder.encode(getOrganisation())); + buffer.append(":"); + } + + buffer.append(java.net.URLEncoder.encode(getName())); + buffer.append("?secret="); + buffer.append(base32Secret); + + return buffer.toString(); + }catch(IOException ex){ + return null; + } + } + + public String generateOtp(Calendar currentTime){ + long time = currentTime.getTimeInMillis()/1000L; + super.setEventCount(time/new Long(mTimeStep)); + + return super.generateOtp(); + } + + +} diff --git a/optlib/src/main/java/com/fly/optlib/utils/Base32.java b/optlib/src/main/java/com/fly/optlib/utils/Base32.java new file mode 100644 index 0000000..c5a9af2 --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/utils/Base32.java @@ -0,0 +1,235 @@ +package com.fly.optlib.utils; + +import java.util.ArrayList; + +public class Base32 { + + private static final String DEF_ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final char DEF_PADDING = '='; + + private String eTable; //Encoding table + private char padding; + private byte[] dTable; //Decoding table + + public Base32 () + { + this (DEF_ENCODING_TABLE, DEF_PADDING); + } + + public Base32 (char padding){ + this (DEF_ENCODING_TABLE, padding); + } + public Base32 (String encodingTable){ + this (encodingTable, DEF_PADDING); + } + + public Base32 (String encodingTable, char padding) { + this.eTable = encodingTable; + this.padding = padding; + dTable = new byte[0x80]; + InitialiseDecodingTable (); + } + + public String encodeBytes (byte[] input) { + StringBuffer output = new StringBuffer (); + int specialLength = input.length % 5; + int normalLength = input.length - specialLength; + + for (int i = 0; i < normalLength; i += 5) { + int b1 = input[i] & 0xff; + int b2 = input[i + 1] & 0xff; + int b3 = input[i + 2] & 0xff; + int b4 = input[i + 3] & 0xff; + int b5 = input[i + 4] & 0xff; + + output.append(eTable.charAt((b1 >> 3) & 0x1f)); + output.append(eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append(eTable.charAt((b2 >> 1) & 0x1f)); + output.append(eTable.charAt(((b2 << 4) | (b3 >> 4)) & 0x1f)); + output.append(eTable.charAt(((b3 << 1) | (b4 >> 7)) & 0x1f)); + output.append(eTable.charAt((b4 >> 2) & 0x1f)); + output.append(eTable.charAt(((b4 << 3) | (b5 >> 5)) & 0x1f)); + output.append(eTable.charAt(b5 & 0x1f)); + } + + switch (specialLength) { + case 1: { + int b1 = input[normalLength] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt((b1 << 2) & 0x1f)); + output.append (padding).append (padding).append (padding).append (padding).append (padding).append (padding); + break; + } + + case 2: { + int b1 = input[normalLength] & 0xff; + int b2 = input[normalLength + 1] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append (eTable.charAt((b2 >> 1) & 0x1f)); + output.append (eTable.charAt((b2 << 4) & 0x1f)); + output.append (padding).append (padding).append (padding).append (padding); + break; + } + case 3: { + int b1 = input[normalLength] & 0xff; + int b2 = input[normalLength + 1] & 0xff; + int b3 = input[normalLength + 2] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append (eTable.charAt((b2 >> 1) & 0x1f)); + output.append (eTable.charAt(((b2 << 4) | (b3 >> 4)) & 0x1f)); + output.append (eTable.charAt((b3 << 1) & 0x1f)); + output.append (padding).append (padding).append (padding); + break; + } + case 4: { + int b1 = input[normalLength] & 0xff; + int b2 = input[normalLength + 1] & 0xff; + int b3 = input[normalLength + 2] & 0xff; + int b4 = input[normalLength + 3] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append (eTable.charAt((b2 >> 1) & 0x1f)); + output.append (eTable.charAt(((b2 << 4) | (b3 >> 4)) & 0x1f)); + output.append (eTable.charAt(((b3 << 1) | (b4 >> 7)) & 0x1f)); + output.append (eTable.charAt((b4 >> 2) & 0x1f)); + output.append (eTable.charAt((b4 << 3) & 0x1)); + output.append (padding); + break; + } + } + + return output.toString(); + } + + public byte[] decodeBytes (String data) { + ArrayList outStream = new ArrayList(); + + int length = data.length(); + + while (length > 0) { + if (!this.Ignore (data.charAt(length - 1))) break; + length--; + } + + int i = 0; + int finish = length - 8; + for (i = this.NextI (data, i, finish); i < finish; i = this.NextI (data, i, finish)) { + byte b1 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b2 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b3 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b4 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b5 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b6 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b7 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b8 = dTable[data.charAt(i++)]; + + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + outStream.add ((byte)((b5 << 7) | (b6 << 2) | (b7 >> 3))); + outStream.add ((byte)((b7 << 5) | b8)); + } + this.DecodeLastBlock (outStream, + data.charAt(length - 8), data.charAt(length - 7), data.charAt(length - 6), data.charAt(length - 5), + data.charAt(length - 4), data.charAt(length - 3), data.charAt(length - 2), data.charAt(length - 1)); + + byte[] result = new byte[outStream.size()]; + for(int j = 0; j < outStream.size(); j++){ + result[j] = outStream.get(j).byteValue(); + } + + return result; + } + + protected int DecodeLastBlock (ArrayList outStream, char c1, char c2, char c3, char c4, char c5, char c6, char c7, char c8) { + if (c3 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + return 1; + } + + if (c5 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + return 2; + } + + if (c6 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + byte b5 = dTable[c5]; + + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + return 3; + } + + if (c8 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + byte b5 = dTable[c5]; + byte b6 = dTable[c6]; + byte b7 = dTable[c7]; + + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + outStream.add ((byte)((b5 << 7) | (b6 << 2) | (b7 >> 3))); + return 4; + } + + else { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + byte b5 = dTable[c5]; + byte b6 = dTable[c6]; + byte b7 = dTable[c7]; + byte b8 = dTable[c8]; + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + outStream.add ((byte)((b5 << 7) | (b6 << 2) | (b7 >> 3))); + outStream.add ((byte)((b7 << 5) | b8)); + return 5; + } + } + + protected int NextI (String data, int i, int finish) { + while ((i < finish) && this.Ignore (data.charAt(i))) i++; + + return i; + } + + protected boolean Ignore (char c) { + return (c == '\n') || (c == '\r') || (c == '\t') || (c == ' ') || (c == '-'); + } + + protected void InitialiseDecodingTable () { + for (int i = 0; i < eTable.length(); i++) { + dTable[eTable.charAt(i)] = (byte)i; + } + } + + +} diff --git a/optlib/src/main/java/com/fly/optlib/utils/Base64.java b/optlib/src/main/java/com/fly/optlib/utils/Base64.java new file mode 100644 index 0000000..add8cb7 --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/utils/Base64.java @@ -0,0 +1,2068 @@ +/** + *

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.7 - Fixed subtle bug when base 64 input stream contained the + * value 01111111, which is an invalid base 64 character but should not + * throw an ArrayIndexOutOfBoundsException either. Led to discovery of + * mishandling (or potential for better handling) of other bad input + * characters. You should now get an IOException if you try decoding + * something that has bad characters in it.
  • + *
  • v2.3.6 - Fixed bug when breaking lines and the final byte of the encoded + * string ended in the last column; the buffer was not properly shrunk and + * contained an extra (null) byte that made it into the string.
  • + *
  • v2.3.5 - Fixed bug in {@link #encodeFromFile} where estimated buffer size + * was wrong for files of size 31, 34, and 37 bytes.
  • + *
  • 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(String, int, 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.7 + */ + +package com.fly.optlib.utils; + + + +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,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 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,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 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,-9 // Decimal 123 - 127 + ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 128 - 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(){} + + + + +/* ******** 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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @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 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 + + + + /** + * 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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @throws java.io.IOException if there is an error + * @throws NullPointerException if source array is null + * @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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @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 + * @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 + * @see Base64#GZIP + * @see Base64#DO_BREAK_LINES + * @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 + * @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; + OutputStream b64os = null; + + try { + // GZip -> Base64 -> ByteArray + baos = new java.io.ByteArrayOutputStream(); + b64os = new 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 ){ + // If breaking lines and the last byte falls right at + // the line length (76 bytes per line), there will be + // one extra byte, and the array will need to be resized. + // Not too bad of an estimate on array size, I'd say. + 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 ) + throws java.io.IOException { + 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 sbiDecode = 0; // Special value from DECODABET + + for( i = off; i < off+len; i++ ) { // Loop through source + + sbiDecode = DECODABET[ source[i]&0xFF ]; + + // 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++ ] = source[i]; // 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( source[i] == 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 decimal %d in array position %d", ((int)source[i])&0xFF, 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 ); + } + + + + /** + * 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, 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, 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( 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 + + OutputStream bos = null; + try { + bos = new 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 { + + OutputStream bos = null; + try{ + bos = new 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; + 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 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; + 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+1),40) ]; // Need max() for math on small files (v2.2.1); Need +1 for a few corner cases (v2.3.5) + int length = 0; + int numBytes = 0; + + // Open a stream + bis = new 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 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 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 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 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 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 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/optlib/src/main/java/com/fly/optlib/utils/Constant.java b/optlib/src/main/java/com/fly/optlib/utils/Constant.java new file mode 100644 index 0000000..243a52a --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/utils/Constant.java @@ -0,0 +1,15 @@ +package com.fly.optlib.utils; + +/** + * @author :fly + * @date :Created in 2021/7/14 17:43 + * @description: + * @modified By: + */ +public class Constant { + //hotp + public static final int TOKEN_TYPE_EVENT = 0; + //totp + public static final int TOKEN_TYPE_TIME = 1; + +} diff --git a/optlib/src/main/java/com/fly/optlib/utils/FontManager.java b/optlib/src/main/java/com/fly/optlib/utils/FontManager.java new file mode 100644 index 0000000..1da304a --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/utils/FontManager.java @@ -0,0 +1,15 @@ +package com.fly.optlib.utils; + +import android.content.Context; +import android.graphics.Typeface; + +public class FontManager { + public static final String ROOT = "fonts/", + FONTAWESOME = ROOT + "fa-regular-400.ttf", + FONTAWESOME_BRANDS = ROOT + "fa-brands-400.ttf", + FONTAWESOME_SOLID = ROOT + "fa-solid-900.ttf"; + + public static Typeface getTypeface(Context context, String font) { + return Typeface.createFromAsset(context.getAssets(), font); + } +} diff --git a/optlib/src/main/java/com/fly/optlib/utils/SeedConvertor.java b/optlib/src/main/java/com/fly/optlib/utils/SeedConvertor.java new file mode 100644 index 0000000..c9a9360 --- /dev/null +++ b/optlib/src/main/java/com/fly/optlib/utils/SeedConvertor.java @@ -0,0 +1,64 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2011 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package com.fly.optlib.utils; + +import com.fly.optlib.tokens.HotpToken; + +import java.io.IOException; + + + +public class SeedConvertor { + + public static final int HEX_FORMAT = 0; + public static final int BASE32_FORMAT = 1; + public static final int BASE64_FORMAT = 2; + + public static byte[] ConvertFromEncodingToBA(String input, int currentFormat) throws IOException{ + + if(currentFormat == 0){ + //hex + return HotpToken.stringToHex(input); + }else if(currentFormat == 1){ + //base 32 + Base32 base32 = new Base32(); + return base32.decodeBytes(input.toUpperCase()); + }else if(currentFormat == 2){ + //base64 + return Base64.decode(input); + }else + return null; + } + + public static String ConvertFromBA(byte[] input, int targetFormat){ + if(targetFormat == 0){ + //hex + return HotpToken.byteArrayToHexString(input); + }else if(targetFormat == 1){ + //base 32 + Base32 base32 = new Base32(); + return base32.encodeBytes(input); + }else if(targetFormat == 2){ + //base64 + return Base64.encodeBytes(input); + }else + return null; + } +} diff --git a/optlib/src/test/java/com/fly/optlib/ExampleUnitTest.java b/optlib/src/test/java/com/fly/optlib/ExampleUnitTest.java new file mode 100644 index 0000000..7616a95 --- /dev/null +++ b/optlib/src/test/java/com/fly/optlib/ExampleUnitTest.java @@ -0,0 +1,63 @@ +package com.fly.optlib; + +import android.util.Base64; + +import com.fly.optlib.tokens.HotpToken; +import com.fly.optlib.utils.Base32; +import com.fly.optlib.utils.SeedConvertor; + +import org.junit.Test; + +import java.io.IOException; + +import static org.junit.Assert.*; + +/** + * Example local unit test, which will execute on the development machine (host). + * + * @see Testing documentation + */ +public class ExampleUnitTest { + @Test + public void hotpCreate() { + HotpToken token = new HotpToken("mark", "123456789", "6300046", 1, 6,"fly"); + String otp = token.generateOtp(); + System.out.println(otp); + } + + @Test + public void seed() throws IOException { + String seed="63000018"; + seed = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(seed, 1), 0); + + System.out.println(seed); + HotpToken token = new HotpToken("mark", "123456789", seed, 1, 6,"dindin"); + String otp = token.generateOtp(); + System.out.println(otp); + String url=token.getUrl(); + System.out.println("url "+url); + + + + + + + +// 加密传入的数据是byte类型的,并非使用decode方法将原始数据转二进制,String类型的数据 使用 str.getBytes()即可 + String str = url; +// 在这里使用的是encode方式,返回的是byte类型加密数据,可使用new String转为String类型 + String strBase64 = new String(Base64.encode(str.getBytes(), Base64.DEFAULT)); + System.out.println("encode >>>" + strBase64); + +// 这里 encodeToString 则直接将返回String类型的加密数据 + String enToStr = Base64.encodeToString(str.getBytes(), Base64.DEFAULT); + System.out.println("encodeToString >>> " + enToStr); + +// 对base64加密后的数据进行解密 + System.out.println("decode >>>" + new String(Base64.decode(strBase64.getBytes(), Base64.DEFAULT))); + + + } + + +} \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 71a8b31..8b9bb32 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,4 @@ +include ':demo' +include ':optlib' include ':app' rootProject.name = "otp" \ No newline at end of file diff --git a/upload_version.gradle b/upload_version.gradle new file mode 100644 index 0000000..fb0fbea --- /dev/null +++ b/upload_version.gradle @@ -0,0 +1,15 @@ +ext { + groupId = "com.fly.otp" + release_url='' + snapshot_url='' + maven_name='' + maven_password='' + + +// 依赖本地项目还是依赖远程仓库 + is_remote_maven=false + cfgvs = [ + optlib : "0.0.2", + ] + +} \ No newline at end of file From 2222fa8ea1c7ef840f1a692d03cc1c984590c76e Mon Sep 17 00:00:00 2001 From: fly Date: Fri, 16 Jul 2021 11:05:16 +0800 Subject: [PATCH 3/5] =?UTF-8?q?build:=E4=B8=8A=E4=BC=A0github?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ optlib/build.gradle | 1 + 2 files changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index d1fb75d..4aa146a 100644 --- a/build.gradle +++ b/build.gradle @@ -12,6 +12,7 @@ buildscript { classpath "com.android.tools.build:gradle:4.1.3" classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" + classpath 'com.github.dcendents:android-maven-gradle-plugin:2.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files } @@ -21,6 +22,7 @@ allprojects { repositories { google() jcenter() + maven { url "https://jitpack.io" } maven { url 'https://maven.aliyun.com/repository/public' } diff --git a/optlib/build.gradle b/optlib/build.gradle index bd4ff32..593fd43 100644 --- a/optlib/build.gradle +++ b/optlib/build.gradle @@ -1,5 +1,6 @@ plugins { id 'com.android.library' + id 'com.github.dcendents.android-maven' } apply from: 'push.gradle' android { From b62e9e4b8a57a95b88f747ff144958b642b93108 Mon Sep 17 00:00:00 2001 From: fly Date: Fri, 16 Jul 2021 16:07:53 +0800 Subject: [PATCH 4/5] =?UTF-8?q?build:=E4=B8=8A=E4=BC=A0github2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- optlib/build.gradle | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/optlib/build.gradle b/optlib/build.gradle index 593fd43..491cc44 100644 --- a/optlib/build.gradle +++ b/optlib/build.gradle @@ -1,7 +1,9 @@ plugins { id 'com.android.library' - id 'com.github.dcendents.android-maven' + id 'maven-publish' } +group = 'com.github.flyopensource' +version = '1.0' apply from: 'push.gradle' android { compileSdkVersion 30 @@ -39,4 +41,17 @@ dependencies { testImplementation 'junit:junit:4.+' androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' +} +afterEvaluate { + publishing { + publications { + // Creates a Maven publication called "release". + release(MavenPublication) { + from components.release + groupId = 'com.fly.opt' + artifactId = 'lib' + version = '1.0' + } + } + } } \ No newline at end of file From 80fee31a9a8dc72ae862e9f7bda04aeede4afe79 Mon Sep 17 00:00:00 2001 From: fly Date: Fri, 16 Jul 2021 16:19:16 +0800 Subject: [PATCH 5/5] =?UTF-8?q?build:=E6=9E=84=E5=BB=BAok=20=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E7=89=88=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- demo/build.gradle | 9 ++------- optlib/build.gradle | 5 +++-- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/demo/build.gradle b/demo/build.gradle index f6e1430..2d32744 100644 --- a/demo/build.gradle +++ b/demo/build.gradle @@ -44,11 +44,6 @@ dependencies { androidTestImplementation 'androidx.test.ext:junit:1.1.3' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' - if(is_remote_maven){ - //com.fly.otp:optlib:0.0.1 - implementation "$groupId:optlib:$cfgvs.optlib" -// implementation "com.fly.otp:optlib:0.0.1" - }else { - implementation project(path: ':optlib') - } +// implementation project(path: ':optlib') + implementation "com.github.flyopensource:androidtoken:b62e9e4b8a" } \ No newline at end of file diff --git a/optlib/build.gradle b/optlib/build.gradle index 491cc44..88676b3 100644 --- a/optlib/build.gradle +++ b/optlib/build.gradle @@ -46,10 +46,11 @@ afterEvaluate { publishing { publications { // Creates a Maven publication called "release". + //这里的 可能没啥用,但也不能取消 release(MavenPublication) { from components.release - groupId = 'com.fly.opt' - artifactId = 'lib' + groupId = 'com.github.flyopensource' + artifactId = 'otplib' version = '1.0' } }

?37MH!0WI0j&U(dp&T! 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 b$muU9;@@)p`~2JgIQEbKyxjgA_t*aeWB-ER literal 0 HcmV?d00001 diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/About.java b/app/src/main/java/uk/co/bitethebullet/android/token/About.java new file mode 100644 index 0000000..8018f72 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/About.java @@ -0,0 +1,33 @@ +package uk.co.bitethebullet.android.token; + +import android.os.Bundle; +import android.text.method.LinkMovementMethod; +import android.view.View; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; + +import androidx.appcompat.app.AppCompatActivity; + +public class About extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.about); + + setupHyperlink(); + } + + public void closeHandler(View v){ + finish(); + } + + private void setupHyperlink() { + TextView linkTextView = findViewById(R.id.about_url); + TextView linkZalabria = findViewById(R.id.about_info); + + linkTextView.setMovementMethod(LinkMovementMethod.getInstance()); + linkZalabria.setMovementMethod(LinkMovementMethod.getInstance()); + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/PinChange.java b/app/src/main/java/uk/co/bitethebullet/android/token/PinChange.java new file mode 100644 index 0000000..a7ef5e4 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/PinChange.java @@ -0,0 +1,135 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +import androidx.appcompat.app.AppCompatActivity; + +public class PinChange extends AppCompatActivity { + + private static final int DIALOG_INVALID_EXISTING_PIN = 0; + private static final int DIALOG_DIFF_NEW_PIN = 1; + private static final int DIALOG_NO_NEW_PIN = 2; + + Boolean hasExistingPin = true; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.pinchange); + + if(!PinManager.hasPinDefined(this)){ + hasExistingPin = false; + EditText existPinEdit = (EditText)findViewById(R.id.pinChangeExistingPinEdit); + existPinEdit.setEnabled(false); + } + + Button submitBtn = (Button)findViewById(R.id.pinChangeSubmit); + submitBtn.setOnClickListener(submitClick); + } + + private OnClickListener submitClick = new OnClickListener() { + + public void onClick(View v) { + //validate the existing pin + if(hasExistingPin){ + + String existingPin = ((EditText)findViewById(R.id.pinChangeExistingPinEdit)).getText().toString(); + + if(!PinManager.validatePin(v.getContext(), existingPin)){ + //the pin entered is not the one stored, show + //warning and stop + showDialog(DIALOG_INVALID_EXISTING_PIN); + return; + } + } + + //validate the two new pins match + String newPin1 = ((EditText)findViewById(R.id.pinChangeNew1Edit)).getText().toString(); + String newPin2 = ((EditText)findViewById(R.id.pinChangeNew2Edit)).getText().toString(); + + if(newPin1.length() == 0){ + showDialog(DIALOG_NO_NEW_PIN); + return; + } + + if(!newPin1.contentEquals(newPin2)){ + showDialog(DIALOG_DIFF_NEW_PIN); + return; + } + + //store + PinManager.storePin(v.getContext(), newPin1); + finish(); + } + }; + + private Dialog createAlertDialog(int messageId){ + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(messageId) + .setCancelable(false) + .setPositiveButton(R.string.dialogPositive, dialogClose); + + return builder.create(); + + } + + @Override + protected Dialog onCreateDialog(int id) { + Dialog d; + + switch(id){ + case DIALOG_DIFF_NEW_PIN: + d = createAlertDialog(R.string.pinAlertNewPinsDifferent); + break; + + case DIALOG_INVALID_EXISTING_PIN: + d = createAlertDialog(R.string.pinAlertInvalidPin); + break; + + case DIALOG_NO_NEW_PIN: + d = createAlertDialog(R.string.pinAlertNewPinBlank); + break; + + default: + d = null; + } + + return d; + } + + private DialogInterface.OnClickListener dialogClose = new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }; + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/PinManager.java b/app/src/main/java/uk/co/bitethebullet/android/token/PinManager.java new file mode 100644 index 0000000..8a9a760 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/PinManager.java @@ -0,0 +1,91 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +import android.content.Context; +import android.content.SharedPreferences; +import android.database.Cursor; + +import androidx.preference.PreferenceManager; + +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; +import uk.co.bitethebullet.android.token.tokens.HotpToken; + +public class PinManager { + + private static final String SALT = "EE08F4A6-8497-4330-8CD5-8A4ABD93CD46"; + public static final String PIN_CODE_SETTING = "PIN_CODE"; + + public static Boolean hasPinDefined(Context c){ + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(c); + + return sharedPreferences.getString("securityLock", "0").equals("1"); + } + + public static Boolean validatePin(Context c, String pin){ + + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(c); + String storedPin = sharedPreferences.getString(PIN_CODE_SETTING, ""); + + return storedPin.equals(pin); + } + + public static void storePin(Context c, String pin){ + SharedPreferences sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(c); + SharedPreferences.Editor editor = sharedPreferences.edit(); + editor.putString(PinManager.PIN_CODE_SETTING, pin); + editor.commit(); + } + +// public static void removePin(Context c){ +// TokenDbAdapter db = new TokenDbAdapter(c); +// db.open(); +// +// db.deletePin(); +// +// db.close(); +// } + + private static String createPinHash(String pin) { + + try{ + + String toHash = SALT + pin; + + MessageDigest md = MessageDigest.getInstance("SHA1"); + md.reset(); + md.update(toHash.getBytes()); + byte[] hashOutput = md.digest(); + + return HotpToken.byteArrayToHexString(hashOutput); + + }catch(NoSuchAlgorithmException ex){ + return null; + } + + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/PinRemove.java b/app/src/main/java/uk/co/bitethebullet/android/token/PinRemove.java new file mode 100644 index 0000000..85ee9b2 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/PinRemove.java @@ -0,0 +1,102 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.Button; +import android.widget.EditText; + +public class PinRemove extends Activity { + + private static final int DIALOG_INVALID_PIN = 0; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.pinremove); + + Button removePinBtn = (Button)findViewById(R.id.pinRemoveSubmit); + removePinBtn.setOnClickListener(removeBtn); + } + + private OnClickListener removeBtn = new OnClickListener() { + + public void onClick(View v) { + //valid the pin + + String pin = ((EditText)findViewById(R.id.pinRemoveExistingPinEdit)).getText().toString(); + + if(PinManager.validatePin(v.getContext(), pin)){ + //todo: MM fix me + //PinManager.removePin(v.getContext()); + finish(); + }else{ + // the pin isn't the same as the one stored, do nothing + showDialog(DIALOG_INVALID_PIN); + return; + } + + } + }; + + @Override + protected Dialog onCreateDialog(int id) { + Dialog d; + + switch(id){ + + case DIALOG_INVALID_PIN: + d = createAlertDialog(R.string.pinAlertInvalidPin); + break; + + default: + d = null; + break; + + } + + return d; + } + + private Dialog createAlertDialog(int messageId){ + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(messageId) + .setCancelable(false) + .setPositiveButton(R.string.dialogPositive, dialogClose); + + return builder.create(); + + } + + private DialogInterface.OnClickListener dialogClose = new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }; + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/QRCodeActivity.java b/app/src/main/java/uk/co/bitethebullet/android/token/QRCodeActivity.java new file mode 100644 index 0000000..55abffc --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/QRCodeActivity.java @@ -0,0 +1,58 @@ +package uk.co.bitethebullet.android.token; + +import androidx.appcompat.app.AppCompatActivity; + +import android.graphics.Bitmap; +import android.graphics.Color; +import android.graphics.Point; +import android.os.Bundle; +import android.util.Log; +import android.view.Display; +import android.view.WindowManager; +import android.widget.ImageView; +import android.widget.TextView; + +import com.google.zxing.WriterException; + +import org.w3c.dom.Text; + +import androidmads.library.qrgenearator.QRGContents; +import androidmads.library.qrgenearator.QRGEncoder; + +public class QRCodeActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + String qrUrl = getIntent().getStringExtra("qrUrl"); + String fullName = getIntent().getStringExtra("fullName"); + + WindowManager manager = (WindowManager) getSystemService(WINDOW_SERVICE); + Display display = manager.getDefaultDisplay(); + Point point = new Point(); + display.getSize(point); + int width = point.x; + int height = point.y; + int smallerDimension = width < height ? width : height; + smallerDimension = smallerDimension * 3 / 4; + + + setContentView(R.layout.activity_q_r_code); + + ImageView qrImage = (ImageView) findViewById(R.id.qr_code_image); + TextView qrFullName = (TextView) findViewById(R.id.qr_code_full_name); + qrFullName.setText(fullName); + + QRGEncoder qrgEncoder = new QRGEncoder(qrUrl, null, QRGContents.Type.TEXT, smallerDimension); + + try { + // Getting QR-Code as Bitmap + Bitmap bitmap = qrgEncoder.getBitmap(); + // Setting Bitmap to ImageView + qrImage.setImageBitmap(bitmap); + } catch (Exception e) { + Log.e("QRCode", e.getMessage()); + } + } +} \ No newline at end of file diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/SettingActivity.java b/app/src/main/java/uk/co/bitethebullet/android/token/SettingActivity.java new file mode 100644 index 0000000..b3acdf8 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/SettingActivity.java @@ -0,0 +1,21 @@ +package uk.co.bitethebullet.android.token; + +import androidx.appcompat.app.AppCompatActivity; + +import android.app.Activity; +import android.os.Bundle; + +public class SettingActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_setting); + + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.fragContainer, new SettingsFragment()) + .commit(); + + } +} \ No newline at end of file diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/SettingsFragment.java b/app/src/main/java/uk/co/bitethebullet/android/token/SettingsFragment.java new file mode 100644 index 0000000..9dac0f8 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/SettingsFragment.java @@ -0,0 +1,124 @@ +package uk.co.bitethebullet.android.token; + +import androidx.biometric.BiometricManager; + +import android.app.Activity; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; + +import androidx.fragment.app.DialogFragment; +import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; +import androidx.preference.ListPreference; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.PreferenceManager; + +import java.util.Arrays; + +import uk.co.bitethebullet.android.token.dialogs.PinDefintionDialog; + +public class SettingsFragment extends PreferenceFragmentCompat { + + private FragmentActivity myContext; + public static final int PIN_DEFINITION_DIALOG_FRAGMENT = 1; + + @Override + public void onAttach(Activity activity) { + myContext=(FragmentActivity) activity; + super.onAttach(activity); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + + final ListPreference lockPreferences = (ListPreference) findPreference("securityLock"); + final Fragment settingFragment = this; + + lockPreferences.setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + + setLockPreferencesData(lockPreferences); + return false; + } + }); + + lockPreferences.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() { + + @Override + public boolean onPreferenceChange(Preference preference, + Object newValue) { + + String oldValue = lockPreferences.getValue(); + + Log.d("Preference", "IF show pin entry number"); + + //if we have change to use a PIN code we need to get the + //new pin value to secure the app + if(newValue.toString().equals("1") && !oldValue.equals("1")){ + + Log.d("Preference", "show pin entry number"); + + DialogFragment newPinDefintion = new PinDefintionDialog(); + newPinDefintion.setTargetFragment(settingFragment, PIN_DEFINITION_DIALOG_FRAGMENT); + newPinDefintion.show(myContext.getSupportFragmentManager(), "pinNewDefintion"); + } + + Log.d("Preference", "Old Value: " + oldValue + ", New Value: " + newValue.toString()); + return true; + } + }); + } + + protected void setLockPreferencesData(ListPreference lp) { + CharSequence[] entries = this.getContext() + .getResources() + .getStringArray(R.array.listSecurityPrefKeys); + + CharSequence[] entryValues = this.getContext() + .getResources() + .getStringArray(R.array.listSecurityPrefValues); + + //if the device doesn't support biometric security + //then we'll just remove that option for the list + if(!getDeviceSupportsBioMetrics()){ + + entries = Arrays.copyOf(entries, 2); + entryValues = Arrays.copyOf(entryValues, 2); + } + + lp.setEntries(entries); + lp.setEntryValues(entryValues); + } + + protected boolean getDeviceSupportsBioMetrics(){ + BiometricManager biometricManager = BiometricManager.from(this.getContext()); + return biometricManager.canAuthenticate() == BiometricManager.BIOMETRIC_SUCCESS; + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch(requestCode) { + case PIN_DEFINITION_DIALOG_FRAGMENT: + + if (resultCode == Activity.RESULT_OK) { + // After Ok code. + Log.d("settings", "PIN set"); + + } else if (resultCode == Activity.RESULT_CANCELED){ + // After Cancel code. + Log.d("settings", "PIN Cancel"); + final ListPreference lockPreferences = (ListPreference) findPreference("securityLock"); + lockPreferences.setValue("0"); + } + + break; + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/TokenAdd.java b/app/src/main/java/uk/co/bitethebullet/android/token/TokenAdd.java new file mode 100644 index 0000000..964f454 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/TokenAdd.java @@ -0,0 +1,550 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.DialogInterface; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.View.OnClickListener; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.LinearLayout; +import android.widget.RadioButton; +import android.widget.RelativeLayout; +import android.widget.Spinner; +import android.widget.TextView; +import android.widget.AdapterView.OnItemSelectedListener; + +import androidx.appcompat.app.AppCompatActivity; + +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; +import uk.co.bitethebullet.android.token.tokens.HotpToken; +import uk.co.bitethebullet.android.token.util.*; + +public class TokenAdd extends AppCompatActivity { + + private static final int DIALOG_STEP1_NO_NAME = 0; + private static final int DIALOG_STEP1_NO_SERIAL = 1; + private static final int DIALOG_STEP2_NO_SEED = 2; + private static final int DIALOG_STEP2_INVALID_SEED_TOO_SHORT = 3; + private static final int DIALOG_STEP2_INVALID_SEED_NOT_HEX = 4; + private static final int DIALOG_STEP2_SEED_CONVERT_ERROR = 5; + private static final int DIALOG_STEP2_UNABLE_TO_SWITCH_FORMAT = 6; + + //defines the define steps the activity can display + private static final int ACTIVITY_STEP_ONE = 0; + private static final int ACTIVITY_STEP_TWO = 1; + + private static final String KEY_ACTIVITY_STATE = "currentState"; + private static final String KEY_TOKEN_TYPE = "tokenType"; + private static final String KEY_OTP_LENGTH = "otpLength"; + private static final String KEY_TIME_STEP = "timeStep"; + private static final String KEY_NAME = "tokenName"; + private static final String KEY_SERIAL = "tokenSerial"; + private static final String KEY_SEED_FORMAT = "tokenSeedFormat"; + private static final String KEY_ORGANISATION = "tokenOrganisation"; + + //current state of the activity + private int mCurrentActivityStep = ACTIVITY_STEP_ONE; + + //holds the data from step 1 + private String mName; + private String mOrganisation; + private String mSerial; + private int mTokenType; + private int mOtpLength; + private int mTimeStep; + private int mTokenSeedFormat; + private Boolean mAcceptWeakSeed; + + private static final int RANDOM_SEED_LENGTH = 160; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mAcceptWeakSeed = false; + + setContentView(R.layout.token_add); + + loadSpinnerArrayData(R.id.tokenTypeSpinner, R.array.tokenType, 1); + loadSpinnerArrayData(R.id.tokenOtpSpinner, R.array.otpLength); + loadSpinnerArrayData(R.id.tokenTimeStepSpinner, R.array.timeStep); + loadSpinnerArrayData(R.id.tokenSeedFormat, R.array.tokenSeedFormatType, 1); + + if(savedInstanceState != null){ + mCurrentActivityStep = savedInstanceState.getInt(KEY_ACTIVITY_STATE); + + int tokenType = savedInstanceState.getInt(KEY_TOKEN_TYPE); + int otpLength = savedInstanceState.getInt(KEY_OTP_LENGTH); + int timeStep = savedInstanceState.getInt(KEY_TIME_STEP); + String tokenName = savedInstanceState.getString(KEY_NAME); + String tokenSerial = savedInstanceState.getString(KEY_SERIAL); + String tokenOrganisation = savedInstanceState.getString(KEY_ORGANISATION); + + if(mCurrentActivityStep == ACTIVITY_STEP_TWO){ + //step 2 + mName = tokenName; + mSerial = tokenSerial; + mTokenType = tokenType; + mOtpLength = otpLength; + mTimeStep = timeStep; + mOrganisation = tokenOrganisation; + + showStepTwo(); + + }else{ + //step 1 + ((Spinner)findViewById(R.id.tokenTypeSpinner)).setSelection(tokenType); + ((Spinner)findViewById(R.id.tokenOtpSpinner)).setSelection(otpLength); + ((Spinner)findViewById(R.id.tokenTimeStepSpinner)).setSelection(timeStep); + } + } + + Button btnNext = (Button)findViewById(R.id.btnAddStep2); + btnNext.setOnClickListener(buttonNext); + + RadioButton rbManual = (RadioButton)findViewById(R.id.rbSeedManual); + RadioButton rbRandom = (RadioButton)findViewById(R.id.rbSeedRandom); + RadioButton rbPassword = (RadioButton)findViewById(R.id.rbSeedPassword); + + rbManual.setOnClickListener(radioSeed); + rbRandom.setOnClickListener(radioSeed); + rbPassword.setOnClickListener(radioSeed); + + Button btnComplete = (Button)findViewById(R.id.tokenAddComplete); + btnComplete.setOnClickListener(buttonComplete); + + Spinner tokenType = (Spinner)findViewById(R.id.tokenTypeSpinner); + tokenType.setOnItemSelectedListener(tokenTypeSelected); + + Spinner tokenSeedFormat = (Spinner)findViewById(R.id.tokenSeedFormat); + tokenSeedFormat.setOnItemSelectedListener(tokenSeedFormatSelected); + } + + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + int tokenType; + int otpLength; + int timeStep; + + if(mCurrentActivityStep == ACTIVITY_STEP_ONE){ + tokenType = ((Spinner)findViewById(R.id.tokenTypeSpinner)).getSelectedItemPosition(); + otpLength = ((Spinner)findViewById(R.id.tokenOtpSpinner)).getSelectedItemPosition(); + timeStep = ((Spinner)findViewById(R.id.tokenTimeStepSpinner)).getSelectedItemPosition(); + + outState.putInt(KEY_TOKEN_TYPE, tokenType); + outState.putInt(KEY_OTP_LENGTH, otpLength); + outState.putInt(KEY_TIME_STEP, timeStep); + }else{ + outState.putInt(KEY_TOKEN_TYPE, mTokenType); + outState.putInt(KEY_OTP_LENGTH, mOtpLength); + outState.putInt(KEY_TIME_STEP, mTimeStep); + } + + outState.putInt(KEY_ACTIVITY_STATE, mCurrentActivityStep); + outState.putString(KEY_NAME, mName); + outState.putString(KEY_SERIAL, mSerial); + outState.putString(KEY_ORGANISATION, mOrganisation); + } + + + private OnItemSelectedListener tokenTypeSelected = new OnItemSelectedListener() { + + public void onItemSelected(AdapterView arg0, View arg1, int arg2, + long arg3) { + + TextView caption = (TextView)findViewById(R.id.tokenTimeStep); + Spinner spinner = (Spinner)findViewById(R.id.tokenTimeStepSpinner); + + if(arg2 == 0){ + caption.setVisibility(View.INVISIBLE); + spinner.setVisibility(View.INVISIBLE); + }else{ + caption.setVisibility(View.VISIBLE); + spinner.setVisibility(View.VISIBLE); + } + + } + + public void onNothingSelected(AdapterView arg0) { + //ignore + } + + }; + + + private OnItemSelectedListener tokenSeedFormatSelected = new OnItemSelectedListener() { + + public void onItemSelected(AdapterView arg0, View arg1, int arg2, + long arg3) { + + EditText tokenSeedEdit = (EditText)findViewById(R.id.tokenSeedEdit); + String seed = tokenSeedEdit.getText().toString(); + + try{ + //only convert if we have something entered + if(seed.length() > 0) { + seed = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(seed, mTokenSeedFormat), arg2); + } + } + catch(Exception ex){ + //cancel the change of seed format, if have + //some error + showDialog(DIALOG_STEP2_UNABLE_TO_SWITCH_FORMAT); + + Spinner tokenSeedFormat = (Spinner)findViewById(R.id.tokenSeedFormat); + tokenSeedFormat.setSelection(mTokenSeedFormat); + return; + } + + tokenSeedEdit.setText(seed); + mTokenSeedFormat = arg2; + } + + public void onNothingSelected(AdapterView arg0) { + // ignore not required + } + + }; + + + + @Override + protected Dialog onCreateDialog(int id) { + Dialog d; + + switch(id){ + case DIALOG_STEP1_NO_NAME: + d = createAlertDialog(R.string.tokenAddDialogNoName); + break; + + case DIALOG_STEP1_NO_SERIAL: + d = createAlertDialog(R.string.tokenAddDialogNoSerial); + break; + + case DIALOG_STEP2_NO_SEED: + d = createAlertDialog(R.string.tokenAddDialogNoSeed); + break; + + case DIALOG_STEP2_INVALID_SEED_TOO_SHORT: + d = createAlertDialog(R.string.tokenAddDialogInvalidSeedTooShort2); + break; + + case DIALOG_STEP2_INVALID_SEED_NOT_HEX: + d = createAlertDialog(R.string.tokenAddDialogInvalidSeedNotHex); + break; + + case DIALOG_STEP2_SEED_CONVERT_ERROR: + d = createAlertDialog(R.string.tokenAddDialogInvalidSeedConvertion); + break; + + case DIALOG_STEP2_UNABLE_TO_SWITCH_FORMAT: + d = createAlertDialog(R.string.tokenAddDialogUnableToSwitchFormat); + break; + + default: + d = null; + } + + return d; + } + + private Dialog createAlertDialog(int messageId){ + return this.createAlertDialog(messageId, null, dialogClose, null); + } + + private Dialog createAlertDialog(int messageId, + String additionalMessage, + DialogInterface.OnClickListener positiveClick, + DialogInterface.OnClickListener negativeClick){ + + AlertDialog.Builder builder = new AlertDialog.Builder(this); + builder.setMessage(messageId) + .setPositiveButton(R.string.dialogPositive, positiveClick); + + if(negativeClick != null){ + builder.setNegativeButton(R.string.cancel, negativeClick); + }else{ + builder.setCancelable(false); + } + + //load the resource and optional additional information + if(additionalMessage != null){ + builder.setMessage(String.format(getResources().getString(messageId), additionalMessage)); + } + + return builder.create(); + } + + private DialogInterface.OnClickListener dialogClose = new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int which) { + dialog.dismiss(); + } + }; + + /*** + * handles the user accepting the weak seed when we warn then in a dialod + */ + private DialogInterface.OnClickListener dialogAcceptWeakSeed = new DialogInterface.OnClickListener() { + + public void onClick(DialogInterface dialog, int which) { + Log.d("Android Token", "dialogAcceptWeakSeed called"); + mAcceptWeakSeed = true; + dialog.dismiss(); + + buttonComplete.onClick(getCurrentFocus()); + mAcceptWeakSeed = false; + } + }; + + private OnClickListener buttonNext = new OnClickListener() { + + public void onClick(View v) { + //validate we have the required completed fields + + boolean isValid = true; + + String name = ((EditText)findViewById(R.id.tokenNameEdit)).getText().toString(); + String organisation = ((EditText)findViewById(R.id.tokenOrganisationEdit)).getText().toString(); + String serial = ((EditText)findViewById(R.id.tokenSerialEdit)).getText().toString(); + + if(name.length() == 0){ + isValid = false; + showDialog(DIALOG_STEP1_NO_NAME); + } + + if(isValid){ + //store step 1 values in members vars + mName = name; + mOrganisation = organisation; + mSerial = serial; + mTokenType = ((Spinner)findViewById(R.id.tokenTypeSpinner)).getSelectedItemPosition();; + mOtpLength = Integer.parseInt(((Spinner)findViewById(R.id.tokenOtpSpinner)).getSelectedItem().toString()); + mTimeStep = ((Spinner)findViewById(R.id.tokenTimeStepSpinner)).getSelectedItemPosition() == 0 ? 30 : 60; + + showStepTwo(); + mCurrentActivityStep = ACTIVITY_STEP_TWO; + } + } + }; + + private OnClickListener buttonComplete = new OnClickListener() { + + public void onClick(View v) { + //validate we have a valid serial + Boolean isValid = true; + + RadioButton rbPassword = (RadioButton)findViewById(R.id.rbSeedPassword); + String seed = ((EditText)findViewById(R.id.tokenSeedEdit)).getText().toString(); + + try { + //if the seed is not in hex format convert this to hex + //then make sure the length is + if(mTokenSeedFormat == 1){ + //base32 + seed = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(seed, 1), 0); + }else if(mTokenSeedFormat == 2){ + //base 64 + seed = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(seed, 2), 0); + } + } catch (Exception e) { + showDialog(DIALOG_STEP2_SEED_CONVERT_ERROR); + return; + } + + if(seed.length() == 0){ + isValid = false; + showDialog(DIALOG_STEP2_NO_SEED); + return; + } + + if(!rbPassword.isChecked()){ + + //validate the length + int seedLength = seed.length(); + + //valid the chars in the seed + Pattern p = Pattern.compile("[A-Fa-f0-9]*"); + Matcher matcher = p.matcher(seed); + + if(!matcher.matches()){ + showDialog(DIALOG_STEP2_INVALID_SEED_NOT_HEX); + return; + } + + if(seedLength < 2){ + showDialog(DIALOG_STEP2_INVALID_SEED_TOO_SHORT); + return; + } + else if(seedLength < 32 && !mAcceptWeakSeed){ + isValid = false; + + //the seed is shorter than the 128bit minimum as recommended + //in the RFC, therefore warn the user but give them then + //ability to create a weak seed anyway + + Dialog d = createAlertDialog(R.string.tokenAddDialogInvalidSeedTooShort, + "" + seedLength * 4, + dialogAcceptWeakSeed, + dialogClose); + d.show(); + + return; + } + + }else{ + //when creating a seed from password we simple sha1 the data then + //use that as a seed to to concat with the data again + // + // h1 = sha1(password) + // h2 = sha1(password + h1) + // + //h2 should then be stored as a hex string in the database + try{ + + byte[] input = seed.getBytes(); + MessageDigest md = MessageDigest.getInstance("SHA1"); + + md.reset(); + byte[] h1 = md.digest(input); + md.reset(); + byte[] h2 = md.digest(mergeByteArray(input, h1)); + + seed = HotpToken.byteArrayToHexString(h2); + + }catch(NoSuchAlgorithmException nsae){ + + } + + } + + if(isValid){ + //store token in db + TokenDbAdapter db = new TokenDbAdapter(v.getContext()); + db.open(); + db.createToken(mName, mSerial, seed, + mTokenType, mOtpLength, + mTimeStep, mOrganisation); + db.close(); + + setResult(RESULT_OK); + + finish(); + } + + } + }; + + private byte[] mergeByteArray(byte[] b1, byte[] b2){ + + byte[] result = new byte[b1.length + b2.length]; + + int i = 0; + + for(byte b : b1){ + result[i] = b; + i++; + } + + for(byte b : b2){ + result[i] = b; + i++; + } + + return result; + } + + private OnClickListener radioSeed = new OnClickListener() { + + public void onClick(View v) { + RadioButton rb = (RadioButton)v; + + //if we are entering the seed manually/randomly we should display the + //the format option to allow the user to enter as hex/base64/base32 + + if(rb.getId() == R.id.rbSeedRandom){ + EditText seedEdit = (EditText)findViewById(R.id.tokenSeedEdit); + + try{ + //create a new random seed, this will be in hex format. + //see if we need to convert this to whatever we have input + //format selected as + String hexSeed = HotpToken.generateNewSeed(RANDOM_SEED_LENGTH); + byte[] ba = SeedConvertor.ConvertFromEncodingToBA(hexSeed, + SeedConvertor.HEX_FORMAT); + + seedEdit.setText(SeedConvertor.ConvertFromBA(ba, mTokenSeedFormat)); + }catch(Exception ex){ + } + + } + + Spinner tokenSeedFormat = (Spinner)findViewById(R.id.tokenSeedFormat); + + if(rb.getId() == R.id.rbSeedManual || rb.getId() == R.id.rbSeedRandom){ + tokenSeedFormat.setEnabled(true); + }else{ + tokenSeedFormat.setEnabled(false); + } + } + }; + + private void loadSpinnerArrayData(int spinnerId, int arrayData){ + loadSpinnerArrayData(spinnerId, arrayData, -1); + } + + private void loadSpinnerArrayData(int spinnerId, int arrayData, int selectedPosition){ + Spinner spinner = (Spinner)findViewById(spinnerId); + ArrayAdapter adapter = ArrayAdapter.createFromResource(this, + arrayData, + android.R.layout.simple_spinner_item); + adapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + spinner.setAdapter(adapter); + + if(selectedPosition >= 0) { + spinner.setSelection(selectedPosition); + } + } + + + private void showStepTwo() { + //show the next step + RelativeLayout step1 = (RelativeLayout)findViewById(R.id.tokenAddStep1); + RelativeLayout step2 = (RelativeLayout)findViewById(R.id.tokenAddStep2); + + step1.setVisibility(View.GONE); + step2.setVisibility(View.VISIBLE); + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/TokenCountDownView.java b/app/src/main/java/uk/co/bitethebullet/android/token/TokenCountDownView.java new file mode 100644 index 0000000..fe9fe13 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/TokenCountDownView.java @@ -0,0 +1,42 @@ +package uk.co.bitethebullet.android.token; + +/////////////////////////////////////// +//this is not used!! +////////////////////////////////////// + + +import android.content.Context; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.view.View; + +public class TokenCountDownView extends View { + + public TokenCountDownView(Context context){ + super(context); + } + + @Override + protected void onDraw(Canvas canvas) { + + Paint p = new Paint(); + p.setAntiAlias(true); + p.setStyle(Paint.Style.FILL); + p.setColor(0x88FF0000); + + RectF oval = new RectF( 0, 0, 50, 50); + drawArc(canvas, oval, true, p); + } + + + private void drawArc(Canvas canvas, RectF oval, boolean useCenter, Paint paint) { + + float mStart = 0; + float mSweep = 360; + + canvas.drawArc(oval, mStart, mSweep, useCenter, paint); + } + + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/TokenList.java b/app/src/main/java/uk/co/bitethebullet/android/token/TokenList.java new file mode 100644 index 0000000..2f51948 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/TokenList.java @@ -0,0 +1,681 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; +import java.util.Timer; +import java.util.TimerTask; + +import uk.co.bitethebullet.android.token.adapters.TokenAdapter; +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; +import uk.co.bitethebullet.android.token.dialogs.DeleteTokenDialog; +import uk.co.bitethebullet.android.token.dialogs.DeleteTokenPickerDialog; +import uk.co.bitethebullet.android.token.parse.OtpAuthUriException; +import uk.co.bitethebullet.android.token.parse.UrlParser; +import uk.co.bitethebullet.android.token.tokens.HotpToken; +import uk.co.bitethebullet.android.token.tokens.IToken; +import uk.co.bitethebullet.android.token.tokens.ITokenMeta; +import uk.co.bitethebullet.android.token.tokens.TokenFactory; +import uk.co.bitethebullet.android.token.tokens.TokenHelper; +import uk.co.bitethebullet.android.token.tokens.TokenMetaData; +import uk.co.bitethebullet.android.token.tokens.TotpToken; +import uk.co.bitethebullet.android.token.util.FontManager; +import uk.co.bitethebullet.android.token.util.SeedConvertor; +import uk.co.bitethebullet.android.token.zxing.IntentIntegrator; +import uk.co.bitethebullet.android.token.zxing.IntentResult; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.ClipData; +import android.content.ClipboardManager; +import android.content.Context; +import android.content.DialogInterface; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.database.Cursor; +import android.graphics.Color; +import android.media.session.MediaSession; +import android.os.Bundle; +import android.os.Handler; +import android.util.Log; +import android.view.ContextMenu; +import android.view.LayoutInflater; +import android.view.Menu; +import android.view.MenuInflater; +import android.view.MenuItem; +import android.view.View; +import android.view.ViewGroup; +import android.view.View.OnClickListener; +import android.view.inputmethod.InputMethodManager; +import android.widget.AdapterView; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.EditText; +import android.widget.FrameLayout; +import android.widget.LinearLayout; +import android.widget.ListView; +import android.widget.ProgressBar; +import android.widget.TextView; +import android.widget.ImageView; +import android.widget.Toast; + +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.fragment.app.DialogFragment; +import androidx.preference.PreferenceManager; + +import com.google.android.material.floatingactionbutton.FloatingActionButton; +import com.google.android.material.snackbar.Snackbar; + +/** + * Main entry point into Android Token application + * + * Generates OATH compliant HOTP or TOTP tokens which can be used + * in place of a hardware token. + * + * For more information about this project visit + * http://code.google.com/p/androidtoken/ + */ +public class TokenList extends AppCompatActivity + implements DeleteTokenDialog.DeleteTokenDialogListener, + DeleteTokenPickerDialog.DeleteTokenDialogListener{ + + private static final int ACTIVITY_ADD_TOKEN = 0; + private static final int ACTIVITY_CHANGE_PIN = 1; + private static final int ACTIVITY_REMOVE_PIN = 2; + private static final int ACTIVITY_ABOUT = 3; + + private static final int MENU_ADD_ID = Menu.FIRST; + private static final int MENU_PIN_CHANGE_ID = Menu.FIRST + 1; + private static final int MENU_PIN_REMOVE_ID = Menu.FIRST + 2; + private static final int MENU_DELETE_TOKEN_ID = Menu.FIRST + 3; + private static final int MENU_SCAN_QR = Menu.FIRST + 4; + private static final int MENU_SETTINGS = Menu.FIRST + 5; + private static final int MENU_ABOUT = Menu.FIRST + 6; + + private static final String KEY_HAS_PASSED_PIN = "pinValid"; + private static final String KEY_SELECTED_TOKEN_ID = "selectedTokenId"; + + private Boolean mHasPassedPin = false; + private Long mSelectedTokenId = Long.parseLong("-1"); + private Timer mTimer = null; + private TokenDbAdapter mTokenDbHelper = null; + private Handler mHandler; + private Runnable mOtpUpdateTask; + + private LinearLayout mMainPin; + private FrameLayout mMainList; + + private TokenAdapter mtokenAdaptor = null; + SharedPreferences sharedPreferences; + + /** Called when the activity is first created. */ + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.main); + + //check if we need to restore from a saveinstancestate + if(savedInstanceState != null){ + mHasPassedPin = savedInstanceState.getBoolean(KEY_HAS_PASSED_PIN); + mSelectedTokenId = savedInstanceState.getLong(KEY_SELECTED_TOKEN_ID); + } + sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(this); + + mTokenDbHelper = new TokenDbAdapter(this); + mTokenDbHelper.open(); + + //if we have a pin defined, need to enter that first before allow + //the user to see the tokens + mMainPin = (LinearLayout)findViewById(R.id.mainPin); + mMainList = (FrameLayout)findViewById(R.id.list); + + Button loginBtn = (Button)findViewById(R.id.mainLogin); + loginBtn.setOnClickListener(validatePin); + + if(PinManager.hasPinDefined(this)){ + mMainPin.setVisibility(View.VISIBLE); + mMainList.setVisibility(View.GONE); + }else{ + mMainList.setVisibility(View.VISIBLE); + mMainPin.setVisibility(View.GONE); + mHasPassedPin = true; + InitTokenList(); + } + + mHandler = new Handler(); + } + + private void InitTokenList() { + fillData(); + + ListView lv = (ListView)findViewById(R.id.listTokens); + TextView tvEmpty = (TextView)findViewById(R.id.empty); + + if(lv.getCount() > 0){ + tvEmpty.setVisibility(View.GONE); + }else{ + tvEmpty.setVisibility(View.VISIBLE); + } + + lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { + @Override + public void onItemClick(AdapterView adapterView, View view, int index, long id) { + //get the token, we only handle the click + //event if the token is a HOTP, in which + //case we then display a dialog with the + //token code + String otp = generateOtp(id); + + if(otp != null){ + showHotpToken(otp); + } + } + }); + + registerForContextMenu(lv); + + FloatingActionButton fab = findViewById(R.id.fab); + fab.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View view) { + scanQR(); + } + }); + } + + + @Override + protected void onDestroy() { + super.onDestroy(); + mTokenDbHelper.close(); + } + + + @Override + protected void onResume() { + super.onResume(); + + Runnable otpUpdate = new Runnable(){ + + public void run() { + if(mOtpUpdateTask == this){ + TokenList.this.fillData(); + mHandler.postDelayed(mOtpUpdateTask, 1000); + } + } + }; + + mOtpUpdateTask = otpUpdate; + mOtpUpdateTask.run(); + } + + + @Override + protected void onPause() { + super.onPause(); + + mOtpUpdateTask = null; + } + + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + outState.putBoolean(KEY_HAS_PASSED_PIN, mHasPassedPin); + outState.putLong(KEY_SELECTED_TOKEN_ID, mSelectedTokenId); + } + + private OnClickListener validatePin = new OnClickListener() { + + public void onClick(View v) { + + //close the keyboard, so we can show/see the snackbar + //message if the pin is invalid + InputMethodManager inputManager = (InputMethodManager) + getSystemService(Context.INPUT_METHOD_SERVICE); + + if(getCurrentFocus() != null){ + inputManager.hideSoftInputFromWindow(getCurrentFocus().getWindowToken(), + InputMethodManager.HIDE_NOT_ALWAYS); + } + + EditText mainPinEdit = (EditText)findViewById(R.id.mainPinEdit); + String pin = mainPinEdit.getText().toString(); + + if(PinManager.validatePin(v.getContext(), pin)){ + //then display the list view + mMainList.setVisibility(View.VISIBLE); + mMainPin.setVisibility(View.GONE); + mHasPassedPin = true; + InitTokenList(); + }else{ + //reset the input edittext + mainPinEdit.setText(""); + + //display an alert + Snackbar.make(findViewById(R.id.mainPin), R.string.invalid_pin, Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + } + }; + + + /** + * handles showing the HOTP token in a self closing dialog + * @param token + */ + private void showHotpToken(String token){ + final AlertDialog.Builder dialog = new AlertDialog.Builder(this) + .setTitle(token); + + dialog.setPositiveButton(R.string.dismiss, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int whichButton) { + } + }); + + final AlertDialog alert = dialog.create(); + alert.show(); + + // Hide after some seconds + final Handler handler = new Handler(); + final Runnable runnable = new Runnable() { + @Override + public void run() { + if (alert.isShowing()) { + alert.dismiss(); + } + } + }; + + alert.setOnDismissListener(new DialogInterface.OnDismissListener() { + @Override + public void onDismiss(DialogInterface dialog) { + handler.removeCallbacks(runnable); + } + }); + handler.postDelayed(runnable, 8000); + } + + + + + /** + Show the context menu's delete token dialog + **/ + public void showDeleteTokenDialog(IToken token) { + DialogFragment dialog = new DeleteTokenDialog(); + + Bundle args = new Bundle(); + args.putCharSequence("name", token.getFullName()); + args.putLong("tokenId", token.getId()); + + dialog.setArguments(args); + + dialog.show(getSupportFragmentManager(), "NoticeDialogFragment"); + } + + @Override + public void onDialogPositiveClick(DialogFragment dialog) { + Long tokenId = dialog.getArguments().getLong("tokenId"); + mTokenDbHelper.deleteToken(tokenId); + mtokenAdaptor = null; + fillData(); + + Snackbar.make(findViewById(R.id.list), R.string.token_is_deleted, Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + + @Override + public void onDialogNegativeClick(DialogFragment dialog) { + + } + + + @Override + public boolean onPrepareOptionsMenu(Menu menu) { + + for (int i = 0; i < menu.size(); i++){ + menu.getItem(i).setEnabled(true); + } + + //if we have no tokens disable the delete token option + ListView lv = findViewById(R.id.listTokens); + menu.findItem(MENU_DELETE_TOKEN_ID).setEnabled(lv.getCount() > 0); + + if(mHasPassedPin){ + //menu.findItem(MENU_PIN_REMOVE_ID).setEnabled(PinManager.hasPinDefined(this)); + }else{ + for (int i = 0; i < menu.size(); i++){ + menu.getItem(i).setEnabled(false); + } + } + + super.onPrepareOptionsMenu(menu); + return true; + } + + private void fillData() { + ListView lv = (ListView)findViewById(R.id.listTokens); + + if(mtokenAdaptor == null){ + mtokenAdaptor = new TokenAdapter(this, mTokenDbHelper); + lv.setAdapter(mtokenAdaptor); + }else{ + mtokenAdaptor.notifyDataSetChanged(); + } + + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + + Log.d("activityResult", "Activity Result received, request code:" + requestCode + " resultCode:" + resultCode); + int toastRId = 0; + + IntentResult scanResult = IntentIntegrator.parseActivityResult(requestCode, resultCode, data); + if (scanResult != null) { + + if(storeOtpAuthUrl(scanResult.getContents())){ + mtokenAdaptor = null; + fillData(); + toastRId = R.string.toastAdded; + }else{ + toastRId = R.string.toastInvalidQr; + } + } + else if(requestCode == ACTIVITY_ADD_TOKEN && resultCode == Activity.RESULT_OK){ + mtokenAdaptor = null; + fillData(); + toastRId = R.string.toastAdded; + } + + if(toastRId > 0){ + Snackbar.make(findViewById(R.id.list), toastRId, Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + } + + private boolean storeOtpAuthUrl(String url) { + try { + + Log.d("QR scanned URL", url); + + ITokenMeta token = UrlParser.parseOtpAuthUrl(url); + + String hexSeed = SeedConvertor.ConvertFromBA( + SeedConvertor.ConvertFromEncodingToBA(token.getSecretBase32(), + SeedConvertor.BASE32_FORMAT), + SeedConvertor.HEX_FORMAT); + + TokenDbAdapter db = new TokenDbAdapter(this.getBaseContext()); + db.open(); + long tokenId = db.createToken(token.getName(), + "", + hexSeed, + token.getTokenType(), + token.getDigits(), + token.getTimeStep(), + token.getOrganisation()); + + //if we have created HOTP and counter is greater + //than zero we need to set the token to this in the db + if(token.getTokenType() == TokenMetaData.HOTP_TOKEN && token.getCounter() > 0){ + db.setTokenCounter(tokenId, token.getCounter()); + } + + db.close(); + + return true; + + } catch (OtpAuthUriException e) { + Log.e(TokenList.class.getName(), e.getMessage(), e); + return false; + }catch(IOException e){ + Log.e(TokenList.class.getName(), e.getMessage(), e); + return false; + } + } + + @Override + public boolean onCreateOptionsMenu(Menu menu) { + super.onCreateOptionsMenu(menu); + + menu.add(0, MENU_ADD_ID, 0, R.string.menu_add_token).setIcon(android.R.drawable.ic_menu_add); + menu.add(0, MENU_PIN_CHANGE_ID, 1, R.string.menu_pin_change).setIcon(android.R.drawable.ic_lock_lock); + //menu.add(0, MENU_PIN_REMOVE_ID, 2, R.string.menu_pin_remove).setIcon(android.R.drawable.ic_menu_delete); + menu.add(0, MENU_DELETE_TOKEN_ID, 2, R.string.menu_delete_token).setIcon(android.R.drawable.ic_menu_delete); + menu.add(0, MENU_SCAN_QR, 3, R.string.menu_scan).setIcon(android.R.drawable.ic_menu_camera); + menu.add(0, MENU_SETTINGS, 4, R.string.menu_settings).setIcon(android.R.drawable.ic_menu_preferences); + menu.add(0, MENU_ABOUT, 5, R.string.menu_about); + + return true; + } + + @Override + public boolean onOptionsItemSelected(@NonNull MenuItem item) { + + switch(item.getItemId()){ + case MENU_ADD_ID: + createToken(); + return true; + + case MENU_PIN_CHANGE_ID: + changePin(); + return true; + + case MENU_PIN_REMOVE_ID: + removePin(); + return true; + + case MENU_DELETE_TOKEN_ID: + showDeletePickerDialog(); + return true; + + case MENU_SCAN_QR: + scanQR(); + return true; + + case MENU_SETTINGS: + showSettings(); + return true; + + case MENU_ABOUT: + showAbout(); + return true; + } + return super.onOptionsItemSelected(item); + } + + private void showAbout(){ + Intent intent = new Intent(this, About.class); + startActivityForResult(intent, ACTIVITY_ABOUT); + } + + private void showDeletePickerDialog(){ + DialogFragment dialog = new DeleteTokenPickerDialog(); + + CharSequence[] tokenNames = new TokenHelper(mTokenDbHelper) + .getTokenFullNames(); + + Bundle args = new Bundle(); + args.putCharSequenceArray("tokens", tokenNames); + + dialog.setArguments(args); + dialog.show(getSupportFragmentManager(), "DeleteTokenPickerDialog"); + } + + @Override + public void onDeleteTokensDialogPositiveClick(DialogFragment dialog, ArrayList selectedTokens) { + TokenHelper tokenHelper = new TokenHelper(this.mTokenDbHelper); + ArrayList tokens = tokenHelper.getTokens(); + ArrayList tokensToDelete = new ArrayList(); + + //workout the tokens we need to delete, then just remove them + //one by one + for(int i = 0; i < selectedTokens.size(); i++){ + tokensToDelete.add(tokens.get((int)selectedTokens.get(i))); + } + + for(int i = 0; i < tokensToDelete.size(); i++){ + mTokenDbHelper.deleteToken(((IToken)tokensToDelete.get(i)).getId()); + } + mtokenAdaptor = null; + fillData(); + } + + @Override + public void onDeleteTokensDialogNegativeClick(DialogFragment dialog) { + //ignore nothing required + } + + private void scanQR() { + IntentIntegrator integrator = new IntentIntegrator(this); + integrator.initiateScan(IntentIntegrator.QR_CODE_TYPES); + } + + + private void removePin() { + Intent i = new Intent(this, PinRemove.class); + startActivityForResult(i, ACTIVITY_REMOVE_PIN); + } + + private void changePin() { + Intent i = new Intent(this, PinChange.class); + startActivityForResult(i, ACTIVITY_CHANGE_PIN); + } + + /** + * Starts the process of creating a new token in the application + */ + private void createToken() { + Intent intent = new Intent(this, TokenAdd.class); + startActivityForResult(intent, ACTIVITY_ADD_TOKEN); + } + + private void showSettings(){ + Intent intent = new Intent(this, SettingActivity.class); + startActivity(intent); + } + + + @Override + public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + super.onCreateContextMenu(menu, v, menuInfo); + MenuInflater inflater = getMenuInflater(); + inflater.inflate(R.menu.token_item_menu, menu); + + Log.d(null, "onCreateContextMenu: "); + } + + private IToken tokenAtPos(int position) { + ListView lv = findViewById(R.id.listTokens); + + IToken token = (IToken) lv.getAdapter().getItem(position); + return token; + } + + @Override + public boolean onContextItemSelected(@NonNull MenuItem item) { + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); + IToken token = tokenAtPos(info.position); + + boolean tokenClicked = token != null; + + if (tokenClicked) { + + switch (item.getItemId()) { + + //this menu option is hidden, so no need to complete + case R.id.token_change_icon: + Toast.makeText(this,"change icon",Toast.LENGTH_SHORT).show(); + return true; + + case R.id.token_delete: + this.showDeleteTokenDialog(token); + return true; + + case R.id.token_copy_secret: + copyTokenSeedToClipboard(token); + return true; + + case R.id.token_generate_qr_code: + //generate the QR image and output + //to a activity with the image/QR shown + Intent intent = new Intent(this, QRCodeActivity.class); + intent.putExtra("qrUrl", token.getUrl()); + intent.putExtra("fullName", token.getFullName()); + startActivity(intent); + + return true; + + default: + return super.onContextItemSelected(item); + } + } else { + return super.onContextItemSelected(item); + } + } + + private void copyTokenSeedToClipboard(IToken token) { + ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); + clipboard.setPrimaryClip(ClipData.newPlainText( "Seed for " + token.getFullName(), token.getSeed())); + + + Snackbar.make(findViewById(R.id.list), R.string.token_seed_copied, Snackbar.LENGTH_LONG) + .setAction("Action", null).show(); + } + + + private DialogInterface.OnDismissListener dismissOtpDialog = new DialogInterface.OnDismissListener() { + + public void onDismiss(DialogInterface dialog) { + if(mTimer != null){ + mTimer.cancel(); + } + } + }; + + + private String generateOtp(long tokenId) { + Cursor cursor = mTokenDbHelper.fetchToken(tokenId); + IToken token = TokenFactory.CreateToken(cursor); + cursor.close(); + + + if(!(token instanceof TotpToken)){ + String otp = TokenAdapter.otpFormatter(token.generateOtp(), + sharedPreferences.getBoolean("groupIntoTwoDigits", false)); + mTokenDbHelper.incrementTokenCount(tokenId); + return otp; + } + + return null; + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/adapters/TokenAdapter.java b/app/src/main/java/uk/co/bitethebullet/android/token/adapters/TokenAdapter.java new file mode 100644 index 0000000..2d83e7b --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/adapters/TokenAdapter.java @@ -0,0 +1,162 @@ +package uk.co.bitethebullet.android.token.adapters; + +import android.content.Context; +import android.content.SharedPreferences; +import android.content.res.ColorStateList; +import android.database.Cursor; +import android.graphics.Color; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.BaseAdapter; +import android.widget.ProgressBar; +import android.widget.TextView; + +import androidx.preference.PreferenceManager; + +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import uk.co.bitethebullet.android.token.R; +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; +import uk.co.bitethebullet.android.token.tokens.IToken; +import uk.co.bitethebullet.android.token.tokens.IconSuggestor; +import uk.co.bitethebullet.android.token.tokens.TokenFactory; +import uk.co.bitethebullet.android.token.util.FontManager; + +public class TokenAdapter extends BaseAdapter +{ + private Context mContext; + private TokenDbAdapter mDbAdapter; + private List mTokens; + SharedPreferences sharedPreferences; + + public TokenAdapter(Context context, TokenDbAdapter dbAdapter){ + mContext = context; + mDbAdapter = dbAdapter; + + sharedPreferences = + PreferenceManager.getDefaultSharedPreferences(context); + + Cursor cursor = mDbAdapter.fetchAllTokens(); + + //read all the tokens we have and put them into a list + //this will save hitting the db everytime we draw the + //ui with an update + + mTokens = new ArrayList(); + + cursor.moveToFirst(); + while (!cursor.isAfterLast()) { + mTokens.add(TokenFactory.CreateToken(cursor)); + cursor.moveToNext(); + } + + cursor.close(); + } + + public static String otpFormatter(String otp, boolean groupInTwos){ + if(groupInTwos){ + StringBuilder builder = new StringBuilder(); + int k = 0; + for (int i = 0; i < otp.length(); i++) { + builder.append(otp.charAt(i)); + k++; + + if(k >= 2){ + k = 0; + builder.append(" "); + } + } + return builder.toString(); + }else{ + return otp; + } + } + + public int getCount() { + return mTokens.size(); + } + + public Object getItem(int position) { + return mTokens.get(position); + } + + public long getItemId(int position) { + return mTokens.get(position).getId(); + } + + public View getView(int position, View convertView, ViewGroup parent) { + LayoutInflater inflater = (LayoutInflater)mContext.getSystemService(Context.LAYOUT_INFLATER_SERVICE); + View row = inflater.inflate(R.layout.token_list_row, null); + + TextView nameText = (TextView)row.findViewById(R.id.tokenrowtextname); + TextView serialText = (TextView)row.findViewById(R.id.tokenrowtextserial); + TextView totpText = (TextView)row.findViewById(R.id.tokenRowTimeTokenOtp); + TextView faIcon = (TextView)row.findViewById(R.id.faIcon); + ProgressBar totpProgressBar = (ProgressBar)row.findViewById(R.id.totpTimerProgressbar); + + + IToken currentToken = (IToken)getItem(position); + + //get an icon based on the organisation + IconSuggestor iconSuggestor = new IconSuggestor(); + IconSuggestor.IconResult iconResult = iconSuggestor.getSuggestedIcon(currentToken, mContext); + + faIcon.setTypeface(FontManager.getTypeface(mContext, iconResult.getFont())); + faIcon.setText(iconResult.getContent()); + + if(currentToken.getOrganisation() == null || currentToken.getOrganisation().length() == 0){ + nameText.setText(currentToken.getName()); + }else{ + nameText.setText(currentToken.getName() + " (" + currentToken.getOrganisation() + ")"); + } + + if(currentToken.getSerialNumber().length() > 0) + serialText.setText(currentToken.getSerialNumber()); + else{ + TextView serialCaption = (TextView)row.findViewById(R.id.tokenrowtextserialcaption); + serialCaption.setVisibility(View.GONE); + serialText.setVisibility(View.GONE); + } + + //if the token is a time token, just display the current + //value for the token. Event tokens will still need to + //be click to display the otp + if(currentToken.getTokenType() == TokenDbAdapter.TOKEN_TYPE_TIME){ + totpText.setVisibility(View.VISIBLE); + totpText.setText(otpFormatter(currentToken.generateOtp(), + sharedPreferences.getBoolean("groupIntoTwoDigits", false))); + + totpProgressBar.setVisibility(View.VISIBLE); + + Date dt = new Date(); + float curSec = (float)dt.getSeconds(); + int progress; + + if(currentToken.getTimeStep() == 30){ + + if(curSec > 30) + curSec = curSec - 30; + + progress = (int)(100 - ((curSec/30)*100)); + }else{ + progress = (int)(100 - ((curSec/60)*100)); + } + + totpProgressBar.setProgress(progress); + + //when we get to the last 5 seconds, lets change + //the token colour to red as a warning + if(progress > 10){ + totpText.setTextColor(nameText.getTextColors()); + }else{ + totpText.setTextColor(Color.RED); + } + } + + return row; + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/datalayer/TokenDbAdapter.java b/app/src/main/java/uk/co/bitethebullet/android/token/datalayer/TokenDbAdapter.java new file mode 100644 index 0000000..404535a --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/datalayer/TokenDbAdapter.java @@ -0,0 +1,239 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token.datalayer; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; +import android.database.SQLException; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteOpenHelper; + +/** + * Performs the CRUD database actions for the Android Token application + */ +public class TokenDbAdapter { + //const holding the table field names + public static final String KEY_TOKEN_ROWID = "_id"; + public static final String KEY_TOKEN_NAME = "name"; + public static final String KEY_TOKEN_SERIAL = "serial"; + public static final String KEY_TOKEN_SEED = "seed"; + public static final String KEY_TOKEN_COUNT = "eventcount"; + public static final String KEY_TOKEN_TYPE = "tokentype"; + public static final String KEY_TOKEN_OTP_LENGTH = "otplength"; + public static final String KEY_TOKEN_TIME_STEP = "timestep"; + public static final String KEY_TOKEN_NAME_SORT = "namesort"; + public static final String KEY_TOKEN_ORGANISATION = "organisation"; + + public static final String KEY_PIN_ROWID = "_id"; + public static final String KEY_PIN_HASH = "pinhash"; + + //const define the different token type + public static final int TOKEN_TYPE_EVENT = 0; + public static final int TOKEN_TYPE_TIME = 1; + + public static final String TAG = "TokenDbAdapter"; + + //const database tables, version + private static final String DATABASE_NAME = "androidtoken.db"; + private static final String DATABASE_TOKEN_TABLE = "token"; + private static final String DATABASE_PIN_TABLE = "pin"; + private static final int DATABASE_VERSION = 2; + + private DatabaseHelper mDbHelper; + private SQLiteDatabase mDb; + private final Context mContext; + + private static final String DATABASE_CREATE_TOKEN = + "create table token (_id integer primary key autoincrement," + + " name text not null," + + " serial text," + + " seed text," + + " eventcount integer," + + " tokentype integer," + + " otplength integer," + + " timestep integer," + + " namesort text," + + " organisation text);"; + + private static final String DATABASE_CREATE_PIN = + "create table pin(_id integer primary key autoincrement," + + " pinhash text);"; + + private static final String DATABASE_DROP_TOKEN = "DROP TABLE IF EXISTS token;"; + private static final String DATABASE_DROP_PIN = "DROP TABLE IF EXISTS pin;"; + + private static final String DATABASE_ALTER_TOKEN_1 = "ALTER TABLE " + + DATABASE_TOKEN_TABLE + " ADD COLUMN " + KEY_TOKEN_ORGANISATION + " string;"; + + + private static class DatabaseHelper extends SQLiteOpenHelper{ + + public DatabaseHelper(Context context){ + super(context,DATABASE_NAME, null, DATABASE_VERSION); + } + + @Override + public void onCreate(SQLiteDatabase db) { + db.execSQL(DATABASE_CREATE_TOKEN); + db.execSQL(DATABASE_CREATE_PIN); + } + + @Override + public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { + + if (oldVersion < 2) { + db.execSQL(DATABASE_ALTER_TOKEN_1); + } + } + } + + public TokenDbAdapter(Context context){ + this.mContext = context; + } + + public TokenDbAdapter open() throws SQLException{ + mDbHelper = new DatabaseHelper(mContext); + mDb = mDbHelper.getWritableDatabase(); + return this; + } + + public void close(){ + mDbHelper.close(); + } + + + + + // Data Access Methods + //////////////////////////////// + + + //TOKEN TABLE + public long createToken(String name, String serial, String seed, + int tokenType, int otpLength, int timeStep, + String organisation){ + ContentValues values = new ContentValues(); + values.put(KEY_TOKEN_NAME, name); + values.put(KEY_TOKEN_SERIAL, serial); + values.put(KEY_TOKEN_SEED, seed); + values.put(KEY_TOKEN_TYPE, tokenType); + values.put(KEY_TOKEN_OTP_LENGTH, otpLength); + values.put(KEY_TOKEN_TIME_STEP, timeStep); + values.put(KEY_TOKEN_COUNT, 0); + values.put(KEY_TOKEN_NAME_SORT, name.toLowerCase()); + values.put(KEY_TOKEN_ORGANISATION, organisation); + + return mDb.insert(DATABASE_TOKEN_TABLE, null, values); + } + + public boolean deleteToken(long tokenId){ + return mDb.delete(DATABASE_TOKEN_TABLE, KEY_TOKEN_ROWID + "=" + tokenId, null) > 0; + } + + public boolean renameToken(long tokenId, String name){ + ContentValues values = new ContentValues(); + values.put(KEY_TOKEN_NAME, name); + + return mDb.update(DATABASE_TOKEN_TABLE, values, KEY_TOKEN_ROWID + "=" + tokenId, null) > 0; + } + + public void incrementTokenCount(long tokenId){ + mDb.execSQL("UPDATE token SET eventcount = eventcount + 1 WHERE _id = " + tokenId); + } + + public void setTokenCounter(long tokenId, int eventCounter){ + mDb.execSQL("UPDATE token SET eventcount = " + eventCounter + " WHERE _id = " + tokenId); + } + + public Cursor fetchToken(long tokenId){ + Cursor c = mDb.query(DATABASE_TOKEN_TABLE, + new String[] {KEY_TOKEN_ROWID, KEY_TOKEN_NAME, KEY_TOKEN_SERIAL, KEY_TOKEN_SEED, + KEY_TOKEN_COUNT, KEY_TOKEN_TYPE, KEY_TOKEN_OTP_LENGTH, + KEY_TOKEN_TIME_STEP, KEY_TOKEN_ORGANISATION}, + KEY_TOKEN_ROWID + "=" + tokenId, + null, + null, + null, + null); + + if(c != null) + c.moveToFirst(); + + return c; + } + + public Cursor fetchAllTokens(){ + return mDb.query(DATABASE_TOKEN_TABLE, + new String[] {KEY_TOKEN_ROWID, KEY_TOKEN_NAME, KEY_TOKEN_SERIAL, KEY_TOKEN_SEED, + KEY_TOKEN_COUNT, KEY_TOKEN_TYPE, KEY_TOKEN_OTP_LENGTH, + KEY_TOKEN_TIME_STEP, KEY_TOKEN_ORGANISATION}, + null, + null, + null, + null, + KEY_TOKEN_NAME_SORT + " ASC"); + } + + + //PIN TABLE + public boolean createOrUpdatePin(String pinHash){ + + boolean result = false; + + ContentValues values = new ContentValues(); + values.put(KEY_PIN_HASH, pinHash); + + Cursor c = fetchPin(); + + if(c.getCount() == 0){ + //no pin set, insert new row + result = mDb.insert(DATABASE_PIN_TABLE, null, values) > 0; + }else{ + //pin already set update existing + result = mDb.update(DATABASE_PIN_TABLE, values, null, null) > 0; + } + + c.close(); + + return result; + } + + public void deletePin(){ + mDb.delete(DATABASE_PIN_TABLE, null, null); + } + + public Cursor fetchPin(){ + Cursor c = mDb.query(DATABASE_PIN_TABLE, + new String[]{KEY_PIN_HASH}, + null, + null, + null, + null, + null, + "1"); + + if(c != null) + c.moveToFirst(); + + return c; + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenDialog.java b/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenDialog.java new file mode 100644 index 0000000..b240400 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenDialog.java @@ -0,0 +1,58 @@ +package uk.co.bitethebullet.android.token.dialogs; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.fragment.app.DialogFragment; + +import uk.co.bitethebullet.android.token.R; + +public class DeleteTokenDialog extends DialogFragment { + + public interface DeleteTokenDialogListener { + public void onDialogPositiveClick(DialogFragment dialog); + public void onDialogNegativeClick(DialogFragment dialog); + } + + DeleteTokenDialogListener listener; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + listener = (DeleteTokenDialogListener) context; + } catch (ClassCastException e) { + throw new ClassCastException("Must implement DeleteTokenDialogListener"); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + CharSequence tokenName = this.getArguments().getCharSequence("name"); + String message = getResources() + .getString(R.string.delete_token_dialog_message) + .replace("[name]", tokenName); + + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setMessage(message) + .setTitle(R.string.delete_token_dialog_title) + .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + listener.onDialogPositiveClick(DeleteTokenDialog.this); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + listener.onDialogNegativeClick(DeleteTokenDialog.this); + } + }); + // Create the AlertDialog object and return it + return builder.create(); + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenPickerDialog.java b/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenPickerDialog.java new file mode 100644 index 0000000..ad9fcef --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/DeleteTokenPickerDialog.java @@ -0,0 +1,72 @@ +package uk.co.bitethebullet.android.token.dialogs; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.os.Bundle; + +import androidx.fragment.app.DialogFragment; + +import java.util.ArrayList; + +import uk.co.bitethebullet.android.token.R; +import uk.co.bitethebullet.android.token.dialogs.DeleteTokenDialog; + +public class DeleteTokenPickerDialog extends DialogFragment { + + public interface DeleteTokenDialogListener { + public void onDeleteTokensDialogPositiveClick(DialogFragment dialog, + ArrayList selectedTokensToDelete); + public void onDeleteTokensDialogNegativeClick(DialogFragment dialog); + } + + DeleteTokenPickerDialog.DeleteTokenDialogListener listener; + ArrayList selectedItems; + + @Override + public void onAttach(Context context) { + super.onAttach(context); + try { + listener = (DeleteTokenPickerDialog.DeleteTokenDialogListener) context; + } catch (ClassCastException e) { + throw new ClassCastException("Must implement DeleteTokenDialogListener"); + } + } + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + selectedItems = new ArrayList(); + + CharSequence[] tokenNames = this.getArguments().getCharSequenceArray("tokens"); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + builder.setTitle(R.string.delete_token_dialog_title) + .setMultiChoiceItems(tokenNames, null, + new DialogInterface.OnMultiChoiceClickListener(){ + @Override + public void onClick(DialogInterface dialogInterface, int i, boolean isChecked) { + if (isChecked) { + selectedItems.add(i); + } else if (selectedItems.contains(i)) { + selectedItems.remove(Integer.valueOf(i)); + } + } + }) + .setPositiveButton(R.string.delete, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + + listener.onDeleteTokensDialogPositiveClick(DeleteTokenPickerDialog.this, + selectedItems); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + listener.onDeleteTokensDialogNegativeClick(DeleteTokenPickerDialog.this); + } + }); + // Create the AlertDialog object and return it + return builder.create(); + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/PinDefintionDialog.java b/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/PinDefintionDialog.java new file mode 100644 index 0000000..8e6a4a5 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/dialogs/PinDefintionDialog.java @@ -0,0 +1,62 @@ +package uk.co.bitethebullet.android.token.dialogs; + +import android.app.Activity; +import android.app.AlertDialog; +import android.app.Dialog; +import android.content.Context; +import android.content.DialogInterface; +import android.content.SharedPreferences; +import android.os.Bundle; +import android.util.Log; +import android.view.LayoutInflater; +import android.view.View; +import android.view.ViewGroup; +import android.widget.EditText; + +import androidx.fragment.app.DialogFragment; +import androidx.preference.PreferenceManager; + +import java.util.ArrayList; + +import uk.co.bitethebullet.android.token.PinManager; +import uk.co.bitethebullet.android.token.R; + +public class PinDefintionDialog extends DialogFragment { + + + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + + final Context myContext = this.getContext(); + View viewInflated = LayoutInflater.from(getContext()) + .inflate(R.layout.pindefinitiondialog, + (ViewGroup) getView(), + false); + final EditText pinInput = (EditText) viewInflated.findViewById(R.id.pin); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + builder.setView(viewInflated) + .setTitle(R.string.set_pin) + .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + PinManager.storePin(myContext, pinInput.getText().toString()); + + getTargetFragment() + .onActivityResult(getTargetRequestCode(), + Activity.RESULT_OK, + getActivity().getIntent()); + } + }) + .setNegativeButton(R.string.cancel, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + getTargetFragment() + .onActivityResult(getTargetRequestCode(), + Activity.RESULT_CANCELED, + getActivity().getIntent()); + } + }); + // Create the AlertDialog object and return it + return builder.create(); + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/parse/OtpAuthUriException.java b/app/src/main/java/uk/co/bitethebullet/android/token/parse/OtpAuthUriException.java new file mode 100644 index 0000000..7a7fd78 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/parse/OtpAuthUriException.java @@ -0,0 +1,24 @@ +package uk.co.bitethebullet.android.token.parse; + +public class OtpAuthUriException extends Exception { + + /** + * + */ + private static final long serialVersionUID = 1L; + String err; + + public OtpAuthUriException() + { + super(); // call superclass constructor + err = "unknown"; + } + + //----------------------------------------------- + // Constructor receives some kind of message that is saved in an instance variable. + public OtpAuthUriException(String err) + { + super(err); // call super class constructor + this.err = err; // save message + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/parse/UrlParser.java b/app/src/main/java/uk/co/bitethebullet/android/token/parse/UrlParser.java new file mode 100644 index 0000000..907fa25 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/parse/UrlParser.java @@ -0,0 +1,82 @@ +package uk.co.bitethebullet.android.token.parse; + + +import uk.co.bitethebullet.android.token.tokens.ITokenMeta; +import uk.co.bitethebullet.android.token.tokens.TokenMetaData; + +public class UrlParser { + + public static ITokenMeta parseOtpAuthUrl(String url) throws OtpAuthUriException { + + int tokenType; + String tokenName; + String organisation = null; + String secret = null; + int digits = 6; + int counter = 0; + int period = 30; + boolean hasCounterParameter = false; + + if(!url.startsWith("otpauth://")){ + //not a valid otpauth url + //throw new OtpAuthUriException(context.getString(R.string.otpAuthUrlInvalid)); + throw new OtpAuthUriException(); + } + + String tokenTypeString = url.substring(10, url.indexOf("/", 10)); + + if(tokenTypeString.equals("hotp")){ + tokenType = TokenMetaData.HOTP_TOKEN; + }else if(tokenTypeString.equals("totp")){ + tokenType = TokenMetaData.TOTP_TOKEN; + }else{ + //the token type parameter is not valid + //throw new OtpAuthUriException(context.getString(R.string.otpAuthTokenTypeInvalid)); + throw new OtpAuthUriException(); + } + + tokenName = url.substring(url.indexOf("/", 10) + 1, url.indexOf("?", 10)); + + //decode url + tokenName = java.net.URLDecoder.decode(tokenName); + + //check to see if we have an organisation prefix for the token name + if(tokenName.contains(":")){ + organisation = tokenName.substring(0, tokenName.indexOf(":")).trim(); + tokenName = tokenName.substring(tokenName.indexOf(":") + 1).trim(); + } + String[] parameters = url.substring(url.indexOf("?") + 1).split("&"); + + for(int i = 0; i < parameters.length; i++){ + String[] paraDetail = new String[2]; + + //get the key + paraDetail[0] = parameters[i].substring(0, parameters[i].indexOf("=")); + + //get the value + paraDetail[1] = parameters[i].substring(parameters[i].indexOf("=") + 1, parameters[i].length()); + + //read the parameter and work out if its + //a valid parameter, if not just ignore + if(paraDetail[0].equals("secret")){ + secret = paraDetail[1]; + }else if(paraDetail[0].equals("digits")){ + digits = Integer.parseInt(paraDetail[1]); + }else if(paraDetail[0].equals("counter")){ + counter = Integer.parseInt(paraDetail[1]); + hasCounterParameter = true; + }else if(paraDetail[0].equals("period")){ + period = Integer.parseInt(paraDetail[1]); + } + } + + if(tokenType == TokenMetaData.HOTP_TOKEN && !hasCounterParameter){ + //when the token is a hotp token it must have the counter + //parameter supplied otherwise we should throw an error + //throw new OtpAuthUriException(context.getString(R.string.otpAuthMissingCounterParameter)); + throw new OtpAuthUriException(); + } + + return new TokenMetaData(tokenName, tokenType, secret, digits, period, counter, organisation); + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/HotpToken.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/HotpToken.java new file mode 100644 index 0000000..fb0aa09 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/HotpToken.java @@ -0,0 +1,298 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token.tokens; + +import java.io.IOException; +import java.lang.reflect.UndeclaredThrowableException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Calendar; +import java.util.TimeZone; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; + +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; +import uk.co.bitethebullet.android.token.util.SeedConvertor; + + +/** + * Hotp Token + * + * This is an event based OATH token, for further details + * see the RFC http://tools.ietf.org/html/rfc4226 + * + */ +public class HotpToken implements IToken { + + private String mName; + private String mOrganisation; + private String mSerial; + private String mSeed; + private long mEventCount; + private int mOtpLength; + private long id; + + + private static final int[] DIGITS_POWER + // 0 1 2 3 4 5 6 7 8 + = {1,10,100,1000,10000,100000,1000000,10000000,100000000}; + + + public HotpToken(String name, String serial, String seed, + long eventCount, int otpLength, String organisation){ + mName = name; + mSerial = serial; + mSeed = seed; + mEventCount = eventCount; + mOtpLength = otpLength; + mOrganisation = organisation; + } + + public int getTimeStep(){ + return 0; + } + + public long getId(){ + return this.id; + } + + public void setId(long id){ + this.id = id; + } + + public int getTokenType(){ + return TokenDbAdapter.TOKEN_TYPE_EVENT; + } + + public String getName() { + return mName; + } + + public String getOrganisation(){ return mOrganisation; } + + public void setName(String name) { + this.mName = name; + } + + + public String getSerialNumber() { + return mSerial; + } + + + public void setSerialNumber(String serial) { + this.mSerial = serial; + } + + + public String getSeed() { + return mSeed; + } + + + protected void setSeed(String seed) { + this.mSeed = seed; + } + + + public long getEventCount() { + return mEventCount; + } + + + protected void setEventCount(long eventCount) { + this.mEventCount = eventCount; + } + + + public int getOtpLength() { + return mOtpLength; + } + + + public void setOtpLength(int otpLength) { + this.mOtpLength = otpLength; + } + + + public String getUrl() { + //create the uri for the token that can be used to generate the QR code + //otpauth://hotp/organisation:alice@google.com?secret=JBSWY3DPEHPK3PXP&counter=10" + try { + //convert the seed from hex to base32 + String base32Secret = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(getSeed(), SeedConvertor.HEX_FORMAT), + SeedConvertor.BASE32_FORMAT); + + StringBuilder buffer = new StringBuilder(); + buffer.append("otpauth://hotp/"); + + if (getOrganisation() != null && getOrganisation().length() > 0) { + buffer.append(java.net.URLEncoder.encode(getOrganisation())); + buffer.append(":"); + } + + buffer.append(java.net.URLEncoder.encode(getName())); + buffer.append("?secret="); + buffer.append(base32Secret); + buffer.append("&counter="); + buffer.append(getEventCount()); + + return buffer.toString(); + }catch(IOException ex){ + return null; + } + } + + public String getFullName(){ + StringBuilder sb = new StringBuilder(); + + if(this.getOrganisation() != null){ + sb.append(this.getOrganisation()); + sb.append("/"); + } + + sb.append(this.getName()); + + return sb.toString(); + } + + + public String generateOtp() { + + byte[] counter = new byte[8]; + long movingFactor = mEventCount; + + for(int i = counter.length - 1; i >= 0; i--){ + counter[i] = (byte)(movingFactor & 0xff); + movingFactor >>= 8; + } + + byte[] hash = hmacSha(stringToHex(mSeed), counter); + int offset = hash[hash.length - 1] & 0xf; + + int otpBinary = ((hash[offset] & 0x7f) << 24) + |((hash[offset + 1] & 0xff) << 16) + |((hash[offset + 2] & 0xff) << 8) + |(hash[offset + 3] & 0xff); + + int otp = otpBinary % DIGITS_POWER[mOtpLength]; + String result = Integer.toString(otp); + + + while(result.length() < mOtpLength){ + result = "0" + result; + } + + return result; + } + + public static byte[] stringToHex(String hexInputString){ + + byte[] bts = new byte[hexInputString.length() / 2]; + + for (int i = 0; i < bts.length; i++) { + bts[i] = (byte) Integer.parseInt(hexInputString.substring(2*i, 2*i+2), 16); + } + + return bts; + } + + private byte[] hmacSha(byte[] seed, byte[] counter) { + + try{ + Mac hmacSha1; + + try{ + hmacSha1 = Mac.getInstance("HmacSHA1"); + }catch(NoSuchAlgorithmException ex){ + hmacSha1 = Mac.getInstance("HMAC-SHA-1"); + } + + SecretKeySpec macKey = new SecretKeySpec(seed, "RAW"); + hmacSha1.init(macKey); + + return hmacSha1.doFinal(counter); + + }catch(GeneralSecurityException ex){ + throw new UndeclaredThrowableException(ex); + } + } + + /** + * Generates a new seed value for a token + * the returned string will contain a randomly generated + * hex value + * @param length - defines the length of the new seed this should be either 128 or 160 + * @return + */ + public static String generateNewSeed(int length){ + + String salt = ""; + long ticks = Calendar.getInstance(TimeZone.getTimeZone("GMT")).getTimeInMillis(); + salt = salt + ticks; + + byte[] byteToHash = salt.getBytes(); + + MessageDigest md; + + try{ + if(length == 128){ + //128 long + md = MessageDigest.getInstance("MD5"); + }else{ + //160 long + md = MessageDigest.getInstance("SHA1"); + } + + md.reset(); + md.update(byteToHash); + + byte[] digest = md.digest(); + + //convert to hex string + + return byteArrayToHexString(digest); + + }catch(NoSuchAlgorithmException ex){ + return null; + } + } + + + public static String byteArrayToHexString(byte[] digest) { + + StringBuffer buffer = new StringBuffer(); + + for(int i =0; i < digest.length; i++){ + String hex = Integer.toHexString(0xff & digest[i]); + + if(hex.length() == 1) + buffer.append("0"); + + buffer.append(hex); + + } + + return buffer.toString(); + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/IToken.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/IToken.java new file mode 100644 index 0000000..bdf7a78 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/IToken.java @@ -0,0 +1,45 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token.tokens; + +public interface IToken { + + public String getName(); + + public String getSerialNumber(); + + public int getTokenType(); + + public String generateOtp(); + + public long getId(); + + public void setId(long id); + + public int getTimeStep(); + + public String getOrganisation(); + + public String getSeed(); + + public String getFullName(); + + public String getUrl(); +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/ITokenMeta.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/ITokenMeta.java new file mode 100644 index 0000000..20e2aa1 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/ITokenMeta.java @@ -0,0 +1,18 @@ +package uk.co.bitethebullet.android.token.tokens; + +public interface ITokenMeta { + + public String getName(); + + public String getOrganisation(); + + public int getTokenType(); + + public String getSecretBase32(); + + public int getDigits(); + + public int getTimeStep(); + + public int getCounter(); +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/IconSuggestor.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/IconSuggestor.java new file mode 100644 index 0000000..ad0572c --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/IconSuggestor.java @@ -0,0 +1,94 @@ +package uk.co.bitethebullet.android.token.tokens; + +import android.content.Context; + +import uk.co.bitethebullet.android.token.R; +import uk.co.bitethebullet.android.token.util.FontManager; + + +/** + * Class that will suggest the icon to use for a given token + * this will look at the tokens organisation and attempt to + * find a suitable match in the brands we define + */ +public class IconSuggestor { + + static String[] icons = {"fabAmazon", "fabApple", "fabAtlassian", "fabAws", "fabBitbucket", + "fabEbay", "fabEtsy", "fabEvernote", "fabFacebook", "fabGithub", + "fabGoogle", "fabInstagram", "fabJira", "fabMicrosoft", "fabPaypal", + "fabSaleforce", "fabSnapchat", "fabSquarespace", "fabStripe", + "fabTrello", "fabWordpress"}; + + + /** + * work out the icon we can use for the this token + * @param token + * @param context + * @return + */ + public IconResult getSuggestedIcon(IToken token, Context context){ + + IconResult result = null; + + String org = token.getOrganisation(); + int tokenType = token.getTokenType(); + + if(org != null && org.length() > 0){ + org = org.toLowerCase(); + + for (int i = 0; i < icons.length; i++){ + String iconMatch = icons[i].substring(3).toLowerCase(); + + if(org.contains(iconMatch)){ + result = new IconResult(FontManager.FONTAWESOME_BRANDS, + getStringResource(context, icons[i])); + + return result; + } + } + + } + + //if we get here we've not found anything to + //match the organisation on + //just show an icon based on the token type + if(tokenType == TokenMetaData.HOTP_TOKEN){ + result = new IconResult(FontManager.FONTAWESOME, + context.getString(R.string.farPlusSquare)); + }else{ + result = new IconResult(FontManager.FONTAWESOME, + context.getString(R.string.farClock)); + } + + return result; + } + + private static String getStringResource(Context context, String name) { + int stringId = context.getResources().getIdentifier(name, "string", context.getPackageName()); + + return context.getString(stringId); + } + + /** + * the result of the suggested icon to use + */ + public class IconResult { + + private String _fontname; + private String _content; + + public IconResult(String fontName, String content){ + this._fontname = fontName; + this._content = content; + } + + public String getFont(){ + return _fontname; + } + + public String getContent(){ + return _content; + } + + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenFactory.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenFactory.java new file mode 100644 index 0000000..e76ce12 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenFactory.java @@ -0,0 +1,85 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token.tokens; + +import android.database.Cursor; + +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; + +public class TokenFactory { + + /** + * Creates a IToken object from the database cursor + * @param c + * @param ctx + * @return + */ + public static IToken CreateToken(Cursor c){ + + if(c == null){ + return null; + } + + IToken token = null; + + int tokenType = c.getInt(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_TYPE)); + + switch(tokenType){ + + case TokenDbAdapter.TOKEN_TYPE_EVENT: + token = CreateHotpToken(c); + break; + + case TokenDbAdapter.TOKEN_TYPE_TIME: + token = CreateTotpToken(c); + break; + + default: + return null; + } + + token.setId(c.getLong(c.getColumnIndex(TokenDbAdapter.KEY_TOKEN_ROWID))); + return token; + } + + private static IToken CreateHotpToken(Cursor c){ + HotpToken token = new HotpToken( + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_NAME)), + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_SERIAL)), + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_SEED)), + c.getLong(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_COUNT)), + c.getInt(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_OTP_LENGTH)), + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_ORGANISATION))); + + return token; + } + + private static IToken CreateTotpToken(Cursor c){ + TotpToken token = new TotpToken( + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_NAME)), + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_SERIAL)), + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_SEED)), + c.getInt(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_TIME_STEP)), + c.getInt(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_OTP_LENGTH)), + c.getString(c.getColumnIndexOrThrow(TokenDbAdapter.KEY_TOKEN_ORGANISATION))); + + return token; + } +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenHelper.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenHelper.java new file mode 100644 index 0000000..94d491a --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenHelper.java @@ -0,0 +1,43 @@ +package uk.co.bitethebullet.android.token.tokens; + +import android.database.Cursor; + +import java.util.ArrayList; + +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; + + +public class TokenHelper { + + TokenDbAdapter mDbAdaptor; + + public TokenHelper(TokenDbAdapter dbAdapter){ + this.mDbAdaptor = dbAdapter; + } + + public ArrayList getTokens(){ + ArrayList tokens = new ArrayList(); + Cursor c = mDbAdaptor.fetchAllTokens(); + + if (c.moveToFirst()){ + do{ + tokens.add(TokenFactory.CreateToken(c)); + }while(c.moveToNext()); + } + c.close(); + + return tokens; + } + + public CharSequence[] getTokenFullNames(){ + ArrayList tokens = this.getTokens(); + + CharSequence[] charSequences = new CharSequence[tokens.size()]; + for(int i = 0; i < tokens.size(); i++){ + charSequences[i] = tokens.get(i).getFullName(); + } + + return charSequences; + } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenMetaData.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenMetaData.java new file mode 100644 index 0000000..be1bab8 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TokenMetaData.java @@ -0,0 +1,63 @@ +package uk.co.bitethebullet.android.token.tokens; + +import uk.co.bitethebullet.android.token.tokens.ITokenMeta; + +public class TokenMetaData implements ITokenMeta { + + public static final int HOTP_TOKEN = 0; + public static final int TOTP_TOKEN = 1; + + String tokenName; + String organisation; + int tokenType; + String secretBase32; + int digits; + int timeStep; + int counter; + + public TokenMetaData(String tokenName, int tokenType, String secret, + int digits, int timeStep, int counter){ + + this(tokenName, tokenType, secret, digits, timeStep, counter, null); + } + + public TokenMetaData(String tokenName, int tokenType, String secret, + int digits, int timeStep, int counter, + String organisation) + { + this.tokenName = tokenName; + this.tokenType = tokenType; + this.secretBase32 = secret; + this.digits = digits; + this.timeStep = timeStep; + this.counter = counter; + this.organisation = organisation; + } + + public String getName() { + return tokenName; + } + + public int getTokenType() { + return tokenType; + } + + public String getSecretBase32() { + return secretBase32; + } + + public int getDigits() { + return digits; + } + + public int getTimeStep() { + return timeStep; + } + + public int getCounter() { + return counter; + } + + public String getOrganisation(){ return organisation; } + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TotpToken.java b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TotpToken.java new file mode 100644 index 0000000..48908fb --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/tokens/TotpToken.java @@ -0,0 +1,99 @@ +/* + * Copyright Mark McAvoy - www.bitethebullet.co.uk 2009 - 2020 + * + * This file is part of Android Token. + * + * Android Token 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. + * + * Android Token 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 Android Token. If not, see . + * + */ +package uk.co.bitethebullet.android.token.tokens; + +import java.io.IOException; +import java.util.Calendar; +import java.util.TimeZone; + +import uk.co.bitethebullet.android.token.datalayer.TokenDbAdapter; +import uk.co.bitethebullet.android.token.util.SeedConvertor; + + +/** + * TOTP Token + * + * Generates an OTP based on the time, for more information see + * http://tools.ietf.org/html/draft-mraihi-totp-timebased-00 + * + */ +public class TotpToken extends HotpToken { + + private int mTimeStep; + + public TotpToken(String name, String serial, String seed, int timeStep, + int otpLength, String organisation){ + super(name, serial, seed, 0, otpLength, organisation); + + mTimeStep = timeStep; + } + + @Override + public int getTimeStep(){ + return mTimeStep; + } + + @Override + public int getTokenType(){ + return TokenDbAdapter.TOKEN_TYPE_TIME; + } + + @Override + public String generateOtp() { + + //calculate the moving counter using the time + return generateOtp(Calendar.getInstance(TimeZone.getTimeZone("UTC"))); + } + + @Override + public String getUrl() { + //otpauth://totp/organisation:alice@google.com?secret=JBSWY3DPEHPK3PXP" + try { + //convert the seed from hex to base32 + String base32Secret = SeedConvertor.ConvertFromBA(SeedConvertor.ConvertFromEncodingToBA(getSeed(), SeedConvertor.HEX_FORMAT), + SeedConvertor.BASE32_FORMAT); + + StringBuilder buffer = new StringBuilder(); + buffer.append("otpauth://totp/"); + + if (getOrganisation() != null && getOrganisation().length() > 0) { + buffer.append(java.net.URLEncoder.encode(getOrganisation())); + buffer.append(":"); + } + + buffer.append(java.net.URLEncoder.encode(getName())); + buffer.append("?secret="); + buffer.append(base32Secret); + + return buffer.toString(); + }catch(IOException ex){ + return null; + } + } + + public String generateOtp(Calendar currentTime){ + long time = currentTime.getTimeInMillis()/1000L; + super.setEventCount(time/new Long(mTimeStep)); + + return super.generateOtp(); + } + + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/util/Base32.java b/app/src/main/java/uk/co/bitethebullet/android/token/util/Base32.java new file mode 100644 index 0000000..f7e397a --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/util/Base32.java @@ -0,0 +1,235 @@ +package uk.co.bitethebullet.android.token.util; + +import java.util.ArrayList; + +public class Base32 { + + private static final String DEF_ENCODING_TABLE = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567"; + private static final char DEF_PADDING = '='; + + private String eTable; //Encoding table + private char padding; + private byte[] dTable; //Decoding table + + public Base32 () + { + this (DEF_ENCODING_TABLE, DEF_PADDING); + } + + public Base32 (char padding){ + this (DEF_ENCODING_TABLE, padding); + } + public Base32 (String encodingTable){ + this (encodingTable, DEF_PADDING); + } + + public Base32 (String encodingTable, char padding) { + this.eTable = encodingTable; + this.padding = padding; + dTable = new byte[0x80]; + InitialiseDecodingTable (); + } + + public String encodeBytes (byte[] input) { + StringBuffer output = new StringBuffer (); + int specialLength = input.length % 5; + int normalLength = input.length - specialLength; + + for (int i = 0; i < normalLength; i += 5) { + int b1 = input[i] & 0xff; + int b2 = input[i + 1] & 0xff; + int b3 = input[i + 2] & 0xff; + int b4 = input[i + 3] & 0xff; + int b5 = input[i + 4] & 0xff; + + output.append(eTable.charAt((b1 >> 3) & 0x1f)); + output.append(eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append(eTable.charAt((b2 >> 1) & 0x1f)); + output.append(eTable.charAt(((b2 << 4) | (b3 >> 4)) & 0x1f)); + output.append(eTable.charAt(((b3 << 1) | (b4 >> 7)) & 0x1f)); + output.append(eTable.charAt((b4 >> 2) & 0x1f)); + output.append(eTable.charAt(((b4 << 3) | (b5 >> 5)) & 0x1f)); + output.append(eTable.charAt(b5 & 0x1f)); + } + + switch (specialLength) { + case 1: { + int b1 = input[normalLength] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt((b1 << 2) & 0x1f)); + output.append (padding).append (padding).append (padding).append (padding).append (padding).append (padding); + break; + } + + case 2: { + int b1 = input[normalLength] & 0xff; + int b2 = input[normalLength + 1] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append (eTable.charAt((b2 >> 1) & 0x1f)); + output.append (eTable.charAt((b2 << 4) & 0x1f)); + output.append (padding).append (padding).append (padding).append (padding); + break; + } + case 3: { + int b1 = input[normalLength] & 0xff; + int b2 = input[normalLength + 1] & 0xff; + int b3 = input[normalLength + 2] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append (eTable.charAt((b2 >> 1) & 0x1f)); + output.append (eTable.charAt(((b2 << 4) | (b3 >> 4)) & 0x1f)); + output.append (eTable.charAt((b3 << 1) & 0x1f)); + output.append (padding).append (padding).append (padding); + break; + } + case 4: { + int b1 = input[normalLength] & 0xff; + int b2 = input[normalLength + 1] & 0xff; + int b3 = input[normalLength + 2] & 0xff; + int b4 = input[normalLength + 3] & 0xff; + output.append (eTable.charAt((b1 >> 3) & 0x1f)); + output.append (eTable.charAt(((b1 << 2) | (b2 >> 6)) & 0x1f)); + output.append (eTable.charAt((b2 >> 1) & 0x1f)); + output.append (eTable.charAt(((b2 << 4) | (b3 >> 4)) & 0x1f)); + output.append (eTable.charAt(((b3 << 1) | (b4 >> 7)) & 0x1f)); + output.append (eTable.charAt((b4 >> 2) & 0x1f)); + output.append (eTable.charAt((b4 << 3) & 0x1)); + output.append (padding); + break; + } + } + + return output.toString(); + } + + public byte[] decodeBytes (String data) { + ArrayList outStream = new ArrayList(); + + int length = data.length(); + + while (length > 0) { + if (!this.Ignore (data.charAt(length - 1))) break; + length--; + } + + int i = 0; + int finish = length - 8; + for (i = this.NextI (data, i, finish); i < finish; i = this.NextI (data, i, finish)) { + byte b1 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b2 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b3 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b4 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b5 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b6 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b7 = dTable[data.charAt(i++)]; + i = this.NextI (data, i, finish); + byte b8 = dTable[data.charAt(i++)]; + + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + outStream.add ((byte)((b5 << 7) | (b6 << 2) | (b7 >> 3))); + outStream.add ((byte)((b7 << 5) | b8)); + } + this.DecodeLastBlock (outStream, + data.charAt(length - 8), data.charAt(length - 7), data.charAt(length - 6), data.charAt(length - 5), + data.charAt(length - 4), data.charAt(length - 3), data.charAt(length - 2), data.charAt(length - 1)); + + byte[] result = new byte[outStream.size()]; + for(int j = 0; j < outStream.size(); j++){ + result[j] = outStream.get(j).byteValue(); + } + + return result; + } + + protected int DecodeLastBlock (ArrayList outStream, char c1, char c2, char c3, char c4, char c5, char c6, char c7, char c8) { + if (c3 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + return 1; + } + + if (c5 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + return 2; + } + + if (c6 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + byte b5 = dTable[c5]; + + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + return 3; + } + + if (c8 == padding) { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + byte b5 = dTable[c5]; + byte b6 = dTable[c6]; + byte b7 = dTable[c7]; + + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + outStream.add ((byte)((b5 << 7) | (b6 << 2) | (b7 >> 3))); + return 4; + } + + else { + byte b1 = dTable[c1]; + byte b2 = dTable[c2]; + byte b3 = dTable[c3]; + byte b4 = dTable[c4]; + byte b5 = dTable[c5]; + byte b6 = dTable[c6]; + byte b7 = dTable[c7]; + byte b8 = dTable[c8]; + outStream.add ((byte)((b1 << 3) | (b2 >> 2))); + outStream.add ((byte)((b2 << 6) | (b3 << 1) | (b4 >> 4))); + outStream.add ((byte)((b4 << 4) | (b5 >> 1))); + outStream.add ((byte)((b5 << 7) | (b6 << 2) | (b7 >> 3))); + outStream.add ((byte)((b7 << 5) | b8)); + return 5; + } + } + + protected int NextI (String data, int i, int finish) { + while ((i < finish) && this.Ignore (data.charAt(i))) i++; + + return i; + } + + protected boolean Ignore (char c) { + return (c == '\n') || (c == '\r') || (c == '\t') || (c == ' ') || (c == '-'); + } + + protected void InitialiseDecodingTable () { + for (int i = 0; i < eTable.length(); i++) { + dTable[eTable.charAt(i)] = (byte)i; + } + } + + +} diff --git a/app/src/main/java/uk/co/bitethebullet/android/token/util/Base64.java b/app/src/main/java/uk/co/bitethebullet/android/token/util/Base64.java new file mode 100644 index 0000000..cad6ce4 --- /dev/null +++ b/app/src/main/java/uk/co/bitethebullet/android/token/util/Base64.java @@ -0,0 +1,2068 @@ +/** + *