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" method 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()
- * ); 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.
+ *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: + *
+ *+ * I am placing this code in the Public Domain. Do with it as you will. + * This software comes with no guarantees or warranties but with + * plenty of well-wishing instead! + * Please visit http://iharder.net/base64 + * periodically to check for updates or to contribute improvements. + *
* * @author Robert Harder * @author rob@iharder.net - * @version 2.3.3 + * @version 2.3.7 */ -public class Base64 { +public class Base64 +{ - /** - * 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 { + /* ******** P U B L I C F I E L D S ******** */ - private final boolean encode; // Encoding or decoding - private int position; // Current position in the buffer - private final byte[] buffer; // Small buffer holding converted data - private final int bufferLength; // Length of buffer (3 or 4) - private int numSigBytes; // Number of meaningful bytes in the buffer - private int lineLength; - private final boolean breakLines; // Break lines at less than 80 characters - private final int options; // Record options used to create the stream. - // private final byte[] alphabet; // Local copies to avoid extra method calls - private final 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)</i> - *- * - * 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
- breakLines = (options & DO_BREAK_LINES) > 0;
- encode = (options & ENCODE) > 0;
- bufferLength = encode ? 4 : 3;
- buffer = new byte[bufferLength];
- position = -1;
- lineLength = 0;
- // alphabet = getAlphabet(options);
- 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 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 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
-
- 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;
- else
- // Must have broken out from above.
- throw new java.io.IOException("Improperly padded Base64 input.");
-
- } // end else: decode
-
- // Got data?
- if (position >= 0) {
- // End of relevant data?
- if ( /* !encode && */position >= numSigBytes)
- return -1;
-
- 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;
-
- return b & 0xFF; // This is how you "cast" a byte that's
- // intended to be unsigned.
- } // end else
- } // end if: position >= 0
- else
- throw new java.io.IOException("Error in Base64 code reading stream.");
- } // 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
- } // end for: each byte read
- return i;
- } // end read
-
- } // end inner class InputStream
-
- /**
- * 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 final boolean encode;
- private int position;
- private byte[] buffer;
- private final int bufferLength;
- private int lineLength;
- private final boolean breakLines;
- private final byte[] b4; // Scratch used in a few places
- private boolean suspendEncoding;
- private final int options; // Record for later
- // private final byte[] alphabet; // Local copies to avoid extra method calls
- private final 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)</i> - *- * - * 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);
- breakLines = (options & DO_BREAK_LINES) > 0;
- encode = (options & ENCODE) > 0;
- bufferLength = encode ? 3 : 4;
- buffer = new byte[bufferLength];
- position = 0;
- lineLength = 0;
- suspendEncoding = false;
- b4 = new byte[4];
- this.options = options;
- // alphabet = getAlphabet(options);
- decodabet = getDecodabet(options);
- } // end constructor
-
- /**
- * 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
- flush();
-
- // 2. Actually close the stream
- // Base class both flushes and closes.
- super.close();
-
- buffer = null;
- out = null;
- } // end close
-
- /**
- * Flushes the stream (and the enclosing streams).
- *
- * @throws java.io.IOException
- * @since 2.3
- */
- @Override
- public void flush()
- throws java.io.IOException {
- flushBase64();
- super.flush();
- }
-
- /**
- * 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 flush
-
- /**
- * 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() {
- suspendEncoding = false;
- } // end resumeEncoding
-
- /**
- * 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();
- suspendEncoding = true;
- } // end suspendEncoding
-
- /**
- * 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) {
- super.out.write(theBytes, off, len);
- return;
- } // end if: supsended
-
- for (int i = 0; i < len; i++)
- write(theBytes[off + i]);
-
- } // end write
-
- /**
- * 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) {
- super.out.write(theByte);
- return;
- } // end if: supsended
-
- // Encode?
- if (encode) {
- buffer[position++] = (byte) theByte;
- if (position >= bufferLength) { // Enough to encode.
-
- out.write(encode3to4(b4, buffer, bufferLength, options));
-
- lineLength += 4;
- if (breakLines && lineLength >= MAX_LINE_LENGTH) {
- out.write(NEW_LINE);
- lineLength = 0;
- } // end if: end of line
-
- position = 0;
- } // end if: enough to output
- } // end if: encoding
- 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 write
-
- } // end inner class OutputStream
/** No options specified. Value is zero. */
public final static int NO_OPTIONS = 0;
@@ -438,627 +159,559 @@ public class Base64 {
/** 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;
- /* ******** P R I V A T E F I E L D S ******** */
-
/**
- * 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.
+ * 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. */
+
+ /**
+ * 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) '=';
+ private final static byte EQUALS_SIGN = (byte)'=';
+
/** The new line character (\n) as a byte. */
- private final static byte NEW_LINE = (byte) '\n';
+ private final static byte NEW_LINE = (byte)'\n';
+
+
/** Preferred encoding. */
private final static String PREFERRED_ENCODING = "US-ASCII";
- /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
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
- /* ******** U R L S A F E B A S E 6 4 A L P H A B E T ******** */
+
+ /* ******** S T A N D A R D B A S E 6 4 A L P H A B E T ******** */
/** The 64 valid Base64 values. */
/* Host platform me be something funny like EBCDIC, so we hardcode these values. */
- private final static byte[] _STANDARD_ALPHABET = {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E',
- (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
- (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W',
- (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
- (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
- (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
- (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6',
- (byte) '7', (byte) '8', (byte) '9', (byte) '+', (byte) '/'};
-
- /**
- * Translates a Base64 value to either its 6-bit reconstruction value or a negative number indicating some other
- * meaning.
- */
- private final static byte[] _STANDARD_DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
- // 0 - 8
- -5, -5, // Whitespace: Tab and Linefeed
- -9, -9, // Decimal 11 - 12
- -5, // Whitespace: Carriage Return
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
- -9, -9, -9, -9, -9, // Decimal 27 - 31
- -5, // Whitespace: Space
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
- 62, // Plus sign at decimal 43
- -9, -9, -9, // Decimal 44 - 46
- 63, // Slash at decimal 47
- 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
- -9, -9, -9, // Decimal 58 - 60
- -1, // Equals sign at decimal 61
- -9, -9, -9, // Decimal 62 - 64
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
- 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
- -9, -9, -9, -9, -9, -9, // Decimal 91 - 96
- 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
- 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
- -9, -9, -9, -9 // Decimal 123 - 126
- /*
- * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
- */
+ 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)'/'
};
- /* ******** O R D E R E D B A S E 6 4 A L P H A B E T ******** */
/**
- * Used in the URL- and Filename-safe dialect described in Section 4 of RFC3548: http://www.faqs.org/rfcs/rfc3548.html. Notice that the last two
- * bytes become "hyphen" and "underscore" instead of "plus" and "slash."
- */
- private final static byte[] _URL_SAFE_ALPHABET = {(byte) 'A', (byte) 'B', (byte) 'C', (byte) 'D', (byte) 'E',
- (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L', (byte) 'M', (byte) 'N',
- (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U', (byte) 'V', (byte) 'W',
- (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) 'a', (byte) 'b', (byte) 'c', (byte) 'd', (byte) 'e', (byte) 'f',
- (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l', (byte) 'm', (byte) 'n', (byte) 'o',
- (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u', (byte) 'v', (byte) 'w', (byte) 'x',
- (byte) 'y', (byte) 'z', (byte) '0', (byte) '1', (byte) '2', (byte) '3', (byte) '4', (byte) '5', (byte) '6',
- (byte) '7', (byte) '8', (byte) '9', (byte) '-', (byte) '_'};
-
- /** Used in decoding URL- and Filename-safe dialects of Base64. */
- private final static byte[] _URL_SAFE_DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
- // 0 - 8
- -5, -5, // Whitespace: Tab and Linefeed
- -9, -9, // Decimal 11 - 12
- -5, // Whitespace: Carriage Return
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
- -9, -9, -9, -9, -9, // Decimal 27 - 31
- -5, // Whitespace: Space
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
- -9, // Plus sign at decimal 43
- -9, // Decimal 44
- 62, // Minus sign at decimal 45
- -9, // Decimal 46
- -9, // Slash at decimal 47
- 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, // Numbers zero through nine
- -9, -9, -9, // Decimal 58 - 60
- -1, // Equals sign at decimal 61
- -9, -9, -9, // Decimal 62 - 64
- 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, // Letters 'A' through 'N'
- 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, // Letters 'O' through 'Z'
- -9, -9, -9, -9, // Decimal 91 - 94
- 63, // Underscore at decimal 95
- -9, // Decimal 96
- 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, // Letters 'a' through 'm'
- 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, // Letters 'n' through 'z'
- -9, -9, -9, -9 // Decimal 123 - 126
- /*
- * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
- */
+ * 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
};
- /* ******** D E T E R M I N E W H I C H A L H A B E T ******** */
+
+ /* ******** U R L S A F E 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.
+ * 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[] _ORDERED_ALPHABET = {(byte) '-', (byte) '0', (byte) '1', (byte) '2', (byte) '3',
- (byte) '4', (byte) '5', (byte) '6', (byte) '7', (byte) '8', (byte) '9', (byte) 'A', (byte) 'B', (byte) 'C',
- (byte) 'D', (byte) 'E', (byte) 'F', (byte) 'G', (byte) 'H', (byte) 'I', (byte) 'J', (byte) 'K', (byte) 'L',
- (byte) 'M', (byte) 'N', (byte) 'O', (byte) 'P', (byte) 'Q', (byte) 'R', (byte) 'S', (byte) 'T', (byte) 'U',
- (byte) 'V', (byte) 'W', (byte) 'X', (byte) 'Y', (byte) 'Z', (byte) '_', (byte) 'a', (byte) 'b', (byte) 'c',
- (byte) 'd', (byte) 'e', (byte) 'f', (byte) 'g', (byte) 'h', (byte) 'i', (byte) 'j', (byte) 'k', (byte) 'l',
- (byte) 'm', (byte) 'n', (byte) 'o', (byte) 'p', (byte) 'q', (byte) 'r', (byte) 's', (byte) 't', (byte) 'u',
- (byte) 'v', (byte) 'w', (byte) 'x', (byte) 'y', (byte) 'z'};
-
- /** Used in decoding the "ordered" dialect of Base64. */
- private final static byte[] _ORDERED_DECODABET = {-9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal
- // 0 - 8
- -5, -5, // Whitespace: Tab and Linefeed
- -9, -9, // Decimal 11 - 12
- -5, // Whitespace: Carriage Return
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 14 - 26
- -9, -9, -9, -9, -9, // Decimal 27 - 31
- -5, // Whitespace: Space
- -9, -9, -9, -9, -9, -9, -9, -9, -9, -9, // Decimal 33 - 42
- -9, // Plus sign at decimal 43
- -9, // Decimal 44
- 0, // Minus sign at decimal 45
- -9, // Decimal 46
- -9, // Slash at decimal 47
- 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, // Numbers zero through nine
- -9, -9, -9, // Decimal 58 - 60
- -1, // Equals sign at decimal 61
- -9, -9, -9, // Decimal 62 - 64
- 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, // Letters 'A' through 'M'
- 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, // Letters 'N' through 'Z'
- -9, -9, -9, -9, // Decimal 91 - 94
- 37, // Underscore at decimal 95
- -9, // Decimal 96
- 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, // Letters 'a' through 'm'
- 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, // Letters 'n' through 'z'
- -9, -9, -9, -9 // Decimal 123 - 126
- /*
- * ,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 127 - 139
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 140 - 152
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 153 - 165
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 166 - 178
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 179 - 191
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 192 - 204
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 205 - 217
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 218 - 230
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9, // Decimal 231 - 243
- * -9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9,-9 // Decimal 244 - 255
- */
+ 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)'_'
};
/**
- * 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
+ * Used in decoding URL- and Filename-safe dialects of Base64.
*/
- public static byte[] decode(byte[] source) {
- byte[] decoded = null;
- try {
- decoded = decode(source, 0, source.length, Base64.NO_OPTIONS);
- } catch (java.io.IOException ex) {
- assert false : "IOExceptions only come from GZipping, which is turned off: " + ex.getMessage();
+ 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;
}
- return decoded;
- }
+ } // end getAlphabet
- /* ******** E N C O D I N G M E T H O D S ******** */
/**
- * 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.
+ * 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 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
+ * @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.
* - * @return decoded data - * - * @throws java.io.IOException If bogus characters exist in source data + * @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 */ - public static byte[] decode(byte[] source, int off, int len, int options) - throws java.io.IOException { + private static byte[] encode3to4( + byte[] source, int srcOffset, int numSigBytes, + byte[] destination, int destOffset, int options ) { - // Lots of error checking and exception throwing - if (source == null) - throw new NullPointerException("Cannot decode null source array."); - 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)); + byte[] ALPHABET = getAlphabet( options ); - 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); + // 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 - byte[] DECODABET = getDecodabet(options); + // 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 ); - 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 + 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; - byte[] b4 = new byte[4]; // Four byte buffer from source, eliminating white space - int b4Posn = 0; // Keep track of four byte input buffer - int i = 0; // Source array counter - byte sbiCrop = 0; // Low seven bits (ASCII) of input - byte sbiDecode = 0; // Special value from DECODABET + 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; - for (i = off; i < off + len; i++) { // Loop through source + 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; - sbiCrop = (byte) (source[i] & 0x7f); // Only the low seven bits - sbiDecode = DECODABET[sbiCrop]; // Special value + default: + return destination; + } // end switch + } // end encode3to4 - // White space, Equals sign, or legit Base64 character - // Note the values such as -5 and -9 in the - // DECODABETs at the top of the file. - if (sbiDecode >= WHITE_SPACE_ENC) { - if (sbiDecode >= EQUALS_SIGN_ENC) { - b4[b4Posn++] = sbiCrop; // Save non-whitespace - if (b4Posn > 3) { // Time to decode? - outBuffPosn += decode4to3(b4, 0, outBuff, outBuffPosn, options); - b4Posn = 0; - // If that was the equals sign, break out of 'for' loop - if (sbiCrop == EQUALS_SIGN) - break; - } // end if: quartet built - } // end if: equals sign or better - } // end if: white space, equals sign or better - else - // There's a bad input character in the Base64 stream. - throw new java.io.IOException(String.format("Bad Base64 input character '%c' in array position %d", - source[i], i)); - } // 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. + * Performs Base64 encoding on theraw 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 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.");
-
- 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)
- if (bytes != null && bytes.length >= 4) {
-
- int head = 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);
-
- // No error? Get new bytes.
- bytes = baos.toByteArray();
-
- } // end try
- catch (java.io.IOException e) {
- // 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
-
- /**
- * 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
-
- /**
- * 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 next 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)."); - 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; - - // 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 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 next 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 - - /** - * 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 { - - // Decode and gunzip if necessary - byte[] objBytes = decode(encodedObject); - - java.io.ByteArrayInputStream bais = null; - java.io.ObjectInputStream ois = null; - Object obj = null; - - try { - bais = new java.io.ByteArrayInputStream(objBytes); - ois = new java.io.ObjectInputStream(bais); - - 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 - - /** - * Performs Base64 encoding on theraw 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 raw input buffer
* @param encoded output buffer
- *
* @since 2.3
*/
- public static void encode(java.nio.ByteBuffer raw, java.nio.ByteBuffer encoded) {
+ 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);
+ 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
+ } // 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}.
+ * 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 raw input buffer
* @param encoded output buffer
- *
* @since 2.3
*/
- public static void encode(java.nio.ByteBuffer raw, java.nio.CharBuffer encoded) {
+ 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
+ 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
}
+
+
+
/**
- * Encodes a byte array into Base64 notation. Does not GZip-compress data.
+ * 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) {
+ 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.
@@ -1067,664 +720,1346 @@ public class Base64 {
encoded = encodeBytes(source, 0, source.length, NO_OPTIONS);
} catch (java.io.IOException ex) {
assert false : ex.getMessage();
- } // end catch
+ } // end catch
assert encoded != null;
return encoded;
- } // end encodeBytes
+ } // end encodeBytes
+
+
/**
- * Encodes a byte array into Base64 notation.
Example options:
- *+ * Encodes a byte array into Base64 notation. + *+ * Example options:
* GZIP: gzip-compresses object before encoding it. * DO_BREAK_LINES: break lines at 76 characters - * <i>Note: Technically, this makes your encoding non-compliant.</i> + * Note: Technically, this makes your encoding non-compliant. *- *Example:
encodeBytes( myData, Base64.GZIP )orExample:
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 next it.
+ *+ * Example:
encodeBytes( myData, Base64.GZIP )or + *+ * Example:
encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )* - * @param source The data to convert - * @param options Specified options * - * @return The Base64-encoded data as a String + *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.
* - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public static String encodeBytes(byte[] source, int options) - throws java.io.IOException { - return encodeBytes(source, 0, source.length, options); - } // end encodeBytes - - /** - * Encodes a byte array into Base64 notation. Does not GZip-compress data.As of v 2.3, if there is an - * error, the method will throw an java.io.IOException. This is new to v2.3! In earlier versions, it just - * returned a null value, but in retrospect that's a pretty poor way to next 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 + * @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. * - * @throws NullPointerException if source array is null + *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) { + 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); + encoded = encodeBytes( source, off, len, NO_OPTIONS ); } catch (java.io.IOException ex) { assert false : ex.getMessage(); - } // end catch + } // end catch assert encoded != null; return encoded; - } // end encodeBytes + } // end encodeBytes + + /** - * Encodes a byte array into Base64 notation.Example options:
- *+ * Encodes a byte array into Base64 notation. + *+ * Example options:
* GZIP: gzip-compresses object before encoding it. * DO_BREAK_LINES: break lines at 76 characters - * <i>Note: Technically, this makes your encoding non-compliant.</i> + * Note: Technically, this makes your encoding non-compliant. *- *Example:
encodeBytes( myData, Base64.GZIP )orExample:
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 next it.
+ *+ * Example:
encodeBytes( myData, Base64.GZIP )or + *+ * Example:
encodeBytes( myData, Base64.GZIP | Base64.DO_BREAK_LINES )* - * @param source The data to convert - * @param off Offset in array where conversion should begin - * @param len Length of data to convert + * + *As of v 2.3, if there is an error with the GZIP stream, + * the method will throw an java.io.IOException. This is new to v2.3! + * In earlier versions, it just returned a null value, but + * in retrospect that's a pretty poor way to handle it.
+ * + * + * @param source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert * @param options Specified options - * * @return The Base64-encoded data as a String - * - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid * @see Base64#GZIP * @see Base64#DO_BREAK_LINES + * @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); + 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 + return new String( encoded, PREFERRED_ENCODING ); + } // end try catch (java.io.UnsupportedEncodingException uue) { - return new String(encoded); - } // end catch + return new String( encoded ); + } // end catch + + } // end encodeBytes + + - } // 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. + * 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) { + public static byte[] encodeBytesToBytes( byte[] source ) { byte[] encoded = null; try { - encoded = encodeBytesToBytes(source, 0, source.length, Base64.NO_OPTIONS); - } catch (java.io.IOException ex) { + 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. + * 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 source The data to convert + * @param off Offset in array where conversion should begin + * @param len Length of data to convert * @param options Specified options - * * @return The Base64-encoded data as a String - * - * @throws java.io.IOException if there is an error - * @throws NullPointerException if source array is null - * @throws IllegalArgumentException if source array, offset, or length are invalid * @see Base64#GZIP * @see Base64#DO_BREAK_LINES + * @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 { + 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."); + 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); + 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 - if (len < 0) - throw new IllegalArgumentException("Cannot have length offset: " + len); - 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)); // Compress? - if ((options & GZIP) > 0) { - java.io.ByteArrayOutputStream baos = null; - java.util.zip.GZIPOutputStream gzos = null; - Base64.OutputStream b64os = null; + 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); + b64os = new Base64.OutputStream( baos, ENCODE | options ); + gzos = new java.util.zip.GZIPOutputStream( b64os ); - gzos.write(source, off, len); + gzos.write( source, off, len ); gzos.close(); - } // end try - catch (java.io.IOException e) { + } // 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 + } // end catch finally { - try { - gzos.close(); - } catch (Exception e) { - } - try { - b64os.close(); - } catch (Exception e) { - } - try { - baos.close(); - } catch (Exception e) { - } - } // end 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 + } // end if: compress // Else, don't compress. Better not to use streams at all then. else { - boolean breakLines = (options & DO_BREAK_LINES) > 0; + 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 + //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) + 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]; + } + 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); + 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; + 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 + } // end if: end of line + } // en dfor: each piece of array - if (d < len) { - encode3to4(source, d + off, len - d, outBuff, e, options); + if( d < len ) { + encode3to4( source, d+off, len - d, outBuff, e, options ); e += 4; - } // end if: some padding needed + } // end if: some padding needed + // Only resize array if we didn't guess it right. - if (e < outBuff.length - 1) { + 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 - // ); + 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."); + } else { + //System.err.println("No need to resize array."); return outBuff; + } - } // end else: don't compress + } // end else: don't compress + + } // end encodeBytesToBytes + + + + + + /* ******** D E C O D I N G M E T H O D S ******** */ - } // end encodeBytesToBytes /** - * Reads infile and encodes it to outfile. + * 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 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 - - /** - * 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 next it.
- * - * @param filename Filename for reading binary data - * - * @return base64-encoded string - * - * @throws java.io.IOException if there is an error - * @since 2.1 - */ - public static String encodeFromFile(String filename) - throws java.io.IOException { - - String encodedData = null; - Base64.InputStream bis = null; - try { - // Set up some useful variables - java.io.File file = new java.io.File(filename); - byte[] buffer = new byte[Math.max((int) (file.length() * 1.4), 40)]; // Need max() for - // math on small - // files (v2.2.1) - int length = 0; - int numBytes = 0; - - // Open a stream - bis = new Base64.InputStream(new java.io.BufferedInputStream(new java.io.FileInputStream(file)), - Base64.ENCODE); - - // Read until done - while ((numBytes = bis.read(buffer, length, 4096)) >= 0) - length += numBytes; - - // 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 - - /** - * 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 next 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 next it.
- * - * The object is not GZip-compressed before being encoded. - * - * Example options: - * - *- * GZIP: gzip-compresses object before encoding it. - * DO_BREAK_LINES: break lines at 76 characters - *- * - * Example:encodeObject( myObj, Base64.GZIP )or - * - * Example:encodeObject( myObj, Base64.GZIP | Base64.DO_BREAK_LINES )- * - * @param serializableObject The object to encode - * @param options Specified options - * - * @return The Base64-encoded object - * - * @throws java.io.IOException if there is an error - * @see Base64#GZIP - * @see Base64#DO_BREAK_LINES - * @since 2.0 - */ - public static String encodeObject(java.io.Serializable serializableObject, int options) - throws java.io.IOException { - - if (serializableObject == null) - throw new NullPointerException("Cannot serialize a null object."); - - // Streams - java.io.ByteArrayOutputStream baos = null; - java.io.OutputStream b64os = null; - java.io.ObjectOutputStream oos = null; - - try { - // ObjectOutputStream -> (GZIP) -> Base64 -> ByteArrayOutputStream - // Note that the optional GZIPping is handled by Base64.OutputStream. - baos = new java.io.ByteArrayOutputStream(); - b64os = new Base64.OutputStream(baos, ENCODE | options); - 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 { - 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 - - /** - * 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 next 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."); - - 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 - - /** - * 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 method with all possible parameters.
- * - * @param source the array to convert - * @param srcOffset the index where conversion begins + * @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) - * + * @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. + * @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) { + 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."); - if (destination == null) - throw new NullPointerException("Destination array was null."); - 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)); - 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)); + 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); + + byte[] DECODABET = getDecodabet( options ); // Example: Dk== - if (source[srcOffset + 2] == EQUALS_SIGN) { + 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; + //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); + destination[ destOffset ] = (byte)( outBuff >>> 16 ); return 1; } // Example: DkL= - else if (source[srcOffset + 3] == EQUALS_SIGN) { + 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; + //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); + 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; + //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; + + destination[ destOffset ] = (byte)( outBuff >> 16 ); + destination[ destOffset + 1 ] = (byte)( outBuff >> 8 ); + destination[ destOffset + 2 ] = (byte)( outBuff ); return 3; } - } // end decodeToBytes + } // end decodeToBytes + + + + /** - * 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. + * 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 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 + * @param source The Base64 encoded data + * @return decoded data + * @since 2.3.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 method 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 - - /** - * 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 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 static byte[] getDecodabet(int options) { - if ((options & URL_SAFE) == URL_SAFE) - return _URL_SAFE_DECODABET; - else if ((options & ORDERED) == ORDERED) - return _ORDERED_DECODABET; - else - return _STANDARD_DECODABET; - } // end getAlphabet - - /** Defeats instantiation. */ - private Base64() { + public static 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; } -} // end class Base64 + + + /** + * 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/src/main/java/net/schmizz/sshj/common/Buffer.java b/src/main/java/net/schmizz/sshj/common/Buffer.java index 8cf35ae3..c11852d7 100644 --- a/src/main/java/net/schmizz/sshj/common/Buffer.java +++ b/src/main/java/net/schmizz/sshj/common/Buffer.java @@ -132,8 +132,9 @@ public class Buffer> { protected void ensureAvailable(int a) throws BufferException { - if (available() < a) + if (available() < a) { throw new BufferException("Underflow"); + } } public void ensureCapacity(int capacity) { @@ -147,7 +148,6 @@ public class Buffer > { /** Compact this {@link SSHPacket} */ public void compact() { - System.err.println("COMPACTING"); if (available() > 0) System.arraycopy(data, rpos, data, 0, wpos - rpos); wpos -= rpos; @@ -246,7 +246,7 @@ public class Buffer > { * @return this */ public T putBytes(byte[] b, int off, int len) { - return putUInt32(len - off).putRawBytes(b, off, len); + return putUInt32(len).putRawBytes(b, off, len); } public void readRawBytes(byte[] buf) @@ -356,8 +356,9 @@ public class Buffer > { } public T putUInt64(long uint64) { - if (uint64 < 0) + if (uint64 < 0) { throw new IllegalArgumentException("Invalid value: " + uint64); + } return putUInt64Unchecked(uint64); } @@ -371,6 +372,7 @@ public class Buffer > { @SuppressWarnings("unchecked") private T putUInt64Unchecked(long uint64) { + ensureCapacity(8); data[wpos++] = (byte) (uint64 >> 56); data[wpos++] = (byte) (uint64 >> 48); data[wpos++] = (byte) (uint64 >> 40); @@ -392,8 +394,9 @@ public class Buffer > { public String readString(Charset cs) throws BufferException { int len = readUInt32AsInt(); - if (len < 0 || len > 32768) + if (len < 0 || len > 32768) { throw new BufferException("Bad item length: " + len); + } ensureAvailable(len); String s = new String(data, rpos, len, cs); rpos += len; @@ -460,10 +463,13 @@ public class Buffer > { public PublicKey readPublicKey() throws BufferException { + KeyType keyType = KeyType.fromString(readString()); try { - return KeyType.fromString(readString()).readPubKeyFromBuffer(this); + return keyType.readPubKeyFromBuffer(this); } catch (GeneralSecurityException e) { throw new SSHRuntimeException(e); + } catch (UnsupportedOperationException uoe) { + throw new BufferException("Could not decode keytype " + keyType); } } diff --git a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java index baf1048a..4d96c3e6 100644 --- a/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java +++ b/src/main/java/net/schmizz/sshj/common/ByteArrayUtils.java @@ -94,4 +94,34 @@ public class ByteArrayUtils { return sb.toString(); } + + public static byte[] parseHex(String hex) { + if (hex == null) { + throw new IllegalArgumentException("Hex string is null"); + } + if (hex.length() % 2 != 0) { + throw new IllegalArgumentException("Hex string '" + hex + "' should have even length."); + } + + byte[] result = new byte[hex.length() / 2]; + for (int i = 0; i < result.length; i++) { + int hi = parseHexDigit(hex.charAt(i * 2)) << 4; + int lo = parseHexDigit(hex.charAt(i * 2 + 1)); + result[i] = (byte) (hi + lo); + } + return result; + } + + private static int parseHexDigit(char c) { + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 10; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 10; + } + throw new IllegalArgumentException("Digit '" + c + "' out of bounds [0-9a-fA-F]"); + } } diff --git a/src/main/java/net/schmizz/sshj/common/ECDSAVariationsAdapter.java b/src/main/java/net/schmizz/sshj/common/ECDSAVariationsAdapter.java index bf810fc7..8e7d8003 100644 --- a/src/main/java/net/schmizz/sshj/common/ECDSAVariationsAdapter.java +++ b/src/main/java/net/schmizz/sshj/common/ECDSAVariationsAdapter.java @@ -15,26 +15,27 @@ */ package net.schmizz.sshj.common; +import com.hierynomus.sshj.secg.SecgUtils; +import org.bouncycastle.asn1.nist.NISTNamedCurves; +import org.bouncycastle.asn1.x9.X9ECParameters; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.math.BigInteger; import java.security.GeneralSecurityException; +import java.security.Key; import java.security.KeyFactory; import java.security.PublicKey; +import java.security.interfaces.ECKey; import java.security.interfaces.ECPublicKey; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; import java.util.Arrays; import java.util.HashMap; import java.util.Map; -import org.bouncycastle.asn1.nist.NISTNamedCurves; -import org.bouncycastle.asn1.x9.X9ECParameters; -import org.bouncycastle.jce.spec.ECParameterSpec; -import org.bouncycastle.jce.spec.ECPublicKeySpec; -import org.bouncycastle.math.ec.ECPoint; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.hierynomus.sshj.secg.SecgUtils; - -public class ECDSAVariationsAdapter { +class ECDSAVariationsAdapter { private final static String BASE_ALGORITHM_NAME = "ecdsa-sha2-nistp"; @@ -53,7 +54,7 @@ public class ECDSAVariationsAdapter { SUPPORTED_CURVES.put("521", "nistp521"); } - public static PublicKey readPubKeyFromBuffer(Buffer> buf, String variation) throws GeneralSecurityException { + static PublicKey readPubKeyFromBuffer(Buffer> buf, String variation) throws GeneralSecurityException { String algorithm = BASE_ALGORITHM_NAME + variation; if (!SecurityUtils.isBouncyCastleRegistered()) { throw new GeneralSecurityException("BouncyCastle is required to read a key of type " + algorithm); @@ -80,19 +81,20 @@ public class ECDSAVariationsAdapter { BigInteger bigX = new BigInteger(1, x); BigInteger bigY = new BigInteger(1, y); - X9ECParameters ecParams = NISTNamedCurves.getByName(NIST_CURVES_NAMES.get(variation)); - ECPoint pPublicPoint = ecParams.getCurve().createPoint(bigX, bigY); - ECParameterSpec spec = new ECParameterSpec(ecParams.getCurve(), ecParams.getG(), ecParams.getN()); - ECPublicKeySpec publicSpec = new ECPublicKeySpec(pPublicPoint, spec); + String name = NIST_CURVES_NAMES.get(variation); + X9ECParameters ecParams = NISTNamedCurves.getByName(name); + ECNamedCurveSpec ecCurveSpec = new ECNamedCurveSpec(name, ecParams.getCurve(), ecParams.getG(), ecParams.getN()); + ECPoint p = new ECPoint(bigX, bigY); + ECPublicKeySpec publicKeySpec = new ECPublicKeySpec(p, ecCurveSpec); KeyFactory keyFactory = KeyFactory.getInstance("ECDSA"); - return keyFactory.generatePublic(publicSpec); + return keyFactory.generatePublic(publicKeySpec); } catch (Exception ex) { throw new GeneralSecurityException(ex); } } - public static void writePubKeyContentsIntoBuffer(PublicKey pk, Buffer> buf) { + static void writePubKeyContentsIntoBuffer(PublicKey pk, Buffer> buf) { final ECPublicKey ecdsa = (ECPublicKey) pk; byte[] encoded = SecgUtils.getEncoded(ecdsa.getW(), ecdsa.getParams().getCurve()); @@ -100,8 +102,12 @@ public class ECDSAVariationsAdapter { .putBytes(encoded); } - public static int fieldSizeFromKey(ECPublicKey ecPublicKey) { - return ecPublicKey.getParams().getCurve().getField().getFieldSize(); + static boolean isECKeyWithFieldSize(Key key, int fieldSize) { + return "ECDSA".equals(key.getAlgorithm()) + && fieldSizeFromKey((ECKey) key) == fieldSize; } + private static int fieldSizeFromKey(ECKey ecPublicKey) { + return ecPublicKey.getParams().getCurve().getField().getFieldSize(); + } } diff --git a/src/main/java/net/schmizz/sshj/common/IOUtils.java b/src/main/java/net/schmizz/sshj/common/IOUtils.java index 72963ad8..8c39bf91 100644 --- a/src/main/java/net/schmizz/sshj/common/IOUtils.java +++ b/src/main/java/net/schmizz/sshj/common/IOUtils.java @@ -40,7 +40,7 @@ public class IOUtils { if (c != null) c.close(); } catch (IOException logged) { - loggerFactory.getLogger(IOUtils.class).warn("Error closing {} - {}", c, logged); + loggerFactory.getLogger(IOUtils.class).warn("Error closing {} - {}", c, logged); } } } diff --git a/src/main/java/net/schmizz/sshj/common/KeyType.java b/src/main/java/net/schmizz/sshj/common/KeyType.java index a54620f7..a86c99c4 100644 --- a/src/main/java/net/schmizz/sshj/common/KeyType.java +++ b/src/main/java/net/schmizz/sshj/common/KeyType.java @@ -15,6 +15,16 @@ */ package net.schmizz.sshj.common; +import com.hierynomus.sshj.signature.Ed25519PublicKey; +import com.hierynomus.sshj.userauth.certificate.Certificate; +import net.i2p.crypto.eddsa.EdDSAPublicKey; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; +import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; +import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; +import net.schmizz.sshj.common.Buffer.BufferException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.Key; @@ -22,30 +32,11 @@ import java.security.KeyFactory; import java.security.PublicKey; import java.security.interfaces.DSAPrivateKey; import java.security.interfaces.DSAPublicKey; -import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; import java.security.spec.DSAPublicKeySpec; import java.security.spec.RSAPublicKeySpec; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collections; -import java.util.Date; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; - -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import com.hierynomus.sshj.signature.Ed25519PublicKey; -import com.hierynomus.sshj.userauth.certificate.Certificate; - -import net.i2p.crypto.eddsa.EdDSAPublicKey; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveSpec; -import net.i2p.crypto.eddsa.spec.EdDSANamedCurveTable; -import net.i2p.crypto.eddsa.spec.EdDSAPublicKeySpec; -import net.schmizz.sshj.common.Buffer.BufferException; +import java.util.*; /** Type of key e.g. rsa, dsa */ public enum KeyType { @@ -130,7 +121,7 @@ public enum KeyType { @Override protected boolean isMyType(Key key) { - return ("ECDSA".equals(key.getAlgorithm()) && ECDSAVariationsAdapter.fieldSizeFromKey((ECPublicKey) key) == 256); + return ECDSAVariationsAdapter.isECKeyWithFieldSize(key, 256); } }, @@ -151,7 +142,7 @@ public enum KeyType { @Override protected boolean isMyType(Key key) { - return ("ECDSA".equals(key.getAlgorithm()) && ECDSAVariationsAdapter.fieldSizeFromKey((ECPublicKey) key) == 384); + return ECDSAVariationsAdapter.isECKeyWithFieldSize(key, 384); } }, @@ -172,7 +163,7 @@ public enum KeyType { @Override protected boolean isMyType(Key key) { - return ("ECDSA".equals(key.getAlgorithm()) && ECDSAVariationsAdapter.fieldSizeFromKey((ECPublicKey) key) == 521); + return ECDSAVariationsAdapter.isECKeyWithFieldSize(key, 521); } }, @@ -192,7 +183,7 @@ public enum KeyType { ); } - EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName(EdDSANamedCurveTable.CURVE_ED25519_SHA512); + EdDSANamedCurveSpec ed25519 = EdDSANamedCurveTable.getByName("Ed25519"); EdDSAPublicKeySpec publicSpec = new EdDSAPublicKeySpec(p, ed25519); return new Ed25519PublicKey(publicSpec); diff --git a/src/main/java/net/schmizz/sshj/common/SecurityUtils.java b/src/main/java/net/schmizz/sshj/common/SecurityUtils.java index a549b90c..eb4bab02 100644 --- a/src/main/java/net/schmizz/sshj/common/SecurityUtils.java +++ b/src/main/java/net/schmizz/sshj/common/SecurityUtils.java @@ -15,13 +15,24 @@ */ package net.schmizz.sshj.common; -import java.security.*; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.security.Provider; +import java.security.PublicKey; +import java.security.Security; +import java.security.Signature; + import javax.crypto.Cipher; import javax.crypto.KeyAgreement; import javax.crypto.Mac; import javax.crypto.NoSuchPaddingException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; import static java.lang.String.format; @@ -36,12 +47,17 @@ public class SecurityUtils { */ public static final String BOUNCY_CASTLE = "BC"; + /** + * Identifier for the BouncyCastle JCE provider + */ + public static final String SPONGY_CASTLE = "SC"; + /* * Security provider identifier. null = default JCE */ private static String securityProvider = null; - // relate to BC registration + // relate to BC registration (or SpongyCastle on Android) private static Boolean registerBouncyCastle; private static boolean registrationDone; @@ -68,19 +84,21 @@ public class SecurityUtils { } if (securityProvider == null) { - MessageDigest.getInstance("MD5", provider.getName()); - KeyAgreement.getInstance("DH", provider.getName()); + MessageDigest.getInstance("MD5", provider); + KeyAgreement.getInstance("DH", provider); setSecurityProvider(provider.getName()); return true; } } catch (NoSuchAlgorithmException e) { LOG.info(format("Security Provider '%s' does not support necessary algorithm", providerClassName), e); - } catch (NoSuchProviderException e) { - LOG.info("Registration of Security Provider '{}' unexpectedly failed", providerClassName); + } catch (Exception e) { + LOG.info(format("Registration of Security Provider '%s' unexpectedly failed", providerClassName), e); } return false; } + + public static synchronized Cipher getCipher(String transformation) throws NoSuchAlgorithmException, NoSuchPaddingException, NoSuchProviderException { register(); @@ -221,11 +239,11 @@ public class SecurityUtils { * Attempts registering BouncyCastle as security provider if it has not been previously attempted and returns * whether the registration succeeded. * - * @return whether BC registered + * @return whether BC (or SC on Android) registered */ public static synchronized boolean isBouncyCastleRegistered() { register(); - return BOUNCY_CASTLE.equals(securityProvider); + return BOUNCY_CASTLE.equals(securityProvider) || SPONGY_CASTLE.equals(securityProvider); } public static synchronized void setRegisterBouncyCastle(boolean registerBouncyCastle) { diff --git a/src/main/java/net/schmizz/sshj/connection/ConnectionImpl.java b/src/main/java/net/schmizz/sshj/connection/ConnectionImpl.java index 5657d3a1..cfaee366 100644 --- a/src/main/java/net/schmizz/sshj/connection/ConnectionImpl.java +++ b/src/main/java/net/schmizz/sshj/connection/ConnectionImpl.java @@ -130,6 +130,9 @@ public class ConnectionImpl getChannel(buf).handle(msg, buf); } else if (msg.in(80, 90)) { switch (msg) { + case GLOBAL_REQUEST: + gotGlobalRequest(buf); + break; case REQUEST_SUCCESS: gotGlobalReqResponse(buf); break; @@ -259,6 +262,20 @@ public class ConnectionImpl channels.clear(); } + private void gotGlobalRequest(SSHPacket buf) + throws ConnectionException, TransportException { + try { + final String requestName = buf.readString(); + boolean wantReply = buf.readBoolean(); + log.debug("Received GLOBAL_REQUEST `{}`; want reply: {}", requestName, wantReply); + if (wantReply) { + trans.write(new SSHPacket(Message.REQUEST_FAILURE)); + } + } catch (Buffer.BufferException be) { + throw new ConnectionException(be); + } + } + @Override public void setTimeoutMs(int timeoutMs) { this.timeoutMs = timeoutMs; diff --git a/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java b/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java index ff318cd7..e31d0f4e 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/AbstractChannel.java @@ -102,7 +102,8 @@ public abstract class AbstractChannel protected void init(int recipient, long remoteWinSize, long remoteMaxPacketSize) { this.recipient = recipient; - rwin = new Window.Remote(remoteWinSize, (int) Math.min(remoteMaxPacketSize, REMOTE_MAX_PACKET_SIZE_CEILING), loggerFactory); + rwin = new Window.Remote(remoteWinSize, (int) Math.min(remoteMaxPacketSize, REMOTE_MAX_PACKET_SIZE_CEILING), + conn.getTimeoutMs(), loggerFactory); out = new ChannelOutputStream(this, trans, rwin); log.debug("Initialized - {}", this); } @@ -362,10 +363,12 @@ public abstract class AbstractChannel } catch (Buffer.BufferException be) { throw new ConnectionException(be); } - if (len < 0 || len > getLocalMaxPacketSize() || len > buf.available()) + if (len < 0 || len > getLocalMaxPacketSize() || len > buf.available()) { throw new ConnectionException(DisconnectReason.PROTOCOL_ERROR, "Bad item length: " + len); - if (log.isTraceEnabled()) + } + if (log.isTraceEnabled()) { log.trace("IN #{}: {}", id, ByteArrayUtils.printHex(buf.array(), buf.rpos(), len)); + } stream.receive(buf.array(), buf.rpos(), len); } diff --git a/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java b/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java index 5c7f1e64..6e79df0f 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/ChannelInputStream.java @@ -92,36 +92,43 @@ public final class ChannelInputStream throws IOException { synchronized (buf) { for (; ; ) { - if (buf.available() > 0) + if (buf.available() > 0) { break; - if (eof) - if (error != null) + } + if (eof) { + if (error != null) { throw error; - else + } else { return -1; + } + } try { buf.wait(); } catch (InterruptedException e) { throw (IOException) new InterruptedIOException().initCause(e); } } - if (len > buf.available()) + if (len > buf.available()) { len = buf.available(); + } buf.readRawBytes(b, off, len); - if (buf.rpos() > win.getMaxPacketSize() && buf.available() == 0) + if (buf.rpos() > win.getMaxPacketSize() && buf.available() == 0) { buf.clear(); + } } - if (!chan.getAutoExpand()) + if (!chan.getAutoExpand()) { checkWindow(); + } return len; } public void receive(byte[] data, int offset, int len) throws ConnectionException, TransportException { - if (eof) + if (eof) { throw new ConnectionException("Getting data on EOF'ed stream"); + } synchronized (buf) { buf.putRawBytes(data, offset, len); buf.notifyAll(); @@ -132,8 +139,9 @@ public final class ChannelInputStream synchronized (win) { win.consume(len); } - if (chan.getAutoExpand()) + if (chan.getAutoExpand()) { checkWindow(); + } } private void checkWindow() @@ -143,7 +151,7 @@ public final class ChannelInputStream if (adjustment > 0) { log.debug("Sending SSH_MSG_CHANNEL_WINDOW_ADJUST to #{} for {} bytes", chan.getRecipient(), adjustment); trans.write(new SSHPacket(Message.CHANNEL_WINDOW_ADJUST) - .putUInt32(chan.getRecipient()).putUInt32(adjustment)); + .putUInt32(chan.getRecipient()).putUInt32(adjustment)); win.expand(adjustment); } } diff --git a/src/main/java/net/schmizz/sshj/connection/channel/SocketStreamCopyMonitor.java b/src/main/java/net/schmizz/sshj/connection/channel/SocketStreamCopyMonitor.java index 9d373279..49a69df2 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/SocketStreamCopyMonitor.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/SocketStreamCopyMonitor.java @@ -39,15 +39,21 @@ public class SocketStreamCopyMonitor new SocketStreamCopyMonitor(new Runnable() { public void run() { try { - for (Event ev = x; - !ev.tryAwait(frequency, unit); - ev = (ev == x) ? y : x) { - } + await(x); + await(y); } catch (IOException ignored) { } finally { IOUtils.closeQuietly(channel, asCloseable(socket)); } } + + private void await(final Event event) throws IOException { + while(true){ + if(event.tryAwait(frequency, unit)){ + break; + } + } + } }).start(); } diff --git a/src/main/java/net/schmizz/sshj/connection/channel/Window.java b/src/main/java/net/schmizz/sshj/connection/channel/Window.java index ca83fca6..8839f1e5 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/Window.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/Window.java @@ -20,6 +20,8 @@ import net.schmizz.sshj.common.SSHRuntimeException; import net.schmizz.sshj.connection.ConnectionException; import org.slf4j.Logger; +import java.util.concurrent.TimeUnit; + public abstract class Window { protected final Logger log; @@ -59,8 +61,9 @@ public abstract class Window { synchronized (lock) { size -= dec; log.debug("Consuming by {} down to {}", dec, size); - if (size < 0) + if (size < 0) { throw new ConnectionException("Window consumed to below 0"); + } } } @@ -72,17 +75,23 @@ public abstract class Window { /** Controls how much data we can send before an adjustment notification from remote end is required. */ public static final class Remote extends Window { + private final long timeoutMs; - public Remote(long initialWinSize, int maxPacketSize, LoggerFactory loggerFactory) { + public Remote(long initialWinSize, int maxPacketSize, long timeoutMs, LoggerFactory loggerFactory) { super(initialWinSize, maxPacketSize, loggerFactory); + this.timeoutMs = timeoutMs; } public long awaitExpansion(long was) throws ConnectionException { synchronized (lock) { + long end = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMs); while (size <= was) { log.debug("Waiting, need size to grow from {} bytes", was); try { - lock.wait(); + lock.wait(timeoutMs); + if ((size <= was) && ((System.nanoTime() - end) > 0)) { + throw new ConnectionException("Timeout when trying to expand the window size"); + } } catch (InterruptedException ie) { throw new ConnectionException(ie); } diff --git a/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java b/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java index 3407e368..3a4d4eaf 100644 --- a/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java +++ b/src/main/java/net/schmizz/sshj/connection/channel/direct/LocalPortForwarder.java @@ -137,6 +137,15 @@ public class LocalPortForwarder { listen(Thread.currentThread()); } + /** + * Returns whether this listener is running (ie. whether a thread is attached to it). + * + * @return + */ + public boolean isRunning() { + return this.runningThread != null && !serverSocket.isClosed(); + } + /** * Start listening for incoming connections and forward to remote host as a channel and ensure that the thread is registered. * This is useful if for instance {@link #close() is called from another thread} @@ -172,8 +181,8 @@ public class LocalPortForwarder { public void close() throws IOException { if (!serverSocket.isClosed()) { log.info("Closing listener on {}", serverSocket.getLocalSocketAddress()); - serverSocket.close(); runningThread.interrupt(); + serverSocket.close(); } } diff --git a/src/main/java/net/schmizz/sshj/sftp/PathComponents.java b/src/main/java/net/schmizz/sshj/sftp/PathComponents.java index 37ad0bed..6774c602 100644 --- a/src/main/java/net/schmizz/sshj/sftp/PathComponents.java +++ b/src/main/java/net/schmizz/sshj/sftp/PathComponents.java @@ -18,8 +18,14 @@ package net.schmizz.sshj.sftp; public class PathComponents { static String adjustForParent(String parent, String path, String pathSep) { - return (path.startsWith(pathSep)) ? path // Absolute path, nothing to adjust - : (parent + (parent.endsWith(pathSep) ? "" : pathSep) + path); // Relative path + if (path.startsWith(pathSep)) { + return path; // Absolute path, nothing to adjust + } else if (parent.endsWith(pathSep)) { + return parent + path; // Relative path, parent endsWith '/' + } else if (parent.isEmpty()) { + return path; + } + return parent + pathSep + path; // Relative path } static String trimTrailingSeparator(String somePath, String pathSep) { @@ -33,7 +39,8 @@ public class PathComponents { public PathComponents(String parent, String name, String pathSep) { this.parent = parent; this.name = name; - this.path = trimTrailingSeparator(adjustForParent(parent, name, pathSep), pathSep); + String adjusted = adjustForParent(parent, name, pathSep); + this.path = !pathSep.equals(adjusted) ? trimTrailingSeparator(adjusted, pathSep) : adjusted; } public String getParent() { diff --git a/src/main/java/net/schmizz/sshj/sftp/PathHelper.java b/src/main/java/net/schmizz/sshj/sftp/PathHelper.java index b40fcd11..e8a01fda 100644 --- a/src/main/java/net/schmizz/sshj/sftp/PathHelper.java +++ b/src/main/java/net/schmizz/sshj/sftp/PathHelper.java @@ -70,16 +70,25 @@ public class PathHelper { */ public PathComponents getComponents(final String path) throws IOException { - if (path.equals(pathSep)) - return getComponents("", ""); + if (path.equals(pathSep)) { + return getComponents("", "/"); + } - if (path.isEmpty() || ".".equals(path) || ("." + pathSep).equals(path)) + if (path.isEmpty() || ".".equals(path) || ("." + pathSep).equals(path)) { return getComponents(getDotDir()); + } final String withoutTrailSep = trimTrailingSeparator(path); final int lastSep = withoutTrailSep.lastIndexOf(pathSep); - final String parent = (lastSep == -1) ? "" : withoutTrailSep.substring(0, lastSep); - final String name = (lastSep == -1) ? withoutTrailSep : withoutTrailSep.substring(lastSep + pathSep.length()); + String parent; + String name; + if (lastSep == -1) { + parent = ""; + name = withoutTrailSep; + } else { + parent = lastSep == 0 ? "/" : withoutTrailSep.substring(0, lastSep); + name = withoutTrailSep.substring(lastSep + pathSep.length()); + } if (".".equals(name) || "..".equals(name)) { return getComponents(canonicalizer.canonicalize(path)); @@ -87,5 +96,4 @@ public class PathHelper { return getComponents(parent, name); } } - } \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java b/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java index 14d054e1..25463bfb 100644 --- a/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java +++ b/src/main/java/net/schmizz/sshj/sftp/RemoteFile.java @@ -82,9 +82,7 @@ public class RemoteFile throws IOException { return requester.request(newRequest(PacketType.WRITE) .putUInt64(fileOffset) - // TODO The SFTP spec claims this field is unneeded...? See #187 - .putUInt32(len) - .putRawBytes(data, off, len) + .putString(data, off, len) ); } diff --git a/src/main/java/net/schmizz/sshj/sftp/Response.java b/src/main/java/net/schmizz/sshj/sftp/Response.java index c269291a..b6c2b1a5 100644 --- a/src/main/java/net/schmizz/sshj/sftp/Response.java +++ b/src/main/java/net/schmizz/sshj/sftp/Response.java @@ -68,6 +68,9 @@ public final class Response this.code = code; } + public int getCode() { + return code; + } } private final int protocolVersion; diff --git a/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java b/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java index 9cd44a6f..3eb9ae96 100644 --- a/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java +++ b/src/main/java/net/schmizz/sshj/signature/AbstractSignature.java @@ -15,34 +15,48 @@ */ package net.schmizz.sshj.signature; +import net.schmizz.sshj.common.Buffer; import net.schmizz.sshj.common.SSHRuntimeException; import net.schmizz.sshj.common.SecurityUtils; -import java.security.GeneralSecurityException; -import java.security.PrivateKey; -import java.security.PublicKey; -import java.security.SignatureException; +import java.security.*; -/** An abstract class for {@link Signature} that implements common functionality. */ +/** + * An abstract class for {@link Signature} that implements common functionality. + */ public abstract class AbstractSignature implements Signature { - protected final String algorithm; - protected java.security.Signature signature; + @SuppressWarnings("PMD.UnnecessaryFullyQualifiedName") + protected final java.security.Signature signature; protected AbstractSignature(String algorithm) { - this.algorithm = algorithm; + try { + this.signature = SecurityUtils.getSignature(algorithm); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + } + + protected AbstractSignature(@SuppressWarnings("PMD.UnnecessaryFullyQualifiedName") + java.security.Signature signatureEngine) { + this.signature = signatureEngine; } @Override - public void init(PublicKey publicKey, PrivateKey privateKey) { + public void initVerify(PublicKey publicKey) { try { - signature = SecurityUtils.getSignature(algorithm); - if (publicKey != null) - signature.initVerify(publicKey); - if (privateKey != null) - signature.initSign(privateKey); - } catch (GeneralSecurityException e) { + signature.initVerify(publicKey); + } catch (InvalidKeyException e) { + throw new SSHRuntimeException(e); + } + } + + @Override + public void initSign(PrivateKey privateKey) { + try { + signature.initSign(privateKey); + } catch (InvalidKeyException e) { throw new SSHRuntimeException(e); } } @@ -70,23 +84,24 @@ public abstract class AbstractSignature } } - protected byte[] extractSig(byte[] sig) { - if (sig[0] == 0 && sig[1] == 0 && sig[2] == 0) { - int i = 0; - int j = sig[i++] << 24 & 0xff000000 - | sig[i++] << 16 & 0x00ff0000 - | sig[i++] << 8 & 0x0000ff00 - | sig[i++] & 0x000000ff; - i += j; - j = sig[i++] << 24 & 0xff000000 - | sig[i++] << 16 & 0x00ff0000 - | sig[i++] << 8 & 0x0000ff00 - | sig[i++] & 0x000000ff; - byte[] newSig = new byte[j]; - System.arraycopy(sig, i, newSig, 0, j); - return newSig; + /** + * Check whether the signature is generated using the expected algorithm, and if so, return the signature blob + * + * @param sig The full signature + * @param expectedKeyAlgorithm The expected key algorithm + * @return The blob part of the signature + */ + protected byte[] extractSig(byte[] sig, String expectedKeyAlgorithm) { + Buffer.PlainBuffer buffer = new Buffer.PlainBuffer(sig); + try { + String algo = buffer.readString(); + if (!expectedKeyAlgorithm.equals(algo)) { + throw new SSHRuntimeException("Expected '" + expectedKeyAlgorithm + "' key algorithm, but got: " + algo); + } + return buffer.readBytes(); + } catch (Buffer.BufferException e) { + throw new SSHRuntimeException(e); } - return sig; } -} \ No newline at end of file +} diff --git a/src/main/java/net/schmizz/sshj/signature/Signature.java b/src/main/java/net/schmizz/sshj/signature/Signature.java index 6fc143b1..6ad1ac45 100644 --- a/src/main/java/net/schmizz/sshj/signature/Signature.java +++ b/src/main/java/net/schmizz/sshj/signature/Signature.java @@ -22,13 +22,24 @@ import java.security.PublicKey; public interface Signature { /** - * Initialize this signature with the given public key and private key. If the private key is null, only signature - * verification can be performed. + * Initialize this signature with the given public key for signature verification. * - * @param pubkey (null-ok) specify in case verification is needed - * @param prvkey (null-ok) specify in case signing is needed + * Note that subsequent calls to either {@link #initVerify(PublicKey)} or {@link #initSign(PrivateKey)} will + * overwrite prior initialization. + * + * @param pubkey the public key to use for signature verification */ - void init(PublicKey pubkey, PrivateKey prvkey); + void initVerify(PublicKey pubkey); + + /** + * Initialize this signature with the given private key for signing. + * + * Note that subsequent calls to either {@link #initVerify(PublicKey)} or {@link #initSign(PrivateKey)} will + * overwrite prior initialization. + * + * @param prvkey the private key to use for signing + */ + void initSign(PrivateKey prvkey); /** * Convenience method, same as calling {@link #update(byte[], int, int)} with offset as {@code 0} and {@code diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java index 12305efc..3406e609 100644 --- a/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java +++ b/src/main/java/net/schmizz/sshj/signature/SignatureDSA.java @@ -17,14 +17,23 @@ package net.schmizz.sshj.signature; import net.schmizz.sshj.common.KeyType; import net.schmizz.sshj.common.SSHRuntimeException; +import org.bouncycastle.asn1.*; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; import java.security.SignatureException; +import java.util.Arrays; -/** DSA {@link Signature} */ +/** + * DSA {@link Signature} + */ public class SignatureDSA extends AbstractSignature { - /** A named factory for DSA signature */ + /** + * A named factory for DSA signature + */ public static class Factory implements net.schmizz.sshj.common.Factory.Named { @@ -74,33 +83,33 @@ public class SignatureDSA @Override public boolean verify(byte[] sig) { - sig = extractSig(sig); - - // ASN.1 - int frst = (sig[0] & 0x80) != 0 ? 1 : 0; - int scnd = (sig[20] & 0x80) != 0 ? 1 : 0; - - int length = sig.length + 6 + frst + scnd; - byte[] tmp = new byte[length]; - tmp[0] = (byte) 0x30; - tmp[1] = (byte) 0x2c; - tmp[1] += frst; - tmp[1] += scnd; - tmp[2] = (byte) 0x02; - tmp[3] = (byte) 0x14; - tmp[3] += frst; - System.arraycopy(sig, 0, tmp, 4 + frst, 20); - tmp[4 + tmp[3]] = (byte) 0x02; - tmp[5 + tmp[3]] = (byte) 0x14; - tmp[5 + tmp[3]] += scnd; - System.arraycopy(sig, 20, tmp, 6 + tmp[3] + scnd, 20); - sig = tmp; - try { - return signature.verify(sig); + byte[] sigBlob = extractSig(sig, "ssh-dss"); + return signature.verify(asnEncode(sigBlob)); } catch (SignatureException e) { throw new SSHRuntimeException(e); + } catch (IOException e) { + throw new SSHRuntimeException(e); } } + /** + * Encodes the signature as a DER sequence (ASN.1 format). + */ + private byte[] asnEncode(byte[] sigBlob) throws IOException { + byte[] r = new BigInteger(1, Arrays.copyOfRange(sigBlob, 0, 20)).toByteArray(); + byte[] s = new BigInteger(1, Arrays.copyOfRange(sigBlob, 20, 40)).toByteArray(); + + ASN1EncodableVector vector = new ASN1EncodableVector(); + vector.add(new ASN1Integer(r)); + vector.add(new ASN1Integer(s)); + + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + ASN1OutputStream asnOS = new ASN1OutputStream(baos); + + asnOS.writeObject(new DERSequence(vector)); + asnOS.flush(); + + return baos.toByteArray(); + } } diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java index 7125173f..b063e393 100644 --- a/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java +++ b/src/main/java/net/schmizz/sshj/signature/SignatureECDSA.java @@ -15,19 +15,18 @@ */ package net.schmizz.sshj.signature; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.math.BigInteger; -import java.security.SignatureException; - +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.common.SSHRuntimeException; import org.bouncycastle.asn1.ASN1EncodableVector; import org.bouncycastle.asn1.ASN1Integer; import org.bouncycastle.asn1.ASN1OutputStream; import org.bouncycastle.asn1.DERSequence; -import net.schmizz.sshj.common.Buffer; -import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.common.SSHRuntimeException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.security.SignatureException; /** ECDSA {@link Signature} */ public class SignatureECDSA extends AbstractSignature { @@ -99,7 +98,7 @@ public class SignatureECDSA extends AbstractSignature { System.arraycopy(sig, 4, r, 0, rLen); System.arraycopy(sig, 6 + rLen, s, 0, sLen); - Buffer buf = new Buffer.PlainBuffer(); + Buffer.PlainBuffer buf = new Buffer.PlainBuffer(); buf.putMPInt(new BigInteger(r)); buf.putMPInt(new BigInteger(s)); @@ -108,26 +107,9 @@ public class SignatureECDSA extends AbstractSignature { @Override public boolean verify(byte[] sig) { - byte[] r; - byte[] s; try { - Buffer sigbuf = new Buffer.PlainBuffer(sig); - final String algo = new String(sigbuf.readBytes()); - if (!keyTypeName.equals(algo)) { - throw new SSHRuntimeException(String.format("Signature :: " + keyTypeName + " expected, got %s", algo)); - } - final int rsLen = sigbuf.readUInt32AsInt(); - if (sigbuf.available() != rsLen) { - throw new SSHRuntimeException("Invalid key length"); - } - r = sigbuf.readBytes(); - s = sigbuf.readBytes(); - } catch (Exception e) { - throw new SSHRuntimeException(e); - } - - try { - return signature.verify(asnEncode(r, s)); + byte[] sigBlob = extractSig(sig, keyTypeName); + return signature.verify(asnEncode(sigBlob)); } catch (SignatureException e) { throw new SSHRuntimeException(e); } catch (IOException e) { @@ -135,29 +117,19 @@ public class SignatureECDSA extends AbstractSignature { } } - private byte[] asnEncode(byte[] r, byte[] s) throws IOException { - int rLen = r.length; - int sLen = s.length; - - /* - * We can't have the high bit set, so add an extra zero at the beginning - * if so. - */ - if ((r[0] & 0x80) != 0) { - rLen++; - } - if ((s[0] & 0x80) != 0) { - sLen++; - } - - /* Calculate total output length */ - int length = 6 + rLen + sLen; + /** + * Encodes the signature as a DER sequence (ASN.1 format). + */ + private byte[] asnEncode(byte[] sigBlob) throws IOException { + Buffer.PlainBuffer sigbuf = new Buffer.PlainBuffer(sigBlob); + byte[] r = sigbuf.readBytes(); + byte[] s = sigbuf.readBytes(); ASN1EncodableVector vector = new ASN1EncodableVector(); vector.add(new ASN1Integer(r)); vector.add(new ASN1Integer(s)); - ByteArrayOutputStream baos = new ByteArrayOutputStream(length); + ByteArrayOutputStream baos = new ByteArrayOutputStream(); ASN1OutputStream asnOS = new ASN1OutputStream(baos); asnOS.writeObject(new DERSequence(vector)); diff --git a/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java b/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java index 6ed5f1bb..16f33dbc 100644 --- a/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java +++ b/src/main/java/net/schmizz/sshj/signature/SignatureRSA.java @@ -51,7 +51,7 @@ public class SignatureRSA @Override public boolean verify(byte[] sig) { - sig = extractSig(sig); + sig = extractSig(sig, "ssh-rsa"); try { return signature.verify(sig); } catch (SignatureException e) { diff --git a/src/main/java/net/schmizz/sshj/transport/Converter.java b/src/main/java/net/schmizz/sshj/transport/Converter.java index df1b2ad3..0e817e2e 100644 --- a/src/main/java/net/schmizz/sshj/transport/Converter.java +++ b/src/main/java/net/schmizz/sshj/transport/Converter.java @@ -44,6 +44,7 @@ abstract class Converter { protected int cipherSize = 8; protected long seq = -1; protected boolean authed; + protected boolean etm; long getSequenceNumber() { return seq; @@ -56,6 +57,7 @@ abstract class Converter { if (compression != null) compression.init(getCompressionType()); this.cipherSize = cipher.getIVSize(); + this.etm = mac.isEtm(); } void setAuthenticated() { diff --git a/src/main/java/net/schmizz/sshj/transport/Decoder.java b/src/main/java/net/schmizz/sshj/transport/Decoder.java index 98e015b8..c3619cd5 100644 --- a/src/main/java/net/schmizz/sshj/transport/Decoder.java +++ b/src/main/java/net/schmizz/sshj/transport/Decoder.java @@ -21,7 +21,9 @@ import net.schmizz.sshj.transport.compression.Compression; import net.schmizz.sshj.transport.mac.MAC; import org.slf4j.Logger; -/** Decodes packets from the SSH binary protocol per the current algorithms. */ +/** + * Decodes packets from the SSH binary protocol per the current algorithms. + */ final class Decoder extends Converter { @@ -29,16 +31,26 @@ final class Decoder private final Logger log; - /** What we pass decoded packets to */ + /** + * What we pass decoded packets to + */ private final SSHPacketHandler packetHandler; - /** Buffer where as-yet undecoded data lives */ + /** + * Buffer where as-yet undecoded data lives + */ private final SSHPacket inputBuffer = new SSHPacket(); - /** Used in case compression is active to store the uncompressed data */ + /** + * Used in case compression is active to store the uncompressed data + */ private final SSHPacket uncompressBuffer = new SSHPacket(); - /** MAC result is stored here */ + /** + * MAC result is stored here + */ private byte[] macResult; - /** -1 if packet length not yet been decoded, else the packet length */ + /** + * -1 if packet length not yet been decoded, else the packet length + */ private int packetLength = -1; /** @@ -60,53 +72,97 @@ final class Decoder */ private int decode() throws SSHException { + + if (etm) { + return decodeEtm(); + } else { + return decodeMte(); + } + } + + /** + * Decode an Encrypt-Then-Mac packet. + */ + private int decodeEtm() throws SSHException { + int bytesNeeded; + while (true) { + if (packetLength == -1) { + assert inputBuffer.rpos() == 0 : "buffer cleared"; + bytesNeeded = 4 - inputBuffer.available(); + if (bytesNeeded <= 0) { + // In Encrypt-Then-Mac, the packetlength is sent unencrypted. + packetLength = inputBuffer.readUInt32AsInt(); + checkPacketLength(packetLength); + } else { + // Needs more data + break; + } + } else { + assert inputBuffer.rpos() == 4 : "packet length read"; + bytesNeeded = packetLength + mac.getBlockSize() - inputBuffer.available(); + if (bytesNeeded <= 0) { + seq = seq + 1 & 0xffffffffL; + checkMAC(inputBuffer.array()); + decryptBuffer(4, packetLength); + inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte()); + final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer; + if (log.isTraceEnabled()) { + log.trace("Received packet #{}: {}", seq, plain.printHex()); + } + packetHandler.handle(plain.readMessageID(), plain); // Process the decoded packet + inputBuffer.clear(); + packetLength = -1; + } else { + // Needs more data + break; + } + } + } + return bytesNeeded; + } + + /** + * Decode a Mac-Then-Encrypt packet + * @return + * @throws SSHException + */ + private int decodeMte() throws SSHException { int need; /* Decoding loop */ for (; ; ) - if (packetLength == -1) // Waiting for beginning of packet - { - + if (packetLength == -1) { // Waiting for beginning of packet assert inputBuffer.rpos() == 0 : "buffer cleared"; - need = cipherSize - inputBuffer.available(); - if (need <= 0) + if (need <= 0) { packetLength = decryptLength(); - else + } else { // Need more data break; - + } } else { - assert inputBuffer.rpos() == 4 : "packet length read"; - need = packetLength + (mac != null ? mac.getBlockSize() : 0) - inputBuffer.available(); if (need <= 0) { - - decryptPayload(inputBuffer.array()); - + decryptBuffer(cipherSize, packetLength + 4 - cipherSize); // Decrypt the rest of the payload seq = seq + 1 & 0xffffffffL; - - if (mac != null) + if (mac != null) { checkMAC(inputBuffer.array()); - + } // Exclude the padding & MAC inputBuffer.wpos(packetLength + 4 - inputBuffer.readByte()); - final SSHPacket plain = usingCompression() ? decompressed() : inputBuffer; - - if (log.isTraceEnabled()) + if (log.isTraceEnabled()) { log.trace("Received packet #{}: {}", seq, plain.printHex()); - + } packetHandler.handle(plain.readMessageID(), plain); // Process the decoded packet - inputBuffer.clear(); packetLength = -1; - - } else + } else { // Need more data break; + } } return need; @@ -118,8 +174,9 @@ final class Decoder mac.update(data, 0, packetLength + 4); // packetLength+4 = entire packet w/o mac mac.doFinal(macResult, 0); // compute // Check against the received MAC - if (!ByteArrayUtils.equals(macResult, 0, data, packetLength + 4, mac.getBlockSize())) + if (!ByteArrayUtils.equals(macResult, 0, data, packetLength + 4, mac.getBlockSize())) { throw new TransportException(DisconnectReason.MAC_ERROR, "MAC Error"); + } } private SSHPacket decompressed() @@ -131,7 +188,7 @@ final class Decoder private int decryptLength() throws TransportException { - cipher.update(inputBuffer.array(), 0, cipherSize); + decryptBuffer(0, cipherSize); final int len; // Read packet length try { @@ -140,22 +197,26 @@ final class Decoder throw new TransportException(be); } - if (isInvalidPacketLength(len)) { // Check packet length validity - log.error("Error decoding packet (invalid length) {}", inputBuffer.printHex()); - throw new TransportException(DisconnectReason.PROTOCOL_ERROR, "invalid packet length: " + len); - } + checkPacketLength(len); return len; } - private static boolean isInvalidPacketLength(int len) { - return len < 5 || len > MAX_PACKET_LEN; + private void decryptBuffer(int offset, int length) { + cipher.update(inputBuffer.array(), offset, length); } - private void decryptPayload(final byte[] data) { - cipher.update(data, cipherSize, packetLength + 4 - cipherSize); + private void checkPacketLength(int len) throws TransportException { + if (len < 5 || len > MAX_PACKET_LEN) { // Check packet length validity + log.error("Error decoding packet (invalid length) {}", inputBuffer.printHex()); + throw new TransportException(DisconnectReason.PROTOCOL_ERROR, "invalid packet length: " + len); + } } +// private void decryptPayload(final byte[] data, int offset, int length) { +// cipher.update(data, cipherSize, packetLength + 4 - cipherSize); +// } + /** * Adds {@code len} bytes from {@code b} to the decoder buffer. When a packet has been successfully decoded, hooks * in to {@link SSHPacketHandler#handle} of the {@link SSHPacketHandler} this decoder was initialized with. diff --git a/src/main/java/net/schmizz/sshj/transport/Encoder.java b/src/main/java/net/schmizz/sshj/transport/Encoder.java index c5831718..9ece2041 100644 --- a/src/main/java/net/schmizz/sshj/transport/Encoder.java +++ b/src/main/java/net/schmizz/sshj/transport/Encoder.java @@ -62,39 +62,63 @@ final class Encoder long encode(SSHPacket buffer) { encodeLock.lock(); try { - if (log.isTraceEnabled()) - log.trace("Encoding packet #{}: {}", seq, buffer.printHex()); + if (log.isTraceEnabled()) { + // Add +1 to seq as we log before actually incrementing the sequence. + log.trace("Encoding packet #{}: {}", seq + 1, buffer.printHex()); + } - if (usingCompression()) + if (usingCompression()) { compress(buffer); + } final int payloadSize = buffer.available(); + int lengthWithoutPadding; + if (etm) { + // in Encrypt-Then-Mac mode, the length field is not encrypted, so we should keep it out of the + // padding length calculation + lengthWithoutPadding = 1 + payloadSize; // padLength (1 byte) + payload + } else { + lengthWithoutPadding = 4 + 1 + payloadSize; // packetLength (4 bytes) + padLength (1 byte) + payload + } // Compute padding length - int padLen = -(payloadSize + 5) & cipherSize - 1; - if (padLen < cipherSize) + int padLen = cipherSize - (lengthWithoutPadding % cipherSize); + if (padLen < 4) { padLen += cipherSize; + } final int startOfPacket = buffer.rpos() - 5; - final int packetLen = payloadSize + 1 + padLen; + int packetLen = 1 + payloadSize + padLen; // packetLength = padLen (1 byte) + payload + padding + + if (packetLen < 16) { + padLen += cipherSize; + packetLen = 1 + payloadSize + padLen; + } + + final int endOfPadding = startOfPacket + 4 + packetLen; // Put packet header buffer.wpos(startOfPacket); buffer.putUInt32(packetLen); buffer.putByte((byte) padLen); - // Now wpos will mark end of padding - buffer.wpos(startOfPacket + 5 + payloadSize + padLen); + buffer.wpos(endOfPadding); + // Fill padding - prng.fill(buffer.array(), buffer.wpos() - padLen, padLen); + prng.fill(buffer.array(), endOfPadding - padLen, padLen); seq = seq + 1 & 0xffffffffL; - if (mac != null) - putMAC(buffer, startOfPacket, buffer.wpos()); - - cipher.update(buffer.array(), startOfPacket, 4 + packetLen); + if (etm) { + cipher.update(buffer.array(), startOfPacket + 4, packetLen); + putMAC(buffer, startOfPacket, endOfPadding); + } else { + if (mac != null) { + putMAC(buffer, startOfPacket, endOfPadding); + } + cipher.update(buffer.array(), startOfPacket, 4 + packetLen); + } buffer.rpos(startOfPacket); // Make ready-to-read return seq; diff --git a/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java b/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java index 12432063..50f5b58d 100644 --- a/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java +++ b/src/main/java/net/schmizz/sshj/transport/KeyExchanger.java @@ -197,6 +197,12 @@ final class KeyExchanger if (hkv.verify(transport.getRemoteHost(), transport.getRemotePort(), key)) return; } + log.error("Disconnecting because none of the configured Host key verifiers ({}) could verify '{}' host key with fingerprint {} for {}:{}", + hostVerifiers, + KeyType.fromKey(key), + SecurityUtils.getFingerprint(key), + transport.getRemoteHost(), + transport.getRemotePort()); throw new TransportException(DisconnectReason.HOST_KEY_NOT_VERIFIABLE, "Could not verify `" + KeyType.fromKey(key) diff --git a/src/main/java/net/schmizz/sshj/transport/Reader.java b/src/main/java/net/schmizz/sshj/transport/Reader.java index f39aa15d..2d682011 100644 --- a/src/main/java/net/schmizz/sshj/transport/Reader.java +++ b/src/main/java/net/schmizz/sshj/transport/Reader.java @@ -60,10 +60,8 @@ public final class Reader } } } catch (Exception e) { - //noinspection StatementWithEmptyBody - if (isInterrupted()) { - // We are meant to shut up and draw to a close if interrupted - } else { + // We are meant to shut up and draw to a close if interrupted + if (!isInterrupted()) { trans.die(e); } } diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java index 20cf3d1a..4bdb35de 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CBC.java @@ -15,7 +15,14 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code aes128-cbc} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code aes128-cbc} cipher + * + * @deprecated Use {@link BlockCiphers#AES128CBC()} + */ +@Deprecated public class AES128CBC extends BlockCipher { @@ -32,6 +39,11 @@ public class AES128CBC public String getName() { return "aes128-cbc"; } + + @Override + public String toString() { + return getName(); + } } public AES128CBC() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java index bc838a44..d0a8f51c 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES128CTR.java @@ -15,11 +15,18 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code aes128-ctr} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code aes128-ctr} cipher + * + * @deprecated Use {@link BlockCiphers#AES128CTR()} + */ +@Deprecated public class AES128CTR extends BlockCipher { - /** Named factory for AES128CBC Cipher */ + /** Named factory for AES128CTR Cipher */ public static class Factory implements net.schmizz.sshj.common.Factory.Named { @@ -32,6 +39,11 @@ public class AES128CTR public String getName() { return "aes128-ctr"; } + + @Override + public String toString() { + return getName(); + } } public AES128CTR() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java index 1ce379be..724775c2 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CBC.java @@ -15,7 +15,14 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code aes192-cbc} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code aes192-cbc} cipher + * + * @deprecated Use {@link BlockCiphers#AES192CBC()} + */ +@Deprecated public class AES192CBC extends BlockCipher { @@ -32,6 +39,11 @@ public class AES192CBC public String getName() { return "aes192-cbc"; } + + @Override + public String toString() { + return getName(); + } } public AES192CBC() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java index 0beea5d8..2a4aebe6 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES192CTR.java @@ -15,7 +15,14 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code aes192-ctr} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code aes192-ctr} cipher + * + * @deprecated Use {@link BlockCiphers#AES192CTR()} + */ +@Deprecated public class AES192CTR extends BlockCipher { @@ -32,6 +39,11 @@ public class AES192CTR public String getName() { return "aes192-ctr"; } + + @Override + public String toString() { + return getName(); + } } public AES192CTR() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java index 330568ad..2f33544f 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CBC.java @@ -15,7 +15,14 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code aes256-ctr} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code aes256-cbc} cipher + * + * @deprecated Use {@link BlockCiphers#AES256CBC()} + */ +@Deprecated public class AES256CBC extends BlockCipher { @@ -32,6 +39,11 @@ public class AES256CBC public String getName() { return "aes256-cbc"; } + + @Override + public String toString() { + return getName(); + } } public AES256CBC() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java index b2fa3bf0..d5788d90 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/AES256CTR.java @@ -15,11 +15,18 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code aes256-ctr} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code aes256-ctr} cipher + * + * @deprecated Use {@link BlockCiphers#AES256CTR()} + */ +@Deprecated public class AES256CTR extends BlockCipher { - /** Named factory for AES256CBC Cipher */ + /** Named factory for AES256CTR Cipher */ public static class Factory implements net.schmizz.sshj.common.Factory.Named { @@ -32,6 +39,11 @@ public class AES256CTR public String getName() { return "aes256-ctr"; } + + @Override + public String toString() { + return getName(); + } } public AES256CTR() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java index 378813c7..391953fe 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/BlowfishCBC.java @@ -15,7 +15,14 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code blowfish-ctr} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code blowfish-bcb} cipher + * + * @deprecated Use {@link BlockCiphers#BlowfishCBC()} + */ +@Deprecated public class BlowfishCBC extends BlockCipher { @@ -32,6 +39,11 @@ public class BlowfishCBC public String getName() { return "blowfish-cbc"; } + + @Override + public String toString() { + return getName(); + } } public BlowfishCBC() { diff --git a/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java b/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java index 3c4c30a6..4d3f63df 100644 --- a/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java +++ b/src/main/java/net/schmizz/sshj/transport/cipher/TripleDESCBC.java @@ -15,7 +15,14 @@ */ package net.schmizz.sshj.transport.cipher; -/** {@code 3des-cbc} cipher */ +import com.hierynomus.sshj.transport.cipher.BlockCiphers; + +/** + * {@code 3des-cbc} cipher + * + * @deprecated Use {@link BlockCiphers#TripleDESCBC()} + */ +@Deprecated public class TripleDESCBC extends BlockCipher { @@ -32,6 +39,11 @@ public class TripleDESCBC public String getName() { return "3des-cbc"; } + + @Override + public String toString() { + return getName(); + } } public TripleDESCBC() { diff --git a/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java b/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java index 8556650f..4c045e4a 100644 --- a/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java +++ b/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHG.java @@ -80,7 +80,7 @@ public abstract class AbstractDHG extends AbstractDH Signature signature = Factory.Named.Util.create(trans.getConfig().getSignatureFactories(), KeyType.fromKey(hostKey).toString()); - signature.init(hostKey, null); + signature.initVerify(hostKey); signature.update(H, 0, H.length); if (!signature.verify(sig)) throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED, diff --git a/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHGex.java b/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHGex.java index 25e496bf..5c13d430 100644 --- a/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHGex.java +++ b/src/main/java/net/schmizz/sshj/transport/kex/AbstractDHGex.java @@ -86,7 +86,7 @@ public abstract class AbstractDHGex extends AbstractDH { H = digest.digest(); Signature signature = Factory.Named.Util.create(trans.getConfig().getSignatureFactories(), KeyType.fromKey(hostKey).toString()); - signature.init(hostKey, null); + signature.initVerify(hostKey); signature.update(H, 0, H.length); if (!signature.verify(sig)) throw new TransportException(DisconnectReason.KEY_EXCHANGE_FAILED, diff --git a/src/main/java/net/schmizz/sshj/transport/kex/Curve25519DH.java b/src/main/java/net/schmizz/sshj/transport/kex/Curve25519DH.java index 17c64584..01bd2238 100644 --- a/src/main/java/net/schmizz/sshj/transport/kex/Curve25519DH.java +++ b/src/main/java/net/schmizz/sshj/transport/kex/Curve25519DH.java @@ -15,16 +15,16 @@ */ package net.schmizz.sshj.transport.kex; -import java.math.BigInteger; -import java.security.GeneralSecurityException; -import java.security.spec.AlgorithmParameterSpec; -import java.util.Arrays; +import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.transport.random.Random; import org.bouncycastle.asn1.x9.X9ECParameters; import org.bouncycastle.crypto.ec.CustomNamedCurves; import org.bouncycastle.jce.spec.ECParameterSpec; -import net.schmizz.sshj.common.Factory; -import net.schmizz.sshj.transport.random.Random; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.spec.AlgorithmParameterSpec; +import java.util.Arrays; public class Curve25519DH extends DHBase { diff --git a/src/main/java/net/schmizz/sshj/transport/kex/Curve25519SHA256.java b/src/main/java/net/schmizz/sshj/transport/kex/Curve25519SHA256.java index 61bb42f7..69fa4b24 100644 --- a/src/main/java/net/schmizz/sshj/transport/kex/Curve25519SHA256.java +++ b/src/main/java/net/schmizz/sshj/transport/kex/Curve25519SHA256.java @@ -21,7 +21,7 @@ import java.security.GeneralSecurityException; public class Curve25519SHA256 extends AbstractDHG { /** Named factory for Curve25519SHA256 key exchange */ - public static class Factory + public static class FactoryLibSsh implements net.schmizz.sshj.common.Factory.Named { @Override @@ -35,6 +35,21 @@ public class Curve25519SHA256 extends AbstractDHG { } } + /** Named factory for Curve25519SHA256 key exchange */ + public static class Factory + implements net.schmizz.sshj.common.Factory.Named { + + @Override + public KeyExchange create() { + return new Curve25519SHA256(); + } + + @Override + public String getName() { + return "curve25519-sha256"; + } + } + public Curve25519SHA256() { super(new Curve25519DH(), new SHA256()); } diff --git a/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java b/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java index fa7fa251..d4cdd705 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/BaseMAC.java @@ -30,12 +30,18 @@ public class BaseMAC private final int defbsize; private final int bsize; private final byte[] tmp; + private final boolean etm; private javax.crypto.Mac mac; public BaseMAC(String algorithm, int bsize, int defbsize) { + this(algorithm, bsize, defbsize, false); + } + + public BaseMAC(String algorithm, int bsize, int defbsize, boolean isEtm) { this.algorithm = algorithm; this.bsize = bsize; this.defbsize = defbsize; + this.etm = isEtm; tmp = new byte[defbsize]; } @@ -112,4 +118,8 @@ public class BaseMAC update(tmp, 0, 4); } + @Override + public boolean isEtm() { + return etm; + } } diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java index 1558052f..54e2b172 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD5.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.mac; -/** HMAC-MD5 MAC. */ +import com.hierynomus.sshj.transport.mac.Macs; + +/** HMAC-MD5MAC. + * + * @deprecated Use {@link Macs#HMACMD5()} + */ public class HMACMD5 extends BaseMAC { diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java index 3ad6d925..2dc0fb25 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACMD596.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.mac; -/** HMAC-MD5-96MAC*/ +import com.hierynomus.sshj.transport.mac.Macs; + +/** HMAC-MD5-96MAC+ * + * @deprecated Use {@link Macs#HMACMD596()} + */ public class HMACMD596 extends BaseMAC { diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACRIPEMD160.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACRIPEMD160.java new file mode 100644 index 00000000..7672115c --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACRIPEMD160.java @@ -0,0 +1,43 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.transport.mac; + +import com.hierynomus.sshj.transport.mac.Macs; + +/** + * @deprecated Use {@link Macs#HMACRIPEMD160()} + */ +public class HMACRIPEMD160 extends BaseMAC { + /** Named factory for the HMAC-RIPEMD160MAC*/ + public static class Factory + implements net.schmizz.sshj.common.Factory.Named{ + + @Override + public MAC create() { + return new HMACRIPEMD160(); + } + + @Override + public String getName() { + return "hmac-ripemd160"; + } + } + + + public HMACRIPEMD160() { + super("HMACRIPEMD160", 20, 20); + } +} diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java index e792b7fc..147b10ad 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA1.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.mac; -/** HMAC-SHA1 MAC*/ +import com.hierynomus.sshj.transport.mac.Macs; + +/** HMAC-SHA1MAC+ * + * @deprecated Use {@link Macs#HMACSHA1()} + */ public class HMACSHA1 extends BaseMAC { diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java index 597fc820..8147a615 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA196.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.mac; -/** HMAC-SHA1-96MAC*/ +import com.hierynomus.sshj.transport.mac.Macs; + +/** HMAC-SHA1-96MAC+ * + * @deprecated Use {@link Macs#HMACSHA196()} + */ public class HMACSHA196 extends BaseMAC { diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2256.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2256.java index f1819687..99d7d8bb 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2256.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2256.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.mac; -/** HMAC-SHA1MAC*/ +import com.hierynomus.sshj.transport.mac.Macs; + +/** HMAC-SHA1MAC+ * + * @deprecated Use {@link Macs#HMACSHA2256()} + */ public class HMACSHA2256 extends BaseMAC { diff --git a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2512.java b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2512.java index d76ecdcf..d682b79b 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2512.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/HMACSHA2512.java @@ -15,7 +15,12 @@ */ package net.schmizz.sshj.transport.mac; -/** HMAC-SHA1MAC*/ +import com.hierynomus.sshj.transport.mac.Macs; + +/** HMAC-SHA1MAC+ * + * @deprecated Use {@link Macs#HMACSHA2512()} + */ public class HMACSHA2512 extends BaseMAC { diff --git a/src/main/java/net/schmizz/sshj/transport/mac/MAC.java b/src/main/java/net/schmizz/sshj/transport/mac/MAC.java index 3699414f..2e9abec4 100644 --- a/src/main/java/net/schmizz/sshj/transport/mac/MAC.java +++ b/src/main/java/net/schmizz/sshj/transport/mac/MAC.java @@ -15,7 +15,9 @@ */ package net.schmizz.sshj.transport.mac; -/** Message Authentication Code for use in SSH. It usually wraps a javax.crypto.Mac class. */ +/** + * Message Authentication Code for use in SSH. It usually wraps a javax.crypto.Mac class. + */ public interface MAC { byte[] doFinal(); @@ -33,4 +35,40 @@ public interface MAC { void update(byte[] foo, int start, int len); void update(long foo); + + /** + * Indicates that an Encrypt-Then-Mac algorithm was selected. + *+ * This has the following implementation details. + * 1.5 transport: Protocol 2 Encrypt-then-MAC MAC algorithms + *
+ * OpenSSH supports MAC algorithms, whose names contain "-etm", that + * perform the calculations in a different order to that defined in RFC + * 4253. These variants use the so-called "encrypt then MAC" ordering, + * calculating the MAC over the packet ciphertext rather than the + * plaintext. This ordering closes a security flaw in the SSH transport + * protocol, where decryption of unauthenticated ciphertext provided a + * "decryption oracle" that could, in conjunction with cipher flaws, reveal + * session plaintext. + *
+ * Specifically, the "-etm" MAC algorithms modify the transport protocol + * to calculate the MAC over the packet ciphertext and to send the packet + * length unencrypted. This is necessary for the transport to obtain the + * length of the packet and location of the MAC tag so that it may be + * verified without decrypting unauthenticated data. + *
+ * As such, the MAC covers: + *
+ * mac = MAC(key, sequence_number || packet_length || encrypted_packet) + *
+ * where "packet_length" is encoded as a uint32 and "encrypted_packet" + * contains: + *
+ * byte padding_length + * byte[n1] payload; n1 = packet_length - padding_length - 1 + * byte[n2] random padding; n2 = padding_length + * + * @return Whether the MAC algorithm is an Encrypt-Then-Mac algorithm + */ + boolean isEtm(); } diff --git a/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java b/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java index a85710e0..cb913623 100644 --- a/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java +++ b/src/main/java/net/schmizz/sshj/transport/random/JCERandom.java @@ -15,10 +15,11 @@ */ package net.schmizz.sshj.transport.random; -import java.security.SecureRandom; import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import java.security.SecureRandom; + /** A {@link Random} implementation using the built-in {@link SecureRandom} PRNG. */ public class JCERandom implements Random { diff --git a/src/main/java/net/schmizz/sshj/transport/verification/ConsoleKnownHostsVerifier.java b/src/main/java/net/schmizz/sshj/transport/verification/ConsoleKnownHostsVerifier.java index f8aad9e7..4fbe3b2f 100644 --- a/src/main/java/net/schmizz/sshj/transport/verification/ConsoleKnownHostsVerifier.java +++ b/src/main/java/net/schmizz/sshj/transport/verification/ConsoleKnownHostsVerifier.java @@ -41,7 +41,7 @@ public class ConsoleKnownHostsVerifier protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { final KeyType type = KeyType.fromKey(key); console.printf("The authenticity of host '%s' can't be established.\n" + - "%s key fingerprint is %s.\n", hostname, type, SecurityUtils.getFingerprint(key)); + "%s key fingerprint is %s.\n", hostname, type, SecurityUtils.getFingerprint(key)); String response = console.readLine("Are you sure you want to continue connecting (yes/no)? "); while (!(response.equalsIgnoreCase(YES) || response.equalsIgnoreCase(NO))) { response = console.readLine("Please explicitly enter yes/no: "); @@ -60,7 +60,7 @@ public class ConsoleKnownHostsVerifier } @Override - protected boolean hostKeyChangedAction(KnownHostEntry entry, String hostname, PublicKey key) { + protected boolean hostKeyChangedAction(String hostname, PublicKey key) { final KeyType type = KeyType.fromKey(key); final String fp = SecurityUtils.getFingerprint(key); final String path = getFile().getAbsolutePath(); diff --git a/src/main/java/net/schmizz/sshj/transport/verification/FingerprintVerifier.java b/src/main/java/net/schmizz/sshj/transport/verification/FingerprintVerifier.java new file mode 100644 index 00000000..58656bf5 --- /dev/null +++ b/src/main/java/net/schmizz/sshj/transport/verification/FingerprintVerifier.java @@ -0,0 +1,127 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.transport.verification; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.MessageDigest; +import java.security.PublicKey; +import java.util.Arrays; +import java.util.regex.Pattern; + +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.Buffer; +import net.schmizz.sshj.common.SSHRuntimeException; +import net.schmizz.sshj.common.SecurityUtils; + +public class FingerprintVerifier implements HostKeyVerifier { + private static final Pattern MD5_FINGERPRINT_PATTERN = Pattern.compile("[0-9a-f]{2}+(:[0-9a-f]{2}+){15}+"); + /** + * Valid examples: + * + *
+ *
+ * + * + * @param fingerprint of an SSH fingerprint in MD5 (hex), SHA-1 (base64) or SHA-256(base64) format + * + * @return + */ + public static HostKeyVerifier getInstance(String fingerprint) { + + try { + if (fingerprint.startsWith("SHA1:")) { + return new FingerprintVerifier("SHA-1", fingerprint.substring(5)); + } + + if (fingerprint.startsWith("SHA256:")) { + return new FingerprintVerifier("SHA-256", fingerprint.substring(7)); + } + + final String md5; + if (fingerprint.startsWith("MD5:")) { + md5 = fingerprint.substring(4); // remove the MD5: prefix + } else { + md5 = fingerprint; + } + + if (!MD5_FINGERPRINT_PATTERN.matcher(md5).matches()) { + throw new SSHRuntimeException("Invalid MD5 fingerprint: " + fingerprint); + } + + // Use the old default fingerprint verifier for md5 fingerprints + return (new HostKeyVerifier() { + @Override + public boolean verify(String h, int p, PublicKey k) { + return SecurityUtils.getFingerprint(k).equals(md5); + } + }); + } catch (SSHRuntimeException e) { + throw e; + } catch (IOException e) { + throw new SSHRuntimeException(e); + } + + } + + private final String digestAlgorithm; + private final byte[] fingerprintData; + + /** + * + * @param digestAlgorithm + * the used digest algorithm + * @param base64Fingerprint + * base64 encoded fingerprint data + * + * @throws IOException + */ + private FingerprintVerifier(String digestAlgorithm, String base64Fingerprint) throws IOException { + this.digestAlgorithm = digestAlgorithm; + + // if the length is not padded with "=" chars at the end so that it is divisible by 4 the SSHJ Base64 implementation does not work correctly + StringBuilder base64FingerprintBuilder = new StringBuilder(base64Fingerprint); + while (base64FingerprintBuilder.length() % 4 != 0) { + base64FingerprintBuilder.append("="); + } + fingerprintData = Base64.decode(base64FingerprintBuilder.toString()); + } + + @Override + public boolean verify(String hostname, int port, PublicKey key) { + MessageDigest digest; + try { + digest = SecurityUtils.getMessageDigest(digestAlgorithm); + } catch (GeneralSecurityException e) { + throw new SSHRuntimeException(e); + } + digest.update(new Buffer.PlainBuffer().putPublicKey(key).getCompactData()); + + byte[] digestData = digest.digest(); + return Arrays.equals(fingerprintData, digestData); + } + + @Override + public String toString() { + return "FingerprintVerifier{digestAlgorithm='" + digestAlgorithm + "'}"; + } +} \ No newline at end of file diff --git a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java index 1c035b98..3fbd1401 100644 --- a/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java +++ b/src/main/java/net/schmizz/sshj/transport/verification/OpenSSHKnownHosts.java @@ -87,14 +87,23 @@ public class OpenSSHKnownHosts final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname; + boolean foundApplicableHostEntry = false; for (KnownHostEntry e : entries) { try { - if (e.appliesTo(type, adjustedHostname)) - return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key); + if (e.appliesTo(type, adjustedHostname)) { + foundApplicableHostEntry = true; + if (e.verify(key)) { + return true; + } + } } catch (IOException ioe) { log.error("Error with {}: {}", e, ioe); return false; } + + } + if (foundApplicableHostEntry) { + return hostKeyChangedAction(adjustedHostname, key); } return hostKeyUnverifiableAction(adjustedHostname, key); @@ -104,7 +113,7 @@ public class OpenSSHKnownHosts return false; } - protected boolean hostKeyChangedAction(KnownHostEntry entry, String hostname, PublicKey key) { + protected boolean hostKeyChangedAction(String hostname, PublicKey key) { log.warn("Host key for `{}` has changed!", hostname); return false; } @@ -199,7 +208,7 @@ public class OpenSSHKnownHosts } if(split.length < 3) { log.error("Error reading entry `{}`", line); - return null; + return new BadHostEntry(line); } final String hostnames = split[i++]; final String sType = split[i++]; @@ -209,7 +218,13 @@ public class OpenSSHKnownHosts if (type != KeyType.UNKNOWN) { final String sKey = split[i++]; - key = new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey(); + try { + byte[] keyBytes = Base64.decode(sKey); + key = new Buffer.PlainBuffer(keyBytes).readPublicKey(); + } catch (IOException ioe) { + log.warn("Error decoding Base64 key bytes", ioe); + return new BadHostEntry(line); + } } else if (isBits(sType)) { type = KeyType.RSA; // int bits = Integer.valueOf(sType); @@ -220,11 +235,11 @@ public class OpenSSHKnownHosts key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e)); } catch (Exception ex) { log.error("Error reading entry `{}`, could not create key", line, ex); - return null; + return new BadHostEntry(line); } } else { log.error("Error reading entry `{}`, could not determine type", line); - return null; + return new BadHostEntry(line); } return new HostEntry(marker, hostnames, type, key); @@ -310,7 +325,7 @@ public class OpenSSHKnownHosts protected final PublicKey key; private final KnownHostMatchers.HostMatcher matcher; - HostEntry(Marker marker, String hostPart, KeyType type, PublicKey key) throws SSHException { + public HostEntry(Marker marker, String hostPart, KeyType type, PublicKey key) throws SSHException { this.marker = marker; this.hostPart = hostPart; this.type = type; @@ -340,7 +355,7 @@ public class OpenSSHKnownHosts @Override public boolean verify(PublicKey key) throws IOException { - return key.equals(this.key) && marker != Marker.REVOKED; + return getKeyString(key).equals(getKeyString(this.key)) && marker != Marker.REVOKED; } public String getLine() { @@ -350,17 +365,55 @@ public class OpenSSHKnownHosts line.append(getHostPart()); line.append(" ").append(type.toString()); - line.append(" ").append(getKeyString()); + line.append(" ").append(getKeyString(key)); return line.toString(); } - private String getKeyString() { - final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key); + private String getKeyString(PublicKey pk) { + final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(pk); return Base64.encodeBytes(buf.array(), buf.rpos(), buf.available()); } protected String getHostPart() { - return hostPart; + return hostPart; + } + } + + public static class BadHostEntry implements KnownHostEntry { + private String line; + + public BadHostEntry(String line) { + this.line = line; + } + + @Override + public KeyType getType() { + return KeyType.UNKNOWN; + } + + @Override + public String getFingerprint() { + return null; + } + + @Override + public boolean appliesTo(String host) throws IOException { + return false; + } + + @Override + public boolean appliesTo(KeyType type, String host) throws IOException { + return false; + } + + @Override + public boolean verify(PublicKey key) throws IOException { + return false; + } + + @Override + public String getLine() { + return line; } } @@ -387,4 +440,10 @@ public class OpenSSHKnownHosts return null; } } + + @Override + public String toString() { + return "OpenSSHKnownHosts{khFile='" + khFile + "'}"; + } + } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/BaseFileKeyProvider.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/BaseFileKeyProvider.java index d209e057..f4e7580e 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/BaseFileKeyProvider.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/BaseFileKeyProvider.java @@ -15,6 +15,9 @@ */ package net.schmizz.sshj.userauth.keyprovider; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.userauth.password.*; + import java.io.File; import java.io.IOException; import java.io.Reader; @@ -22,9 +25,6 @@ import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; -import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.userauth.password.*; - public abstract class BaseFileKeyProvider implements FileKeyProvider { protected Resource> resource; protected PasswordFinder pwdf; diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java index eb5246d4..dd94e73c 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/KeyProviderUtil.java @@ -15,10 +15,10 @@ */ package net.schmizz.sshj.userauth.keyprovider; +import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile; import net.schmizz.sshj.common.IOUtils; import java.io.*; -import com.hierynomus.sshj.userauth.keyprovider.OpenSSHKeyV1KeyFile; public class KeyProviderUtil { @@ -89,7 +89,7 @@ public class KeyProviderUtil { private static KeyFormat keyFormatFromHeader(String header, boolean separatePubKey) { if (header.startsWith("-----BEGIN") && header.endsWith("PRIVATE KEY-----")) { - if (separatePubKey && header.contains(OpenSSHKeyV1KeyFile.OPENSSH_PRIVATE_KEY)) { + if (header.contains(OpenSSHKeyV1KeyFile.OPENSSH_PRIVATE_KEY)) { return KeyFormat.OpenSSHv1; } else if (separatePubKey) { // Can delay asking for password since have unencrypted pubkey diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java index 39a9ec0c..da379e63 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/OpenSSHKeyFile.java @@ -75,7 +75,12 @@ public class OpenSSHKeyFile @Override public void init(String privateKey, String publicKey) { if (publicKey != null) { - initPubKey(new StringReader(publicKey)); + try { + initPubKey(new StringReader(publicKey)); + } catch (IOException e) { + // let super provide both public & private key + log.warn("Error reading public key: {}", e.toString()); + } } super.init(privateKey, null); } @@ -85,23 +90,18 @@ public class OpenSSHKeyFile * * @param publicKey Public key accessible through a {@code Reader} */ - private void initPubKey(Reader publicKey) { + private void initPubKey(Reader publicKey) throws IOException { + final BufferedReader br = new BufferedReader(publicKey); try { - final BufferedReader br = new BufferedReader(publicKey); - try { - final String keydata = br.readLine(); - if (keydata != null) { - String[] parts = keydata.trim().split(" "); - assert parts.length >= 2; - type = KeyType.fromString(parts[0]); - pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey(); - } - } finally { - br.close(); + final String keydata = br.readLine(); + if (keydata != null) { + String[] parts = keydata.trim().split(" "); + assert parts.length >= 2; + type = KeyType.fromString(parts[0]); + pubKey = new Buffer.PlainBuffer(Base64.decode(parts[1])).readPublicKey(); } - } catch (IOException e) { - // let super provide both public & private key - log.warn("Error reading public key: {}", e.toString()); + } finally { + br.close(); } } } diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java index 66a28cd8..8f4068d4 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS5KeyFile.java @@ -15,6 +15,15 @@ */ package net.schmizz.sshj.userauth.keyprovider; +import com.hierynomus.sshj.transport.cipher.BlockCiphers; +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.ByteArrayUtils; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.transport.cipher.*; +import net.schmizz.sshj.transport.digest.Digest; +import net.schmizz.sshj.transport.digest.MD5; + import java.io.BufferedReader; import java.io.EOFException; import java.io.IOException; @@ -24,14 +33,6 @@ import java.nio.CharBuffer; import java.security.*; import java.security.spec.*; import java.util.Arrays; -import javax.xml.bind.DatatypeConverter; - -import net.schmizz.sshj.common.Base64; -import net.schmizz.sshj.common.IOUtils; -import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.transport.cipher.*; -import net.schmizz.sshj.transport.digest.Digest; -import net.schmizz.sshj.transport.digest.MD5; /** * Represents a PKCS5-encoded key file. This is the format typically used by OpenSSH, OpenSSL, Amazon, etc. @@ -116,17 +117,17 @@ public class PKCS5KeyFile extends BaseFileKeyProvider { } else { String algorithm = line.substring(10, ptr); if ("DES-EDE3-CBC".equals(algorithm)) { - cipher = new TripleDESCBC(); + cipher = BlockCiphers.TripleDESCBC().create(); } else if ("AES-128-CBC".equals(algorithm)) { - cipher = new AES128CBC(); + cipher = BlockCiphers.AES128CBC().create(); } else if ("AES-192-CBC".equals(algorithm)) { - cipher = new AES192CBC(); + cipher = BlockCiphers.AES192CBC().create(); } else if ("AES-256-CBC".equals(algorithm)) { - cipher = new AES256CBC(); + cipher = BlockCiphers.AES256CBC().create(); } else { throw new FormatException("Not a supported algorithm: " + algorithm); } - iv = Arrays.copyOfRange(DatatypeConverter.parseHexBinary(line.substring(ptr + 1)), 0, cipher.getIVSize()); + iv = Arrays.copyOfRange(ByteArrayUtils.parseHex(line.substring(ptr + 1)), 0, cipher.getIVSize()); } } else if (line.length() > 0) { sb.append(line); diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java index 26f50058..d7b42af3 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PKCS8KeyFile.java @@ -15,8 +15,9 @@ */ package net.schmizz.sshj.userauth.keyprovider; -import java.io.IOException; -import java.security.KeyPair; +import net.schmizz.sshj.common.IOUtils; +import net.schmizz.sshj.common.SecurityUtils; +import net.schmizz.sshj.userauth.password.PasswordUtils; import org.bouncycastle.openssl.EncryptionException; import org.bouncycastle.openssl.PEMEncryptedKeyPair; import org.bouncycastle.openssl.PEMKeyPair; @@ -26,8 +27,8 @@ import org.bouncycastle.openssl.jcajce.JcePEMDecryptorProviderBuilder; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import net.schmizz.sshj.common.IOUtils; -import net.schmizz.sshj.userauth.password.PasswordUtils; +import java.io.IOException; +import java.security.KeyPair; /** Represents a PKCS8-encoded key file. This is the format used by (old-style) OpenSSH and OpenSSL. */ public class PKCS8KeyFile extends BaseFileKeyProvider { @@ -62,12 +63,12 @@ public class PKCS8KeyFile extends BaseFileKeyProvider { final Object o = r.readObject(); final JcaPEMKeyConverter pemConverter = new JcaPEMKeyConverter(); - pemConverter.setProvider("BC"); + pemConverter.setProvider(SecurityUtils.getSecurityProvider()); if (o instanceof PEMEncryptedKeyPair) { final PEMEncryptedKeyPair encryptedKeyPair = (PEMEncryptedKeyPair) o; JcePEMDecryptorProviderBuilder decryptorBuilder = new JcePEMDecryptorProviderBuilder(); - decryptorBuilder.setProvider("BC"); + decryptorBuilder.setProvider(SecurityUtils.getSecurityProvider()); try { passphrase = pwdf == null ? null : pwdf.reqPassword(resource); kp = pemConverter.getKeyPair(encryptedKeyPair.decryptKeyPair(decryptorBuilder.build(passphrase))); diff --git a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java index 54d9f08b..5a8ec681 100644 --- a/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java +++ b/src/main/java/net/schmizz/sshj/userauth/keyprovider/PuTTYKeyFile.java @@ -15,21 +15,21 @@ */ package net.schmizz.sshj.userauth.keyprovider; +import net.schmizz.sshj.common.Base64; +import net.schmizz.sshj.common.KeyType; +import net.schmizz.sshj.userauth.password.PasswordUtils; +import org.bouncycastle.util.encoders.Hex; + +import javax.crypto.Cipher; +import javax.crypto.Mac; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; import java.io.*; import java.math.BigInteger; import java.security.*; import java.security.spec.*; import java.util.HashMap; import java.util.Map; -import javax.crypto.Cipher; -import javax.crypto.Mac; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.SecretKeySpec; -import org.bouncycastle.util.encoders.Hex; - -import net.schmizz.sshj.common.Base64; -import net.schmizz.sshj.common.KeyType; -import net.schmizz.sshj.userauth.password.PasswordUtils; /** *- + *
4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21- + *
MD5:4b:69:6c:72:6f:79:20:77:61:73:20:68:65:72:65:21- + *
SHA1:FghNYu1l/HyE/qWbdQ2mkxrd0rU- + *
SHA1:FghNYu1l/HyE/qWbdQ2mkxrd0rU=- + *
SHA256:l/SjyCoKP8jAx3d8k8MWH+UZG0gcuIR7TQRE/A3faQo- + *
SHA256:l/SjyCoKP8jAx3d8k8MWH+UZG0gcuIR7TQRE/A3faQo=Sample PuTTY file format
diff --git a/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java b/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java index 158e99d4..75ea5431 100644 --- a/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java +++ b/src/main/java/net/schmizz/sshj/userauth/method/KeyedAuthMethod.java @@ -66,7 +66,7 @@ public abstract class KeyedAuthMethod if (signature == null) throw new UserAuthException("Could not create signature instance for " + kt + " key"); - signature.init(null, key); + signature.initSign(key); signature.update(new Buffer.PlainBuffer() .putString(params.getTransport().getSessionID()) .putBuffer(reqBuf) // & rest of the data for sig diff --git a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java index e0af491b..601a6870 100644 --- a/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java +++ b/src/main/java/net/schmizz/sshj/userauth/password/PrivateKeyFileResource.java @@ -27,6 +27,6 @@ public class PrivateKeyFileResource @Override public Reader getReader() throws IOException { - return new InputStreamReader(new FileInputStream(getDetail())); + return new InputStreamReader(new FileInputStream(getDetail()), "UTF-8"); } } diff --git a/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java b/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java index 3a59406c..bf71fd1a 100644 --- a/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java +++ b/src/main/java/net/schmizz/sshj/xfer/AbstractFileTransfer.java @@ -16,7 +16,6 @@ package net.schmizz.sshj.xfer; import net.schmizz.sshj.common.LoggerFactory; - import org.slf4j.Logger; public abstract class AbstractFileTransfer { diff --git a/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java b/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java index 642fc11f..b759f9cb 100644 --- a/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java +++ b/src/main/java/net/schmizz/sshj/xfer/FileSystemFile.java @@ -21,6 +21,7 @@ import org.slf4j.LoggerFactory; import java.io.*; import java.util.ArrayList; import java.util.List; +import java.util.Stack; public class FileSystemFile implements LocalSourceFile, LocalDestFile { @@ -83,8 +84,9 @@ public class FileSystemFile } }); - if (childFiles == null) + if (childFiles == null) { throw new IOException("Error listing files in directory: " + this); + } final Listchildren = new ArrayList (); for (File f : childFiles) { @@ -113,12 +115,13 @@ public class FileSystemFile @Override public int getPermissions() throws IOException { - if (isDirectory()) + if (isDirectory()) { return 0755; - else if (isFile()) + } else if (isFile()) { return 0644; - else + } else { throw new IOException("Unsupported file type"); + } } @Override @@ -130,8 +133,9 @@ public class FileSystemFile @Override public void setLastModifiedTime(long t) throws IOException { - if (!file.setLastModified(t * 1000)) + if (!file.setLastModified(t * 1000)) { log.warn("Could not set last modified time for {} to {}", file, t); + } } @Override @@ -143,22 +147,41 @@ public class FileSystemFile !(FilePermission.OTH_W.isIn(perms) || FilePermission.GRP_W.isIn(perms))); final boolean x = file.setExecutable(FilePermission.USR_X.isIn(perms), !(FilePermission.OTH_X.isIn(perms) || FilePermission.GRP_X.isIn(perms))); - if (!(r && w && x)) + if (!(r && w && x)) { log.warn("Could not set permissions for {} to {}", file, Integer.toString(perms, 16)); + } } @Override public FileSystemFile getChild(String name) { + validateIsChildPath(name); return new FileSystemFile(new File(file, name)); } + private void validateIsChildPath(String name) { + String[] split = name.split("/"); + Stack s = new Stack (); + for (String component : split) { + if (component == null || component.isEmpty() || ".".equals(component)) { + continue; + } else if ("..".equals(component) && !s.isEmpty()) { + s.pop(); + continue; + } else if ("..".equals(component)) { + throw new IllegalArgumentException("Cannot traverse higher than " + file + " to get child " + name); + } + s.push(component); + } + } + @Override public FileSystemFile getTargetFile(String filename) throws IOException { FileSystemFile f = this; - if (f.isDirectory()) + if (f.isDirectory()) { f = f.getChild(filename); + } if (!f.getFile().exists()) { if (!f.getFile().createNewFile()) @@ -174,12 +197,15 @@ public class FileSystemFile throws IOException { FileSystemFile f = this; - if (f.getFile().exists()) + if (f.getFile().exists()) { if (f.isDirectory()) { - if (!f.getName().equals(dirname)) + if (!f.getName().equals(dirname)) { f = f.getChild(dirname); - } else + } + } else { throw new IOException(f + " - already exists as a file; directory required"); + } + } if (!f.getFile().exists() && !f.getFile().mkdir()) throw new IOException("Failed to create directory: " + f); diff --git a/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java b/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java index fd25144d..f9779497 100644 --- a/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java +++ b/src/main/java/net/schmizz/sshj/xfer/scp/SCPDownloadClient.java @@ -75,9 +75,9 @@ public class SCPDownloadClient extends AbstractSCPClient { engine.signal("Start status OK"); String msg = engine.readMessage(); - do + do { process(engine.getTransferListener(), null, msg, targetFile); - while (!(msg = engine.readMessage()).isEmpty()); + } while (!(msg = engine.readMessage()).isEmpty()); } private long parseLong(String longString, String valType) @@ -93,15 +93,17 @@ public class SCPDownloadClient extends AbstractSCPClient { private int parsePermissions(String cmd) throws SCPException { - if (cmd.length() != 5) + if (cmd.length() != 5) { throw new SCPException("Could not parse permissions from `" + cmd + "`"); + } return Integer.parseInt(cmd.substring(1), 8); } private boolean process(TransferListener listener, String bufferedTMsg, String msg, LocalDestFile f) throws IOException { - if (msg.length() < 1) + if (msg.length() < 1) { throw new SCPException("Could not parse message `" + msg + "`"); + } switch (msg.charAt(0)) { @@ -139,8 +141,9 @@ public class SCPDownloadClient extends AbstractSCPClient { final List dMsgParts = tokenize(dMsg, 3, true); // D 0 final long length = parseLong(dMsgParts.get(1), "dir length"); final String dirname = dMsgParts.get(2); - if (length != 0) + if (length != 0) { throw new IOException("Remote SCP command sent strange directory length: " + length); + } final TransferListener dirListener = listener.directory(dirname); { @@ -186,9 +189,9 @@ public class SCPDownloadClient extends AbstractSCPClient { private static List tokenize(String msg, int totalParts, boolean consolidateTail) throws IOException { List parts = Arrays.asList(msg.split(" ")); - if (parts.size() < totalParts || - (!consolidateTail && parts.size() != totalParts)) + if (parts.size() < totalParts || (!consolidateTail && parts.size() != totalParts)) { throw new IOException("Could not parse message received from remote SCP: " + msg); + } if (consolidateTail && totalParts < parts.size()) { final StringBuilder sb = new StringBuilder(parts.get(totalParts - 1)); diff --git a/src/main/java/org/mindrot/jbcrypt/BCrypt.java b/src/main/java/org/mindrot/jbcrypt/BCrypt.java new file mode 100644 index 00000000..117049d8 --- /dev/null +++ b/src/main/java/org/mindrot/jbcrypt/BCrypt.java @@ -0,0 +1,869 @@ +// Copyright (c) 2006 Damien Miller +// +// Permission to use, copy, modify, and distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +// WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +// MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +// ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +// WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +// ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +// OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +package org.mindrot.jbcrypt; + +import java.io.UnsupportedEncodingException; +import java.security.DigestException; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; + +/** + * BCrypt implements OpenBSD-style Blowfish password hashing using + * the scheme described in "A Future-Adaptable Password Scheme" by + * Niels Provos and David Mazieres. + * + * This password hashing system tries to thwart off-line password + * cracking using a computationally-intensive hashing algorithm, + * based on Bruce Schneier's Blowfish cipher. The work factor of + * the algorithm is parameterised, so it can be increased as + * computers get faster. + *
+ * Usage is really simple. To hash a password for the first time, + * call the hashpw method with a random salt, like this: + *
+ *
+ * String pw_hash = BCrypt.hashpw(plain_password, BCrypt.gensalt());+ *
+ *+ * To check whether a plaintext password matches one that has been + * hashed previously, use the checkpw method: + *
+ *
+ * if (BCrypt.checkpw(candidate_password, stored_hash))+ *
+ * System.out.println("It matches");
+ * else
+ * System.out.println("It does not match");
+ *+ * The gensalt() method takes an optional parameter (log_rounds) + * that determines the computational complexity of the hashing: + *
+ *
+ * String strong_salt = BCrypt.gensalt(10)+ *
+ * String stronger_salt = BCrypt.gensalt(12)
+ *+ * The amount of work increases exponentially (2**log_rounds), so + * each increment is twice as much work. The default log_rounds is + * 10, and the valid range is 4 to 30. + * + * @author Damien Miller + * @version 0.2 + */ +public class BCrypt { + // BCrypt parameters + private static final int GENSALT_DEFAULT_LOG2_ROUNDS = 10; + private static final int BCRYPT_SALT_LEN = 16; + + // Blowfish parameters + private static final int BLOWFISH_NUM_ROUNDS = 16; + + // Initial contents of key schedule + private static final int P_orig[] = { + 0x243f6a88, 0x85a308d3, 0x13198a2e, 0x03707344, + 0xa4093822, 0x299f31d0, 0x082efa98, 0xec4e6c89, + 0x452821e6, 0x38d01377, 0xbe5466cf, 0x34e90c6c, + 0xc0ac29b7, 0xc97c50dd, 0x3f84d5b5, 0xb5470917, + 0x9216d5d9, 0x8979fb1b + }; + private static final int S_orig[] = { + 0xd1310ba6, 0x98dfb5ac, 0x2ffd72db, 0xd01adfb7, + 0xb8e1afed, 0x6a267e96, 0xba7c9045, 0xf12c7f99, + 0x24a19947, 0xb3916cf7, 0x0801f2e2, 0x858efc16, + 0x636920d8, 0x71574e69, 0xa458fea3, 0xf4933d7e, + 0x0d95748f, 0x728eb658, 0x718bcd58, 0x82154aee, + 0x7b54a41d, 0xc25a59b5, 0x9c30d539, 0x2af26013, + 0xc5d1b023, 0x286085f0, 0xca417918, 0xb8db38ef, + 0x8e79dcb0, 0x603a180e, 0x6c9e0e8b, 0xb01e8a3e, + 0xd71577c1, 0xbd314b27, 0x78af2fda, 0x55605c60, + 0xe65525f3, 0xaa55ab94, 0x57489862, 0x63e81440, + 0x55ca396a, 0x2aab10b6, 0xb4cc5c34, 0x1141e8ce, + 0xa15486af, 0x7c72e993, 0xb3ee1411, 0x636fbc2a, + 0x2ba9c55d, 0x741831f6, 0xce5c3e16, 0x9b87931e, + 0xafd6ba33, 0x6c24cf5c, 0x7a325381, 0x28958677, + 0x3b8f4898, 0x6b4bb9af, 0xc4bfe81b, 0x66282193, + 0x61d809cc, 0xfb21a991, 0x487cac60, 0x5dec8032, + 0xef845d5d, 0xe98575b1, 0xdc262302, 0xeb651b88, + 0x23893e81, 0xd396acc5, 0x0f6d6ff3, 0x83f44239, + 0x2e0b4482, 0xa4842004, 0x69c8f04a, 0x9e1f9b5e, + 0x21c66842, 0xf6e96c9a, 0x670c9c61, 0xabd388f0, + 0x6a51a0d2, 0xd8542f68, 0x960fa728, 0xab5133a3, + 0x6eef0b6c, 0x137a3be4, 0xba3bf050, 0x7efb2a98, + 0xa1f1651d, 0x39af0176, 0x66ca593e, 0x82430e88, + 0x8cee8619, 0x456f9fb4, 0x7d84a5c3, 0x3b8b5ebe, + 0xe06f75d8, 0x85c12073, 0x401a449f, 0x56c16aa6, + 0x4ed3aa62, 0x363f7706, 0x1bfedf72, 0x429b023d, + 0x37d0d724, 0xd00a1248, 0xdb0fead3, 0x49f1c09b, + 0x075372c9, 0x80991b7b, 0x25d479d8, 0xf6e8def7, + 0xe3fe501a, 0xb6794c3b, 0x976ce0bd, 0x04c006ba, + 0xc1a94fb6, 0x409f60c4, 0x5e5c9ec2, 0x196a2463, + 0x68fb6faf, 0x3e6c53b5, 0x1339b2eb, 0x3b52ec6f, + 0x6dfc511f, 0x9b30952c, 0xcc814544, 0xaf5ebd09, + 0xbee3d004, 0xde334afd, 0x660f2807, 0x192e4bb3, + 0xc0cba857, 0x45c8740f, 0xd20b5f39, 0xb9d3fbdb, + 0x5579c0bd, 0x1a60320a, 0xd6a100c6, 0x402c7279, + 0x679f25fe, 0xfb1fa3cc, 0x8ea5e9f8, 0xdb3222f8, + 0x3c7516df, 0xfd616b15, 0x2f501ec8, 0xad0552ab, + 0x323db5fa, 0xfd238760, 0x53317b48, 0x3e00df82, + 0x9e5c57bb, 0xca6f8ca0, 0x1a87562e, 0xdf1769db, + 0xd542a8f6, 0x287effc3, 0xac6732c6, 0x8c4f5573, + 0x695b27b0, 0xbbca58c8, 0xe1ffa35d, 0xb8f011a0, + 0x10fa3d98, 0xfd2183b8, 0x4afcb56c, 0x2dd1d35b, + 0x9a53e479, 0xb6f84565, 0xd28e49bc, 0x4bfb9790, + 0xe1ddf2da, 0xa4cb7e33, 0x62fb1341, 0xcee4c6e8, + 0xef20cada, 0x36774c01, 0xd07e9efe, 0x2bf11fb4, + 0x95dbda4d, 0xae909198, 0xeaad8e71, 0x6b93d5a0, + 0xd08ed1d0, 0xafc725e0, 0x8e3c5b2f, 0x8e7594b7, + 0x8ff6e2fb, 0xf2122b64, 0x8888b812, 0x900df01c, + 0x4fad5ea0, 0x688fc31c, 0xd1cff191, 0xb3a8c1ad, + 0x2f2f2218, 0xbe0e1777, 0xea752dfe, 0x8b021fa1, + 0xe5a0cc0f, 0xb56f74e8, 0x18acf3d6, 0xce89e299, + 0xb4a84fe0, 0xfd13e0b7, 0x7cc43b81, 0xd2ada8d9, + 0x165fa266, 0x80957705, 0x93cc7314, 0x211a1477, + 0xe6ad2065, 0x77b5fa86, 0xc75442f5, 0xfb9d35cf, + 0xebcdaf0c, 0x7b3e89a0, 0xd6411bd3, 0xae1e7e49, + 0x00250e2d, 0x2071b35e, 0x226800bb, 0x57b8e0af, + 0x2464369b, 0xf009b91e, 0x5563911d, 0x59dfa6aa, + 0x78c14389, 0xd95a537f, 0x207d5ba2, 0x02e5b9c5, + 0x83260376, 0x6295cfa9, 0x11c81968, 0x4e734a41, + 0xb3472dca, 0x7b14a94a, 0x1b510052, 0x9a532915, + 0xd60f573f, 0xbc9bc6e4, 0x2b60a476, 0x81e67400, + 0x08ba6fb5, 0x571be91f, 0xf296ec6b, 0x2a0dd915, + 0xb6636521, 0xe7b9f9b6, 0xff34052e, 0xc5855664, + 0x53b02d5d, 0xa99f8fa1, 0x08ba4799, 0x6e85076a, + 0x4b7a70e9, 0xb5b32944, 0xdb75092e, 0xc4192623, + 0xad6ea6b0, 0x49a7df7d, 0x9cee60b8, 0x8fedb266, + 0xecaa8c71, 0x699a17ff, 0x5664526c, 0xc2b19ee1, + 0x193602a5, 0x75094c29, 0xa0591340, 0xe4183a3e, + 0x3f54989a, 0x5b429d65, 0x6b8fe4d6, 0x99f73fd6, + 0xa1d29c07, 0xefe830f5, 0x4d2d38e6, 0xf0255dc1, + 0x4cdd2086, 0x8470eb26, 0x6382e9c6, 0x021ecc5e, + 0x09686b3f, 0x3ebaefc9, 0x3c971814, 0x6b6a70a1, + 0x687f3584, 0x52a0e286, 0xb79c5305, 0xaa500737, + 0x3e07841c, 0x7fdeae5c, 0x8e7d44ec, 0x5716f2b8, + 0xb03ada37, 0xf0500c0d, 0xf01c1f04, 0x0200b3ff, + 0xae0cf51a, 0x3cb574b2, 0x25837a58, 0xdc0921bd, + 0xd19113f9, 0x7ca92ff6, 0x94324773, 0x22f54701, + 0x3ae5e581, 0x37c2dadc, 0xc8b57634, 0x9af3dda7, + 0xa9446146, 0x0fd0030e, 0xecc8c73e, 0xa4751e41, + 0xe238cd99, 0x3bea0e2f, 0x3280bba1, 0x183eb331, + 0x4e548b38, 0x4f6db908, 0x6f420d03, 0xf60a04bf, + 0x2cb81290, 0x24977c79, 0x5679b072, 0xbcaf89af, + 0xde9a771f, 0xd9930810, 0xb38bae12, 0xdccf3f2e, + 0x5512721f, 0x2e6b7124, 0x501adde6, 0x9f84cd87, + 0x7a584718, 0x7408da17, 0xbc9f9abc, 0xe94b7d8c, + 0xec7aec3a, 0xdb851dfa, 0x63094366, 0xc464c3d2, + 0xef1c1847, 0x3215d908, 0xdd433b37, 0x24c2ba16, + 0x12a14d43, 0x2a65c451, 0x50940002, 0x133ae4dd, + 0x71dff89e, 0x10314e55, 0x81ac77d6, 0x5f11199b, + 0x043556f1, 0xd7a3c76b, 0x3c11183b, 0x5924a509, + 0xf28fe6ed, 0x97f1fbfa, 0x9ebabf2c, 0x1e153c6e, + 0x86e34570, 0xeae96fb1, 0x860e5e0a, 0x5a3e2ab3, + 0x771fe71c, 0x4e3d06fa, 0x2965dcb9, 0x99e71d0f, + 0x803e89d6, 0x5266c825, 0x2e4cc978, 0x9c10b36a, + 0xc6150eba, 0x94e2ea78, 0xa5fc3c53, 0x1e0a2df4, + 0xf2f74ea7, 0x361d2b3d, 0x1939260f, 0x19c27960, + 0x5223a708, 0xf71312b6, 0xebadfe6e, 0xeac31f66, + 0xe3bc4595, 0xa67bc883, 0xb17f37d1, 0x018cff28, + 0xc332ddef, 0xbe6c5aa5, 0x65582185, 0x68ab9802, + 0xeecea50f, 0xdb2f953b, 0x2aef7dad, 0x5b6e2f84, + 0x1521b628, 0x29076170, 0xecdd4775, 0x619f1510, + 0x13cca830, 0xeb61bd96, 0x0334fe1e, 0xaa0363cf, + 0xb5735c90, 0x4c70a239, 0xd59e9e0b, 0xcbaade14, + 0xeecc86bc, 0x60622ca7, 0x9cab5cab, 0xb2f3846e, + 0x648b1eaf, 0x19bdf0ca, 0xa02369b9, 0x655abb50, + 0x40685a32, 0x3c2ab4b3, 0x319ee9d5, 0xc021b8f7, + 0x9b540b19, 0x875fa099, 0x95f7997e, 0x623d7da8, + 0xf837889a, 0x97e32d77, 0x11ed935f, 0x16681281, + 0x0e358829, 0xc7e61fd6, 0x96dedfa1, 0x7858ba99, + 0x57f584a5, 0x1b227263, 0x9b83c3ff, 0x1ac24696, + 0xcdb30aeb, 0x532e3054, 0x8fd948e4, 0x6dbc3128, + 0x58ebf2ef, 0x34c6ffea, 0xfe28ed61, 0xee7c3c73, + 0x5d4a14d9, 0xe864b7e3, 0x42105d14, 0x203e13e0, + 0x45eee2b6, 0xa3aaabea, 0xdb6c4f15, 0xfacb4fd0, + 0xc742f442, 0xef6abbb5, 0x654f3b1d, 0x41cd2105, + 0xd81e799e, 0x86854dc7, 0xe44b476a, 0x3d816250, + 0xcf62a1f2, 0x5b8d2646, 0xfc8883a0, 0xc1c7b6a3, + 0x7f1524c3, 0x69cb7492, 0x47848a0b, 0x5692b285, + 0x095bbf00, 0xad19489d, 0x1462b174, 0x23820e00, + 0x58428d2a, 0x0c55f5ea, 0x1dadf43e, 0x233f7061, + 0x3372f092, 0x8d937e41, 0xd65fecf1, 0x6c223bdb, + 0x7cde3759, 0xcbee7460, 0x4085f2a7, 0xce77326e, + 0xa6078084, 0x19f8509e, 0xe8efd855, 0x61d99735, + 0xa969a7aa, 0xc50c06c2, 0x5a04abfc, 0x800bcadc, + 0x9e447a2e, 0xc3453484, 0xfdd56705, 0x0e1e9ec9, + 0xdb73dbd3, 0x105588cd, 0x675fda79, 0xe3674340, + 0xc5c43465, 0x713e38d8, 0x3d28f89e, 0xf16dff20, + 0x153e21e7, 0x8fb03d4a, 0xe6e39f2b, 0xdb83adf7, + 0xe93d5a68, 0x948140f7, 0xf64c261c, 0x94692934, + 0x411520f7, 0x7602d4f7, 0xbcf46b2e, 0xd4a20068, + 0xd4082471, 0x3320f46a, 0x43b7d4b7, 0x500061af, + 0x1e39f62e, 0x97244546, 0x14214f74, 0xbf8b8840, + 0x4d95fc1d, 0x96b591af, 0x70f4ddd3, 0x66a02f45, + 0xbfbc09ec, 0x03bd9785, 0x7fac6dd0, 0x31cb8504, + 0x96eb27b3, 0x55fd3941, 0xda2547e6, 0xabca0a9a, + 0x28507825, 0x530429f4, 0x0a2c86da, 0xe9b66dfb, + 0x68dc1462, 0xd7486900, 0x680ec0a4, 0x27a18dee, + 0x4f3ffea2, 0xe887ad8c, 0xb58ce006, 0x7af4d6b6, + 0xaace1e7c, 0xd3375fec, 0xce78a399, 0x406b2a42, + 0x20fe9e35, 0xd9f385b9, 0xee39d7ab, 0x3b124e8b, + 0x1dc9faf7, 0x4b6d1856, 0x26a36631, 0xeae397b2, + 0x3a6efa74, 0xdd5b4332, 0x6841e7f7, 0xca7820fb, + 0xfb0af54e, 0xd8feb397, 0x454056ac, 0xba489527, + 0x55533a3a, 0x20838d87, 0xfe6ba9b7, 0xd096954b, + 0x55a867bc, 0xa1159a58, 0xcca92963, 0x99e1db33, + 0xa62a4a56, 0x3f3125f9, 0x5ef47e1c, 0x9029317c, + 0xfdf8e802, 0x04272f70, 0x80bb155c, 0x05282ce3, + 0x95c11548, 0xe4c66d22, 0x48c1133f, 0xc70f86dc, + 0x07f9c9ee, 0x41041f0f, 0x404779a4, 0x5d886e17, + 0x325f51eb, 0xd59bc0d1, 0xf2bcc18f, 0x41113564, + 0x257b7834, 0x602a9c60, 0xdff8e8a3, 0x1f636c1b, + 0x0e12b4c2, 0x02e1329e, 0xaf664fd1, 0xcad18115, + 0x6b2395e0, 0x333e92e1, 0x3b240b62, 0xeebeb922, + 0x85b2a20e, 0xe6ba0d99, 0xde720c8c, 0x2da2f728, + 0xd0127845, 0x95b794fd, 0x647d0862, 0xe7ccf5f0, + 0x5449a36f, 0x877d48fa, 0xc39dfd27, 0xf33e8d1e, + 0x0a476341, 0x992eff74, 0x3a6f6eab, 0xf4f8fd37, + 0xa812dc60, 0xa1ebddf8, 0x991be14c, 0xdb6e6b0d, + 0xc67b5510, 0x6d672c37, 0x2765d43b, 0xdcd0e804, + 0xf1290dc7, 0xcc00ffa3, 0xb5390f92, 0x690fed0b, + 0x667b9ffb, 0xcedb7d9c, 0xa091cf0b, 0xd9155ea3, + 0xbb132f88, 0x515bad24, 0x7b9479bf, 0x763bd6eb, + 0x37392eb3, 0xcc115979, 0x8026e297, 0xf42e312d, + 0x6842ada7, 0xc66a2b3b, 0x12754ccc, 0x782ef11c, + 0x6a124237, 0xb79251e7, 0x06a1bbe6, 0x4bfb6350, + 0x1a6b1018, 0x11caedfa, 0x3d25bdd8, 0xe2e1c3c9, + 0x44421659, 0x0a121386, 0xd90cec6e, 0xd5abea2a, + 0x64af674e, 0xda86a85f, 0xbebfe988, 0x64e4c3fe, + 0x9dbc8057, 0xf0f7c086, 0x60787bf8, 0x6003604d, + 0xd1fd8346, 0xf6381fb0, 0x7745ae04, 0xd736fccc, + 0x83426b33, 0xf01eab71, 0xb0804187, 0x3c005e5f, + 0x77a057be, 0xbde8ae24, 0x55464299, 0xbf582e61, + 0x4e58f48f, 0xf2ddfda2, 0xf474ef38, 0x8789bdc2, + 0x5366f9c3, 0xc8b38e74, 0xb475f255, 0x46fcd9b9, + 0x7aeb2661, 0x8b1ddf84, 0x846a0e79, 0x915f95e2, + 0x466e598e, 0x20b45770, 0x8cd55591, 0xc902de4c, + 0xb90bace1, 0xbb8205d0, 0x11a86248, 0x7574a99e, + 0xb77f19b6, 0xe0a9dc09, 0x662d09a1, 0xc4324633, + 0xe85a1f02, 0x09f0be8c, 0x4a99a025, 0x1d6efe10, + 0x1ab93d1d, 0x0ba5a4df, 0xa186f20f, 0x2868f169, + 0xdcb7da83, 0x573906fe, 0xa1e2ce9b, 0x4fcd7f52, + 0x50115e01, 0xa70683fa, 0xa002b5c4, 0x0de6d027, + 0x9af88c27, 0x773f8641, 0xc3604c06, 0x61a806b5, + 0xf0177a28, 0xc0f586e0, 0x006058aa, 0x30dc7d62, + 0x11e69ed7, 0x2338ea63, 0x53c2dd94, 0xc2c21634, + 0xbbcbee56, 0x90bcb6de, 0xebfc7da1, 0xce591d76, + 0x6f05e409, 0x4b7c0188, 0x39720a3d, 0x7c927c24, + 0x86e3725f, 0x724d9db9, 0x1ac15bb4, 0xd39eb8fc, + 0xed545578, 0x08fca5b5, 0xd83d7cd3, 0x4dad0fc4, + 0x1e50ef5e, 0xb161e6f8, 0xa28514d9, 0x6c51133c, + 0x6fd5c7e7, 0x56e14ec4, 0x362abfce, 0xddc6c837, + 0xd79a3234, 0x92638212, 0x670efa8e, 0x406000e0, + 0x3a39ce37, 0xd3faf5cf, 0xabc27737, 0x5ac52d1b, + 0x5cb0679e, 0x4fa33742, 0xd3822740, 0x99bc9bbe, + 0xd5118e9d, 0xbf0f7315, 0xd62d1c7e, 0xc700c47b, + 0xb78c1b6b, 0x21a19045, 0xb26eb1be, 0x6a366eb4, + 0x5748ab2f, 0xbc946e79, 0xc6a376d2, 0x6549c2c8, + 0x530ff8ee, 0x468dde7d, 0xd5730a1d, 0x4cd04dc6, + 0x2939bbdb, 0xa9ba4650, 0xac9526e8, 0xbe5ee304, + 0xa1fad5f0, 0x6a2d519a, 0x63ef8ce2, 0x9a86ee22, + 0xc089c2b8, 0x43242ef6, 0xa51e03aa, 0x9cf2d0a4, + 0x83c061ba, 0x9be96a4d, 0x8fe51550, 0xba645bd6, + 0x2826a2f9, 0xa73a3ae1, 0x4ba99586, 0xef5562e9, + 0xc72fefd3, 0xf752f7da, 0x3f046f69, 0x77fa0a59, + 0x80e4a915, 0x87b08601, 0x9b09e6ad, 0x3b3ee593, + 0xe990fd5a, 0x9e34d797, 0x2cf0b7d9, 0x022b8b51, + 0x96d5ac3a, 0x017da67d, 0xd1cf3ed6, 0x7c7d2d28, + 0x1f9f25cf, 0xadf2b89b, 0x5ad6b472, 0x5a88f54c, + 0xe029ac71, 0xe019a5e6, 0x47b0acfd, 0xed93fa9b, + 0xe8d3c48d, 0x283b57cc, 0xf8d56629, 0x79132e28, + 0x785f0191, 0xed756055, 0xf7960e44, 0xe3d35e8c, + 0x15056dd4, 0x88f46dba, 0x03a16125, 0x0564f0bd, + 0xc3eb9e15, 0x3c9057a2, 0x97271aec, 0xa93a072a, + 0x1b3f6d9b, 0x1e6321f5, 0xf59c66fb, 0x26dcf319, + 0x7533d928, 0xb155fdf5, 0x03563482, 0x8aba3cbb, + 0x28517711, 0xc20ad9f8, 0xabcc5167, 0xccad925f, + 0x4de81751, 0x3830dc8e, 0x379d5862, 0x9320f991, + 0xea7a90c2, 0xfb3e7bce, 0x5121ce64, 0x774fbe32, + 0xa8b6e37e, 0xc3293d46, 0x48de5369, 0x6413e680, + 0xa2ae0810, 0xdd6db224, 0x69852dfd, 0x09072166, + 0xb39a460a, 0x6445c0dd, 0x586cdecf, 0x1c20c8ae, + 0x5bbef7dd, 0x1b588d40, 0xccd2017f, 0x6bb4e3bb, + 0xdda26a7e, 0x3a59ff45, 0x3e350a44, 0xbcb4cdd5, + 0x72eacea8, 0xfa6484bb, 0x8d6612ae, 0xbf3c6f47, + 0xd29be463, 0x542f5d9e, 0xaec2771b, 0xf64e6370, + 0x740e0d8d, 0xe75b1357, 0xf8721671, 0xaf537d5d, + 0x4040cb08, 0x4eb4e2cc, 0x34d2466a, 0x0115af84, + 0xe1b00428, 0x95983a1d, 0x06b89fb4, 0xce6ea048, + 0x6f3f3b82, 0x3520ab82, 0x011a1d4b, 0x277227f8, + 0x611560b1, 0xe7933fdc, 0xbb3a792b, 0x344525bd, + 0xa08839e1, 0x51ce794b, 0x2f32c9b7, 0xa01fbac9, + 0xe01cc87e, 0xbcc7d1f6, 0xcf0111c3, 0xa1e8aac7, + 0x1a908749, 0xd44fbd9a, 0xd0dadecb, 0xd50ada38, + 0x0339c32a, 0xc6913667, 0x8df9317c, 0xe0b12b4f, + 0xf79e59b7, 0x43f5bb3a, 0xf2d519ff, 0x27d9459c, + 0xbf97222c, 0x15e6fc2a, 0x0f91fc71, 0x9b941525, + 0xfae59361, 0xceb69ceb, 0xc2a86459, 0x12baa8d1, + 0xb6c1075e, 0xe3056a0c, 0x10d25065, 0xcb03a442, + 0xe0ec6e0e, 0x1698db3b, 0x4c98a0be, 0x3278e964, + 0x9f1f9532, 0xe0d392df, 0xd3a0342b, 0x8971f21e, + 0x1b0a7441, 0x4ba3348c, 0xc5be7120, 0xc37632d8, + 0xdf359f8d, 0x9b992f2e, 0xe60b6f47, 0x0fe3f11d, + 0xe54cda54, 0x1edad891, 0xce6279cf, 0xcd3e7e6f, + 0x1618b166, 0xfd2c1d05, 0x848fd2c5, 0xf6fb2299, + 0xf523f357, 0xa6327623, 0x93a83531, 0x56cccd02, + 0xacf08162, 0x5a75ebb5, 0x6e163697, 0x88d273cc, + 0xde966292, 0x81b949d0, 0x4c50901b, 0x71c65614, + 0xe6c6c7bd, 0x327a140a, 0x45e1d006, 0xc3f27b9a, + 0xc9aa53fd, 0x62a80f00, 0xbb25bfe2, 0x35bdd2f6, + 0x71126905, 0xb2040222, 0xb6cbcf7c, 0xcd769c2b, + 0x53113ec0, 0x1640e3d3, 0x38abbd60, 0x2547adf0, + 0xba38209c, 0xf746ce76, 0x77afa1c5, 0x20756060, + 0x85cbfe4e, 0x8ae88dd8, 0x7aaaf9b0, 0x4cf9aa7e, + 0x1948c25c, 0x02fb8a8c, 0x01c36ae4, 0xd6ebe1f9, + 0x90d4f869, 0xa65cdea0, 0x3f09252d, 0xc208e69f, + 0xb74e6132, 0xce77e25b, 0x578fdfe3, 0x3ac372e6 + }; + + // OpenBSD IV: "OxychromaticBlowfishSwatDynamite" in big endian + private static final int[] openbsd_iv = new int[] { + 0x4f787963, 0x68726f6d, 0x61746963, 0x426c6f77, + 0x66697368, 0x53776174, 0x44796e61, 0x6d697465, + }; + + // bcrypt IV: "OrpheanBeholderScryDoubt". The C implementation calls + // this "ciphertext", but it is really plaintext or an IV. We keep + // the name to make code comparison easier. + static private final int bf_crypt_ciphertext[] = { + 0x4f727068, 0x65616e42, 0x65686f6c, + 0x64657253, 0x63727944, 0x6f756274 + }; + + // Table for Base64 encoding + static private final char base64_code[] = { + '.', '/', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', + 'W', 'X', 'Y', 'Z', 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', + 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', + 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4', '5', + '6', '7', '8', '9' + }; + + // Table for Base64 decoding + static private final byte index_64[] = { + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, + -1, -1, -1, -1, -1, -1, 0, 1, 54, 55, + 56, 57, 58, 59, 60, 61, 62, 63, -1, -1, + -1, -1, -1, -1, -1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, + 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, + -1, -1, -1, -1, -1, -1, 28, 29, 30, + 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, + 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, + 51, 52, 53, -1, -1, -1, -1, -1 + }; + + // Expanded Blowfish key + private int P[]; + private int S[]; + + /** + * Encode a byte array using bcrypt's slightly-modified base64 + * encoding scheme. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * + * @param d the byte array to encode + * @param len the number of bytes to encode + * @return base64-encoded string + * @exception IllegalArgumentException if the length is invalid + */ + private static String encode_base64(byte d[], int len) + throws IllegalArgumentException { + int off = 0; + StringBuffer rs = new StringBuffer(); + int c1, c2; + + if (len <= 0 || len > d.length) + throw new IllegalArgumentException ("Invalid len"); + + while (off < len) { + c1 = d[off++] & 0xff; + rs.append(base64_code[(c1 >> 2) & 0x3f]); + c1 = (c1 & 0x03) << 4; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 4) & 0x0f; + rs.append(base64_code[c1 & 0x3f]); + c1 = (c2 & 0x0f) << 2; + if (off >= len) { + rs.append(base64_code[c1 & 0x3f]); + break; + } + c2 = d[off++] & 0xff; + c1 |= (c2 >> 6) & 0x03; + rs.append(base64_code[c1 & 0x3f]); + rs.append(base64_code[c2 & 0x3f]); + } + return rs.toString(); + } + + /** + * Look up the 3 bits base64-encoded by the specified character, + * range-checking againt conversion table + * @param x the base64-encoded value + * @return the decoded value of x + */ + private static byte char64(char x) { + if ((int)x < 0 || (int)x > index_64.length) + return -1; + return index_64[(int)x]; + } + + /** + * Decode a string encoded using bcrypt's base64 scheme to a + * byte array. Note that this is *not* compatible with + * the standard MIME-base64 encoding. + * @param s the string to decode + * @param maxolen the maximum number of bytes to decode + * @return an array containing the decoded bytes + * @throws IllegalArgumentException if maxolen is invalid + */ + private static byte[] decode_base64(String s, int maxolen) + throws IllegalArgumentException { + StringBuffer rs = new StringBuffer(); + int off = 0, slen = s.length(), olen = 0; + byte ret[]; + byte c1, c2, c3, c4, o; + + if (maxolen <= 0) + throw new IllegalArgumentException ("Invalid maxolen"); + + while (off < slen - 1 && olen < maxolen) { + c1 = char64(s.charAt(off++)); + c2 = char64(s.charAt(off++)); + if (c1 == -1 || c2 == -1) + break; + o = (byte)(c1 << 2); + o |= (c2 & 0x30) >> 4; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) + break; + c3 = char64(s.charAt(off++)); + if (c3 == -1) + break; + o = (byte)((c2 & 0x0f) << 4); + o |= (c3 & 0x3c) >> 2; + rs.append((char)o); + if (++olen >= maxolen || off >= slen) + break; + c4 = char64(s.charAt(off++)); + o = (byte)((c3 & 0x03) << 6); + o |= c4; + rs.append((char)o); + ++olen; + } + + ret = new byte[olen]; + for (off = 0; off < olen; off++) + ret[off] = (byte)rs.charAt(off); + return ret; + } + + /** + * Blowfish encipher a single 64-bit block encoded as + * two 32-bit halves + * @param lr an array containing the two 32-bit half blocks + * @param off the position in the array of the blocks + */ + private final void encipher(int lr[], int off) { + int i, n, l = lr[off], r = lr[off + 1]; + + l ^= P[0]; + for (i = 0; i <= BLOWFISH_NUM_ROUNDS - 2;) { + // Feistel substitution on left word + n = S[(l >> 24) & 0xff]; + n += S[0x100 | ((l >> 16) & 0xff)]; + n ^= S[0x200 | ((l >> 8) & 0xff)]; + n += S[0x300 | (l & 0xff)]; + r ^= n ^ P[++i]; + + // Feistel substitution on right word + n = S[(r >> 24) & 0xff]; + n += S[0x100 | ((r >> 16) & 0xff)]; + n ^= S[0x200 | ((r >> 8) & 0xff)]; + n += S[0x300 | (r & 0xff)]; + l ^= n ^ P[++i]; + } + lr[off] = r ^ P[BLOWFISH_NUM_ROUNDS + 1]; + lr[off + 1] = l; + } + + /** + * Cycically extract a word of key material + * @param data the string to extract the data from + * @param offp a "pointer" (as a one-entry array) to the + * current offset into data + * @return the next word of material from data + */ + private static int streamtoword(byte data[], int offp[]) { + int i; + int word = 0; + int off = offp[0]; + + for (i = 0; i < 4; i++) { + word = (word << 8) | (data[off] & 0xff); + off = (off + 1) % data.length; + } + + offp[0] = off; + return word; + } + + /** + * Initialise the Blowfish key schedule + */ + private void init_key() { + P = (int[])P_orig.clone(); + S = (int[])S_orig.clone(); + } + + /** + * Key the Blowfish cipher + * @param key an array containing the key + */ + private void key(byte key[]) { + int i; + int koffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Perform the "enhanced key schedule" step described by + * Provos and Mazieres in "A Future-Adaptable Password Scheme" + * http://www.openbsd.org/papers/bcrypt-paper.ps + * @param data salt information + * @param key password information + */ + private void ekskey(byte data[], byte key[]) { + int i; + int koffp[] = { 0 }, doffp[] = { 0 }; + int lr[] = { 0, 0 }; + int plen = P.length, slen = S.length; + + for (i = 0; i < plen; i++) + P[i] = P[i] ^ streamtoword(key, koffp); + + for (i = 0; i < plen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + P[i] = lr[0]; + P[i + 1] = lr[1]; + } + + for (i = 0; i < slen; i += 2) { + lr[0] ^= streamtoword(data, doffp); + lr[1] ^= streamtoword(data, doffp); + encipher(lr, 0); + S[i] = lr[0]; + S[i + 1] = lr[1]; + } + } + + /** + * Compatibility with new OpenBSD function. + */ + public void hash(byte[] hpass, byte[] hsalt, byte[] output) { + init_key(); + ekskey(hsalt, hpass); + for (int i = 0; i < 64; i++) { + key(hsalt); + key(hpass); + } + + int[] buf = new int[openbsd_iv.length]; + System.arraycopy(openbsd_iv, 0, buf, 0, openbsd_iv.length); + for (int i = 0; i < 8; i += 2) { + for (int j = 0; j < 64; j++) { + encipher(buf, i); + } + } + + for (int i = 0, j = 0; i < buf.length; i++) { + // Output of this is little endian + output[j++] = (byte)(buf[i] & 0xff); + output[j++] = (byte)((buf[i] >> 8) & 0xff); + output[j++] = (byte)((buf[i] >> 16) & 0xff); + output[j++] = (byte)((buf[i] >> 24) & 0xff); + } + } + + /** + * Compatibility with new OpenBSD function. + */ + public void pbkdf(byte[] password, byte[] salt, int rounds, byte[] output) { + try { + MessageDigest sha512 = MessageDigest.getInstance("SHA-512"); + + int nblocks = (output.length + 31) / 32; + byte[] hpass = sha512.digest(password); + + byte[] hsalt = new byte[64]; + byte[] block_b = new byte[4]; + byte[] out = new byte[32]; + byte[] tmp = new byte[32]; + for (int block = 1; block <= nblocks; block++) { + // Block count is in big endian + block_b[0] = (byte) ((block >> 24) & 0xFF); + block_b[1] = (byte) ((block >> 16) & 0xFF); + block_b[2] = (byte) ((block >> 8) & 0xFF); + block_b[3] = (byte) (block & 0xFF); + + sha512.reset(); + sha512.update(salt); + sha512.update(block_b); + sha512.digest(hsalt, 0, hsalt.length); + + hash(hpass, hsalt, out); + System.arraycopy(out, 0, tmp, 0, out.length); + + for (int round = 1; round < rounds; round++) { + sha512.reset(); + sha512.update(tmp); + sha512.digest(hsalt, 0, hsalt.length); + + hash(hpass, hsalt, tmp); + + for (int i = 0; i < tmp.length; i++) { + out[i] ^= tmp[i]; + } + } + + for (int i = 0; i < out.length; i++) { + int idx = i * nblocks + (block - 1); + if (idx < output.length) { + output[idx] = out[i]; + } + } + } + } catch (DigestException e) { + throw new RuntimeException(e); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Perform the central password hashing step in the + * bcrypt scheme + * @param password the password to hash + * @param salt the binary salt to hash with the password + * @param log_rounds the binary logarithm of the number + * of rounds of hashing to apply + * @param cdata the plaintext to encrypt + * @return an array containing the binary hashed password + */ + public byte[] crypt_raw(byte password[], byte salt[], int log_rounds, + int cdata[]) { + int rounds, i, j; + int clen = cdata.length; + byte ret[]; + + if (log_rounds < 4 || log_rounds > 30) + throw new IllegalArgumentException ("Bad number of rounds"); + rounds = 1 << log_rounds; + if (salt.length != BCRYPT_SALT_LEN) + throw new IllegalArgumentException ("Bad salt length"); + + init_key(); + ekskey(salt, password); + for (i = 0; i != rounds; i++) { + key(password); + key(salt); + } + + for (i = 0; i < 64; i++) { + for (j = 0; j < (clen >> 1); j++) + encipher(cdata, j << 1); + } + + ret = new byte[clen * 4]; + for (i = 0, j = 0; i < clen; i++) { + ret[j++] = (byte)((cdata[i] >> 24) & 0xff); + ret[j++] = (byte)((cdata[i] >> 16) & 0xff); + ret[j++] = (byte)((cdata[i] >> 8) & 0xff); + ret[j++] = (byte)(cdata[i] & 0xff); + } + return ret; + } + + /** + * Hash a password using the OpenBSD bcrypt scheme + * @param password the password to hash + * @param salt the salt to hash with (perhaps generated + * using BCrypt.gensalt) + * @return the hashed password + */ + public static String hashpw(String password, String salt) { + BCrypt B; + String real_salt; + byte passwordb[], saltb[], hashed[]; + char minor = (char)0; + int rounds, off = 0; + StringBuffer rs = new StringBuffer(); + + if (salt.charAt(0) != '$' || salt.charAt(1) != '2') + throw new IllegalArgumentException ("Invalid salt version"); + if (salt.charAt(2) == '$') + off = 3; + else { + minor = salt.charAt(2); + if (minor != 'a' || salt.charAt(3) != '$') + throw new IllegalArgumentException ("Invalid salt revision"); + off = 4; + } + + // Extract number of rounds + if (salt.charAt(off + 2) > '$') + throw new IllegalArgumentException ("Missing salt rounds"); + rounds = Integer.parseInt(salt.substring(off, off + 2)); + + real_salt = salt.substring(off + 3, off + 25); + try { + passwordb = (password + (minor >= 'a' ? "\000" : "")).getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + throw new AssertionError("UTF-8 is not supported"); + } + + saltb = decode_base64(real_salt, BCRYPT_SALT_LEN); + + B = new BCrypt(); + hashed = B.crypt_raw(passwordb, saltb, rounds, + (int[])bf_crypt_ciphertext.clone()); + + rs.append("$2"); + if (minor >= 'a') + rs.append(minor); + rs.append("$"); + if (rounds < 10) + rs.append("0"); + if (rounds > 30) { + throw new IllegalArgumentException( + "rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(rounds)); + rs.append("$"); + rs.append(encode_base64(saltb, saltb.length)); + rs.append(encode_base64(hashed, + bf_crypt_ciphertext.length * 4 - 1)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @param random an instance of SecureRandom to use + * @return an encoded salt value + */ + public static String gensalt(int log_rounds, SecureRandom random) { + StringBuffer rs = new StringBuffer(); + byte rnd[] = new byte[BCRYPT_SALT_LEN]; + + random.nextBytes(rnd); + + rs.append("$2a$"); + if (log_rounds < 10) + rs.append("0"); + if (log_rounds > 30) { + throw new IllegalArgumentException( + "log_rounds exceeds maximum (30)"); + } + rs.append(Integer.toString(log_rounds)); + rs.append("$"); + rs.append(encode_base64(rnd, rnd.length)); + return rs.toString(); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method + * @param log_rounds the log2 of the number of rounds of + * hashing to apply - the work factor therefore increases as + * 2**log_rounds. + * @return an encoded salt value + */ + public static String gensalt(int log_rounds) { + return gensalt(log_rounds, new SecureRandom()); + } + + /** + * Generate a salt for use with the BCrypt.hashpw() method, + * selecting a reasonable default for the number of hashing + * rounds to apply + * @return an encoded salt value + */ + public static String gensalt() { + return gensalt(GENSALT_DEFAULT_LOG2_ROUNDS); + } + + /** + * Check that a plaintext password matches a previously hashed + * one + * @param plaintext the plaintext password to verify + * @param hashed the previously-hashed password + * @return true if the passwords match, false otherwise + */ + public static boolean checkpw(String plaintext, String hashed) { + byte hashed_bytes[]; + byte try_bytes[]; + try { + String try_pw = hashpw(plaintext, hashed); + hashed_bytes = hashed.getBytes("UTF-8"); + try_bytes = try_pw.getBytes("UTF-8"); + } catch (UnsupportedEncodingException uee) { + return false; + } + if (hashed_bytes.length != try_bytes.length) + return false; + byte ret = 0; + for (int i = 0; i < try_bytes.length; i++) + ret |= hashed_bytes[i] ^ try_bytes[i]; + return ret == 0; + } +} diff --git a/src/test/groovy/com/hierynomus/sshj/common/KeyTypeSpec.groovy b/src/test/groovy/com/hierynomus/sshj/common/KeyTypeSpec.groovy new file mode 100644 index 00000000..ef51eabb --- /dev/null +++ b/src/test/groovy/com/hierynomus/sshj/common/KeyTypeSpec.groovy @@ -0,0 +1,45 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 com.hierynomus.sshj.common + +import net.schmizz.sshj.common.KeyType +import net.schmizz.sshj.userauth.keyprovider.OpenSSHKeyFile +import spock.lang.Specification +import spock.lang.Unroll + +class KeyTypeSpec extends Specification { + + @Unroll + def "should determine correct keytype for #type key"() { + given: + OpenSSHKeyFile kf = new OpenSSHKeyFile() + kf.init(privKey, pubKey) + + expect: + KeyType.fromKey(kf.getPublic()) == type + KeyType.fromKey(kf.getPrivate()) == type + + where: + privKey << ["""-----BEGIN EC PRIVATE KEY----- +MHcCAQEEIGhcvG8anyHew/xZJfozh5XIc1kmZZs6o2f0l3KFs4jgoAoGCCqGSM49 +AwEHoUQDQgAEDUA1JYVD7URSoOGdwPxjea+ETD6IABMD9CWfk3NVTNkdu/Ksn7uX +cLTQhx4N16z1IgW2bRbSbsmM++UKXmeWyg== +-----END EC PRIVATE KEY-----"""] + pubKey << ["""ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBA1ANSWFQ+1EUqDhncD8Y3mvhEw+iAATA/Qln5NzVUzZHbvyrJ+7l3C00IceDdes9SIFtm0W0m7JjPvlCl5nlso= SSH Key"""] + type << [KeyType.ECDSA256] + + } +} \ No newline at end of file diff --git a/src/test/groovy/com/hierynomus/sshj/connection/channel/direct/LocalPortForwarderSpec.groovy b/src/test/groovy/com/hierynomus/sshj/connection/channel/direct/LocalPortForwarderSpec.groovy index 4c6ac9d6..aa4fc221 100644 --- a/src/test/groovy/com/hierynomus/sshj/connection/channel/direct/LocalPortForwarderSpec.groovy +++ b/src/test/groovy/com/hierynomus/sshj/connection/channel/direct/LocalPortForwarderSpec.groovy @@ -19,6 +19,7 @@ import com.hierynomus.sshj.test.SshFixture import net.schmizz.sshj.connection.channel.direct.LocalPortForwarder import org.junit.Rule import spock.lang.Specification +import spock.util.concurrent.PollingConditions class LocalPortForwarderSpec extends Specification { @Rule @@ -44,6 +45,9 @@ class LocalPortForwarderSpec extends Specification { thread.start() then: + new PollingConditions().eventually { + lpf.isRunning() + } thread.isAlive() when: diff --git a/src/test/groovy/com/hierynomus/sshj/sftp/SFTPClientSpec.groovy b/src/test/groovy/com/hierynomus/sshj/sftp/SFTPClientSpec.groovy index 09764c1b..1fd48615 100644 --- a/src/test/groovy/com/hierynomus/sshj/sftp/SFTPClientSpec.groovy +++ b/src/test/groovy/com/hierynomus/sshj/sftp/SFTPClientSpec.groovy @@ -18,6 +18,7 @@ package com.hierynomus.sshj.sftp import com.hierynomus.sshj.test.SshFixture import com.hierynomus.sshj.test.util.FileUtil import net.schmizz.sshj.SSHClient +import net.schmizz.sshj.sftp.FileMode import net.schmizz.sshj.sftp.SFTPClient import org.junit.Rule import org.junit.rules.TemporaryFolder @@ -167,6 +168,40 @@ class SFTPClientSpec extends Specification { !new File(dest, "totototo.txt").exists() } + def "should mkdirs with existing parent path"() { + given: + SSHClient sshClient = fixture.setupConnectedDefaultClient() + sshClient.authPassword("test", "test") + SFTPClient ftp = sshClient.newSFTPClient() + ftp.mkdir("dir1") + + when: + ftp.mkdirs("dir1/dir2/dir3/dir4") + + then: + ftp.statExistence("dir1/dir2/dir3/dir4") != null + + cleanup: + ["dir1/dir2/dir3/dir4", "dir1/dir2/dir3", "dir1/dir2", "dir1"].each { + ftp.rmdir(it) + } + ftp.close() + sshClient.disconnect() + } + + def "should stat root"() { + given: + SSHClient sshClient = fixture.setupConnectedDefaultClient() + sshClient.authPassword("test", "test") + SFTPClient ftp = sshClient.newSFTPClient() + + when: + def attrs = ftp.statExistence("/") + + then: + attrs.type == FileMode.Type.DIRECTORY + } + private void doUpload(File src, File dest) throws IOException { SSHClient sshClient = fixture.setupConnectedDefaultClient() sshClient.authPassword("test", "test") diff --git a/src/test/groovy/com/hierynomus/sshj/transport/verification/KnownHostMatchersTest.groovy b/src/test/groovy/com/hierynomus/sshj/transport/verification/KnownHostMatchersSpec.groovy similarity index 87% rename from src/test/groovy/com/hierynomus/sshj/transport/verification/KnownHostMatchersTest.groovy rename to src/test/groovy/com/hierynomus/sshj/transport/verification/KnownHostMatchersSpec.groovy index 4eee4756..d335c753 100644 --- a/src/test/groovy/com/hierynomus/sshj/transport/verification/KnownHostMatchersTest.groovy +++ b/src/test/groovy/com/hierynomus/sshj/transport/verification/KnownHostMatchersSpec.groovy @@ -15,7 +15,6 @@ */ package com.hierynomus.sshj.transport.verification -import com.hierynomus.sshj.transport.verification.KnownHostMatchers import spock.lang.Specification import spock.lang.Unroll @@ -50,6 +49,11 @@ class KnownHostMatchersSpec extends Specification { "aaa.b??.com" | "aaa.bccd.com" | false "|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" | "192.168.1.61" | true "|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=" | "192.168.2.61" | false + "[aaa.bbb.com]:2222" | "aaa.bbb.com" | false + "[aaa.bbb.com]:2222" | "[aaa.bbb.com]:2222" | true + "[aaa.?bb.com]:2222" | "[aaa.dbb.com]:2222" | true + "[aaa.?xb.com]:2222" | "[aaa.dbb.com]:2222" | false + "[*.bbb.com]:2222" | "[aaa.bbb.com]:2222" | true yesno = match ? "" : "no" } } diff --git a/src/test/groovy/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsSpec.groovy b/src/test/groovy/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsSpec.groovy new file mode 100644 index 00000000..ec0029c0 --- /dev/null +++ b/src/test/groovy/com/hierynomus/sshj/transport/verification/OpenSSHKnownHostsSpec.groovy @@ -0,0 +1,152 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 com.hierynomus.sshj.transport.verification + +import net.schmizz.sshj.common.Base64 +import net.schmizz.sshj.common.Buffer +import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts +import net.schmizz.sshj.util.KeyUtil +import org.junit.Rule +import org.junit.rules.TemporaryFolder +import spock.lang.Specification +import spock.lang.Unroll + +import java.security.PublicKey + +class OpenSSHKnownHostsSpec extends Specification { + + @Rule + public TemporaryFolder temp = new TemporaryFolder(); + + def "should parse and verify hashed host entry"() { + given: + def f = knownHosts("|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ=="); + final PublicKey key = KeyUtil + .newRSAPublicKey( + "e8ff4797075a861db9d2319960a836b2746ada3da514955d2921f2c6a6c9895cbd557f604e43772b6303e3cab2ad82d83b21acdef4edb72524f9c2bef893335115acacfe2989bcbb2e978e4fedc8abc090363e205d975c1fdc35e55ba4daa4b5d5ab7a22c40f547a4a0fd1c683dfff10551c708ff8c34ea4e175cb9bf2313865308fa23601e5a610e2f76838be7ded3b4d3a2c49d2d40fa20db51d1cc8ab20d330bb0dadb88b1a12853f0ecb7c7632947b098dcf435a54566bcf92befd55e03ee2a57d17524cd3d59d6e800c66059067e5eb6edb81946b3286950748240ec9afa4389f9b62bc92f94ec0fba9e64d6dc2f455f816016a4c5f3d507382ed5d3365", + "23"); + + when: + OpenSSHKnownHosts openSSHKnownHosts = new OpenSSHKnownHosts(f) + + then: + openSSHKnownHosts.verify("192.168.1.61", 22, key) + !openSSHKnownHosts.verify("192.168.1.2", 22, key) + } + + def "should parse and verify v1 host entry"() { + given: + def f = knownHosts("test.com,1.1.1.1 2048 35 22017496617994656680820635966392838863613340434802393112245951008866692373218840197754553998457793202561151141246686162285550121243768846314646395880632789308110750881198697743542374668273149584280424505890648953477691795864456749782348425425954366277600319096366690719901119774784695056100331902394094537054256611668966698242432417382422091372756244612839068092471592121759862971414741954991375710930168229171638843329213652899594987626853020377726482288618521941129157643483558764875338089684351824791983007780922947554898825663693324944982594850256042689880090306493029526546183035567296830604572253312294059766327") + def key = KeyUtil.newRSAPublicKey("ae6983ed63a33afc69fe0b88b4ba14393120a0b66e1460916a8390ff109139cd14f4e1701ab5c5feeb479441fe2091d04c0ba7d3fa1756b80ed103657ab53b5d7daa38af22f59f9cbfc16892d4ef1f8fd3ae49663c295be1f568a160d54328fbc2c0598f48d32296b1b9942336234952c440cda1bfac904e3391db98e52f9b1de229adc18fc34a9a569717aa9a5b1145e73b8a8394354028d02054ca760243fb8fc1575490607dd098e698e02b5d8bdf22d55ec958245222ef4c65b8836b9f13674a2d2895a587bfd4423b4eeb6d3ef98451640e3d63d2fc6a761ffd34446abab028494caf36d67ffd65298d69f19f2d90bae4c207b671db563a08f1bb9bf237", + "23") + when: + OpenSSHKnownHosts knownHosts = new OpenSSHKnownHosts(f) + + then: + knownHosts.verify("test.com", 22, key) + } + + def "should check all host entries for key"() { + given: + def f = knownHosts(""" +host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBCiYp2IDgzDFhl8T4TRLIhEljvEixz1YN0XWh4dYh0REGK9T4QKiyb28EztPMdcOtz1uyX5rUGYXX9hj99S4SiU= +host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck= +""") + def pk = new Buffer.PlainBuffer(Base64.decode("AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=")).readPublicKey() + when: + def knownhosts = new OpenSSHKnownHosts(f) + + then: + knownhosts.verify("host1", 22, pk) + } + + def "should not fail on bad base64 entry"() { + given: + def f = knownHosts(""" +host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTIDgzDFhl8T4TRLIhEljvEixz1YN0XWh4dYh0REGK9T4QKiyb28EztPMdcOtz1uyX5rUGYXX9hj99S4SiU= +host1 ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck= +""") + def pk = new Buffer.PlainBuffer(Base64.decode("AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBLTjA7hduYGmvV9smEEsIdGLdghSPD7kL8QarIIOkeXmBh+LTtT/T1K+Ot/rmXCZsP8hoUXxbvN+Tks440Ci0ck=")).readPublicKey() + when: + def knownhosts = new OpenSSHKnownHosts(f) + + then: + knownhosts.verify("host1", 22, pk) + } + + def "should mark bad line and not fail"() { + given: + def f = knownHosts("M36Lo+Ik5ukNugvvoNFlpnyiHMmtKxt3FpyEfYuryXjNqMNWHn/ARVnpUIl5jRLTB7WBzyLYMG7X5nuoFL9zYqKGtHxChbDunxMVbspw5WXI9VN+qxcLwmITmpEvI9ApyS/Ox2ZyN7zw==\n") + + when: + def knownhosts = new OpenSSHKnownHosts(f) + + then: + knownhosts.entries().size() == 1 + knownhosts.entries().get(0) instanceof OpenSSHKnownHosts.BadHostEntry + } + + @Unroll + def "should add comment for #type line"() { + given: + def f = knownHosts(s) + + when: + def knownHosts = new OpenSSHKnownHosts(f) + + then: + knownHosts.entries().size() == 1 + knownHosts.entries().get(0) instanceof OpenSSHKnownHosts.CommentEntry + + where: + type << ["newline", "comment"] + s << ["\n", "#comment\n"] + } + + @Unroll + def "should match any host name from multi-host line"() { + given: + def f = knownHosts("schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==") + def pk = new Buffer.PlainBuffer(Base64.decode("AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==")).readPublicKey() + + when: + def knownHosts = new OpenSSHKnownHosts(f) + + then: + knownHosts.verify(h, 22, pk) + + where: + h << ["schmizz.net", "69.163.155.180"] + } + + def "should produce meaningful toString()"() { + given: + def f = knownHosts("schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==") + + when: + def knownhosts = new OpenSSHKnownHosts(f) + + def toStringValue = knownhosts.toString() + then: + toStringValue == "OpenSSHKnownHosts{khFile='" + f + "'}" + } + + def knownHosts(String s) { + def f = temp.newFile("known_hosts") + f.write(s) + return f + } +} diff --git a/src/test/groovy/net/schmizz/sshj/sftp/PathHelperSpec.groovy b/src/test/groovy/net/schmizz/sshj/sftp/PathHelperSpec.groovy new file mode 100644 index 00000000..af835ef9 --- /dev/null +++ b/src/test/groovy/net/schmizz/sshj/sftp/PathHelperSpec.groovy @@ -0,0 +1,62 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.sftp + +import spock.lang.Shared +import spock.lang.Specification +import spock.lang.Unroll + +class PathHelperSpec extends Specification { + + @Shared + def pathHelper = new PathHelper(new PathHelper.Canonicalizer() { + /** + * Very basic, it does not try to canonicalize relative bits in the middle of a path. + */ + @Override + String canonicalize(String path) + throws IOException { + if ("" == path || "." == path || "./" == path) + return "/home/me" + if (".." == path || "../" == path) + return "/home" + return path + } + }, "/") + + + @Unroll + def "should correctly componentize path \"#input\""() { + given: + def components = pathHelper.getComponents(input) + + expect: + components.getName() == name + components.getParent() == parent + components.getPath() == path + + where: + input || name | path | parent + "" || "me" | "/home/me" | "/home" + "/" || "/" | "/" | "" + "." || "me" | "/home/me" | "/home" + ".." || "home" | "/home" | "/" + "somefile" || "somefile" | "somefile" | "" + "dir1/dir2" || "dir2" | "dir1/dir2" | "dir1" + "/home/me/../somedir/somefile" || "somefile" | "/home/me/../somedir/somefile" | "/home/me/../somedir" + + } +} diff --git a/src/test/groovy/net/schmizz/sshj/sftp/ResponseStatusCodeSpec.groovy b/src/test/groovy/net/schmizz/sshj/sftp/ResponseStatusCodeSpec.groovy new file mode 100644 index 00000000..01e8cc90 --- /dev/null +++ b/src/test/groovy/net/schmizz/sshj/sftp/ResponseStatusCodeSpec.groovy @@ -0,0 +1,64 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.sftp + +import spock.lang.Specification +import spock.lang.Unroll + +class ResponseStatusCodeSpec extends Specification { + + @Unroll + def "status #status should have status code #code"() { + expect: + code == status.getCode() + + where: + status || code + Response.StatusCode.UNKNOWN || -1 + Response.StatusCode.OK || 0 + Response.StatusCode.EOF || 1 + Response.StatusCode.NO_SUCH_FILE || 2 + Response.StatusCode.PERMISSION_DENIED || 3 + Response.StatusCode.FAILURE || 4 + Response.StatusCode.BAD_MESSAGE || 5 + Response.StatusCode.NO_CONNECTION || 6 + Response.StatusCode.CONNECITON_LOST || 7 + Response.StatusCode.OP_UNSUPPORTED || 8 + Response.StatusCode.INVALID_HANDLE || 9 + Response.StatusCode.NO_SUCH_PATH || 10 + Response.StatusCode.FILE_ALREADY_EXISTS || 11 + Response.StatusCode.WRITE_PROTECT || 12 + Response.StatusCode.NO_MEDIA || 13 + Response.StatusCode.NO_SPACE_ON_FILESYSTEM || 14 + Response.StatusCode.QUOTA_EXCEEDED || 15 + Response.StatusCode.UNKNOWN_PRINCIPAL || 16 + Response.StatusCode.LOCK_CONFLICT || 17 + Response.StatusCode.DIR_NOT_EMPTY || 18 + Response.StatusCode.NOT_A_DIRECTORY || 19 + Response.StatusCode.INVALID_FILENAME || 20 + Response.StatusCode.LINK_LOOP || 21 + Response.StatusCode.CANNOT_DELETE || 22 + Response.StatusCode.INVALID_PARAMETER || 23 + Response.StatusCode.FILE_IS_A_DIRECTORY || 24 + Response.StatusCode.BYTE_RANGE_LOCK_CONFLICT || 25 + Response.StatusCode.BYTE_RANGE_LOCK_REFUSED || 26 + Response.StatusCode.DELETE_PENDING || 27 + Response.StatusCode.FILE_CORRUPT || 28 + Response.StatusCode.OWNER_INVALID || 29 + Response.StatusCode.GROUP_INVALID || 30 + Response.StatusCode.NO_MATCHING_BYTE_RANGE_LOCK || 31 + } +} diff --git a/src/test/groovy/net/schmizz/sshj/signature/SignatureDSASpec.groovy b/src/test/groovy/net/schmizz/sshj/signature/SignatureDSASpec.groovy new file mode 100644 index 00000000..8913b7f5 --- /dev/null +++ b/src/test/groovy/net/schmizz/sshj/signature/SignatureDSASpec.groovy @@ -0,0 +1,116 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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. + */ +/* +* Copyright (C)2009 - SSHJ Contributors +* +* 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 net.schmizz.sshj.signature + +import spock.lang.Unroll; + +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.NoSuchAlgorithmException; +import java.security.PublicKey; +import java.security.spec.DSAPublicKeySpec; +import java.util.ArrayList; +import java.util.List; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import spock.lang.Specification + +class SignatureDSASpec extends Specification { + + def keyFactory = KeyFactory.getInstance("DSA") + + private PublicKey createPublicKey(final byte[] y, final byte[] p, final byte[] q, final byte[] g) throws Exception { + final BigInteger publicKey = new BigInteger(y); + final BigInteger prime = new BigInteger(p); + final BigInteger subPrime = new BigInteger(q); + final BigInteger base = new BigInteger(g); + final DSAPublicKeySpec dsaPubKeySpec = new DSAPublicKeySpec(publicKey, prime, subPrime, base); + return keyFactory.generatePublic(dsaPubKeySpec); + } + + + @Unroll + def "should verify signature"() { + given: + def signatureDSA = new SignatureDSA() + def publicKey = createPublicKey(y, p, q, g) + signatureDSA.initVerify(publicKey) + + when: + signatureDSA.update(H) + + then: + signatureDSA.verify(H_sig) + + where: + y << [[103, 23, -102, -4, -110, -90, 66, -52, -14, 125, -16, -76, -110, 33, -111, -113, -46, 27, -118, -73, 0, -19, -48, 43, -102, 56, -49, -84, 118, -10, 76, 84, -5, 84, 55, 72, -115, -34, 95, 80, 32, -120, 57, 101, -64, 111, -37, -26, 96, 55, -98, -24, -99, -81, 60, 22, 5, -55, 119, -95, -28, 114, -40, 13, 97, 65, 22, 33, 117, -59, 22, 81, -56, 98, -112, 103, -62, 90, -12, 81, 61, -67, 104, -24, 67, -18, -60, 78, -127, 44, 13, 11, -117, -118, -69, 89, -25, 26, 103, 72, -83, 114, -40, -124, -10, -31, -34, -49, -54, -15, 92, 79, -40, 14, -12, 58, -112, -30, 11, 48, 26, 121, 105, -68, 92, -93, 99, -78] as byte[], + [0, -92, 59, 5, 72, 124, 101, 124, -18, 114, 7, 100, 98, -61, 73, -104, 120, -98, 54, 118, 17, -62, 91, -110, 29, 98, 50, -101, -41, 99, -116, 101, 107, -123, 124, -97, 62, 119, 88, -109, -110, -1, 109, 119, -51, 69, -98, -105, 2, -69, -121, -82, -118, 23, -6, 96, -61, -65, 102, -58, -74, 32, -104, 116, -6, -35, -83, -10, -88, -68, 106, -112, 72, -2, 35, 38, 15, -11, -22, 30, -114, -46, -47, -18, -17, -71, 24, -25, 28, 13, 29, -40, 101, 18, 81, 45, -120, -67, -53, -41, 11, 50, -89, -33, 50, 54, -14, -91, -35, 12, -42, 13, -84, -19, 100, -3, -85, -18, 74, 99, -49, 64, -49, 51, -83, -82, -127, 116, 64] as byte[]] + p << [[0, -3, 127, 83, -127, 29, 117, 18, 41, 82, -33, 74, -100, 46, -20, -28, -25, -10, 17, -73, 82, 60, -17, 68, + 0, -61, 30, 63, -128, -74, 81, 38, 105, 69, 93, 64, 34, 81, -5, 89, 61, -115, 88, -6, -65, -59, -11, -70, + 48, -10, -53, -101, 85, 108, -41, -127, 59, -128, 29, 52, 111, -14, 102, 96, -73, 107, -103, 80, -91, -92, + -97, -97, -24, 4, 123, 16, 34, -62, 79, -69, -87, -41, -2, -73, -58, 27, -8, 59, 87, -25, -58, -88, -90, 21, + 15, 4, -5, -125, -10, -45, -59, 30, -61, 2, 53, 84, 19, 90, 22, -111, 50, -10, 117, -13, -82, 43, 97, -41, + 42, -17, -14, 34, 3, 25, -99, -47, 72, 1, -57] as byte[], + [0, -3, 127, 83, -127, 29, 117, 18, 41, 82, -33, 74, -100, 46, -20, -28, -25, -10, 17, -73, 82, 60, -17, 68, + 0, -61, 30, 63, -128, -74, 81, 38, 105, 69, 93, 64, 34, 81, -5, 89, 61, -115, 88, -6, -65, -59, -11, -70, + 48, -10, -53, -101, 85, 108, -41, -127, 59, -128, 29, 52, 111, -14, 102, 96, -73, 107, -103, 80, -91, -92, + -97, -97, -24, 4, 123, 16, 34, -62, 79, -69, -87, -41, -2, -73, -58, 27, -8, 59, 87, -25, -58, -88, -90, 21, + 15, 4, -5, -125, -10, -45, -59, 30, -61, 2, 53, 84, 19, 90, 22, -111, 50, -10, 117, -13, -82, 43, 97, -41, + 42, -17, -14, 34, 3, 25, -99, -47, 72, 1, -57] as byte[]] + q << [[0, -105, 96, 80, -113, 21, 35, 11, -52, -78, -110, -71, -126, -94, -21, -124, 11, -16, 88, 28, -11] as byte[], + [0, -105, 96, 80, -113, 21, 35, 11, -52, -78, -110, -71, -126, -94, -21, -124, 11, -16, 88, 28, -11] as byte[]] + g << [[0, -9, -31, -96, -123, -42, -101, 61, -34, -53, -68, -85, 92, 54, -72, 87, -71, 121, -108, -81, -69, -6, 58, + -22, -126, -7, 87, 76, 11, 61, 7, -126, 103, 81, 89, 87, -114, -70, -44, 89, 79, -26, 113, 7, 16, -127, + -128, -76, 73, 22, 113, 35, -24, 76, 40, 22, 19, -73, -49, 9, 50, -116, -56, -90, -31, 60, 22, 122, -117, + 84, 124, -115, 40, -32, -93, -82, 30, 43, -77, -90, 117, -111, 110, -93, 127, 11, -6, 33, 53, 98, -15, -5, + 98, 122, 1, 36, 59, -52, -92, -15, -66, -88, 81, -112, -119, -88, -125, -33, -31, 90, -27, -97, 6, -110, + -117, 102, 94, -128, 123, 85, 37, 100, 1, 76, 59, -2, -49, 73, 42] as byte[], + [0, -9, -31, -96, -123, -42, -101, 61, -34, -53, -68, -85, 92, 54, -72, 87, -71, 121, -108, -81, -69, -6, 58, + -22, -126, -7, 87, 76, 11, 61, 7, -126, 103, 81, 89, 87, -114, -70, -44, 89, 79, -26, 113, 7, 16, -127, + -128, -76, 73, 22, 113, 35, -24, 76, 40, 22, 19, -73, -49, 9, 50, -116, -56, -90, -31, 60, 22, 122, -117, + 84, 124, -115, 40, -32, -93, -82, 30, 43, -77, -90, 117, -111, 110, -93, 127, 11, -6, 33, 53, 98, -15, -5, + 98, 122, 1, 36, 59, -52, -92, -15, -66, -88, 81, -112, -119, -88, -125, -33, -31, 90, -27, -97, 6, -110, + -117, 102, 94, -128, 123, 85, 37, 100, 1, 76, 59, -2, -49, 73, 42] as byte[]] + H << [[-13, 20, 103, 73, 115, -68, 113, 74, -25, 12, -90, 19, 56, 73, -7, -49, -118, 107, -69, -39, -6, 82, -123, + 54, -10, -43, 16, -117, -59, 36, -49, 27] as byte[], + [-4, 111, -103, 111, 72, -106, 105, -19, 81, -123, 84, -13, -40, -53, -3, -97, -8, 43, -22, -2, -23, -15, 28, + 116, -63, 96, -79, -127, -84, 63, -6, -94] as byte[]] + H_sig << [[0, 0, 0, 7, 115, 115, 104, 45, 100, 115, 115, 0, 0, 0, 40, -113, -52, 88, -117, 80, -105, -92, -124, -49, + 56, -35, 90, -9, -128, 31, -33, -18, 13, -5, 7, 108, -2, 92, 108, 85, 58, 39, 99, 122, -118, 125, -121, 21, + -37, 2, 55, 109, -23, -125, 4] as byte[], + [0, 0, 0, 7, 115, 115, 104, 45, 100, 115, 115, 0, 0, 0, 40, 0, 79, 84, 118, -50, 11, -117, -112, 52, -25, + -78, -50, -20, 6, -69, -26, 7, 90, -34, -124, 80, 76, -32, -23, -8, 43, 38, -48, -89, -17, -60, -1, -78, + 112, -88, 14, -39, -78, -98, -80] as byte[]] + } + +} diff --git a/src/test/groovy/net/schmizz/sshj/transport/verification/FingerprintVerifierSpec.groovy b/src/test/groovy/net/schmizz/sshj/transport/verification/FingerprintVerifierSpec.groovy new file mode 100644 index 00000000..2293a4d6 --- /dev/null +++ b/src/test/groovy/net/schmizz/sshj/transport/verification/FingerprintVerifierSpec.groovy @@ -0,0 +1,68 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.transport.verification + +import net.schmizz.sshj.common.Base64 +import net.schmizz.sshj.common.Buffer +import spock.lang.Specification +import spock.lang.Unroll + +class FingerprintVerifierSpec extends Specification { + + @Unroll + def "should accept #digest fingerprints"() { + given: + def verifier = FingerprintVerifier.getInstance(fingerprint) + expect: + verifier.verify("", 0, getPublicKey()) + where: + digest << ["SHA-1", "SHA-256", "MD5", "old style"] + fingerprint << ["SHA1:2Fo8c/96zv32xc8GZWbOGYOlRak=", + "SHA256:oQGbQTujGeNIgh0ONthcEpA/BHxtt3rcYY+NxXTxQjs=", + "MD5:d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32", + "d3:5e:40:72:db:08:f1:6d:0c:d7:6d:35:0d:ba:7c:32"] + } + + @Unroll + def "should accept too short #digest fingerprints"() { + given: + def verifier = FingerprintVerifier.getInstance(fingerprint) + expect: + verifier.verify("", 0, getPublicKey()) + where: + digest << ["SHA-1", "SHA-256"] + fingerprint << ["SHA1:2Fo8c/96zv32xc8GZWbOGYOlRak", + "SHA256:oQGbQTujGeNIgh0ONthcEpA/BHxtt3rcYY+NxXTxQjs"] + + } + + def "should produce meaningful toString()"() { + given: + def verifier = FingerprintVerifier.getInstance("SHA1:2Fo8c/96zv32xc8GZWbOGYOlRak") + + when: + def toStringValue = verifier.toString() + + then: + toStringValue == "FingerprintVerifier{digestAlgorithm='SHA-1'}" + } + + def getPublicKey() { + def lines = new File("src/test/resources/keytypes/test_ed25519.pub").readLines() + def keystring = lines[0].split(" ")[1] + return new Buffer.PlainBuffer(Base64.decode(keystring)).readPublicKey() + } +} diff --git a/src/test/groovy/net/schmizz/sshj/userauth/password/ConsolePasswordFinderSpec.groovy b/src/test/groovy/net/schmizz/sshj/userauth/password/ConsolePasswordFinderSpec.groovy new file mode 100644 index 00000000..956cc116 --- /dev/null +++ b/src/test/groovy/net/schmizz/sshj/userauth/password/ConsolePasswordFinderSpec.groovy @@ -0,0 +1,37 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.userauth.password + +import spock.lang.Specification + +class ConsolePasswordFinderSpec extends Specification { + +// def "should read password from console"() { +// given: +// def console = Mock(Console) { +// readPassword(*_) >> "password".toCharArray() +// } +// def cpf = new ConsolePasswordFinder(console) +// def resource = new AccountResource("test", "localhost") +// +// when: +// def password = cpf.reqPassword(resource) +// +// then: +// password == "password".toCharArray() +// +// } +} diff --git a/src/test/groovy/net/schmizz/sshj/xfer/FileSystemFileSpec.groovy b/src/test/groovy/net/schmizz/sshj/xfer/FileSystemFileSpec.groovy new file mode 100644 index 00000000..26ee25e9 --- /dev/null +++ b/src/test/groovy/net/schmizz/sshj/xfer/FileSystemFileSpec.groovy @@ -0,0 +1,54 @@ +/* + * Copyright (C)2009 - SSHJ Contributors + * + * 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 net.schmizz.sshj.xfer + +import spock.lang.Specification + +class FileSystemFileSpec extends Specification { + + def "should get child path"() { + given: + def file = new FileSystemFile("foo") + + when: + def child = file.getChild("bar") + + then: + child.getName() == "bar" + } + + def "should not traverse higher than original path when getChild is called"() { + given: + def file = new FileSystemFile("foo") + + when: + file.getChild("bar/.././foo/../../") + + then: + thrown(IllegalArgumentException.class) + } + + def "should ignore double slash (empty path component)"() { + given: + def file = new FileSystemFile("foo") + + when: + def child = file.getChild("bar//etc/passwd") + + then: + child.getFile().getPath().replace('\\', '/') endsWith "foo/bar/etc/passwd" + } +} diff --git a/src/test/java/com/hierynomus/sshj/IntegrationTest.java b/src/test/java/com/hierynomus/sshj/IntegrationTest.java deleted file mode 100644 index b6f97768..00000000 --- a/src/test/java/com/hierynomus/sshj/IntegrationTest.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (C)2009 - SSHJ Contributors - * - * 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 com.hierynomus.sshj; - -import java.io.File; -import java.io.IOException; -import org.junit.Ignore; -import org.junit.Test; - -import net.schmizz.sshj.DefaultConfig; -import net.schmizz.sshj.SSHClient; -import net.schmizz.sshj.transport.verification.OpenSSHKnownHosts; - -import static org.hamcrest.MatcherAssert.assertThat; - -public class IntegrationTest { - - @Test @Ignore // Should only be enabled for testing against VM - public void shouldConnect() throws IOException { - SSHClient sshClient = new SSHClient(new DefaultConfig()); - sshClient.addHostKeyVerifier(new OpenSSHKnownHosts(new File("/Users/ajvanerp/.ssh/known_hosts"))); - sshClient.connect("172.16.37.147"); - sshClient.authPublickey("jeroen"); - assertThat("Is connected", sshClient.isAuthenticated()); - } -} diff --git a/src/test/java/com/hierynomus/sshj/connection/channel/direct/CommandTest.java b/src/test/java/com/hierynomus/sshj/connection/channel/direct/CommandTest.java index bdd11947..3c62c9a1 100644 --- a/src/test/java/com/hierynomus/sshj/connection/channel/direct/CommandTest.java +++ b/src/test/java/com/hierynomus/sshj/connection/channel/direct/CommandTest.java @@ -18,6 +18,7 @@ package com.hierynomus.sshj.connection.channel.direct; import com.hierynomus.sshj.test.SshFixture; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.connection.channel.direct.Session; +import net.schmizz.sshj.sftp.SFTPClient; import org.junit.Rule; import org.junit.Test; import org.junit.rules.TemporaryFolder; @@ -46,6 +47,10 @@ public class CommandTest { exec.join(); assertThat("File should exist", file.exists()); assertThat("File should be directory", file.isDirectory()); - + SFTPClient sftpClient = sshClient.newSFTPClient(); + if (sftpClient.statExistence("&") != null) { + sftpClient.rmdir("&"); + // TODO fail here when this is fixed + } } } diff --git a/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java b/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java index dc05613d..eda13fab 100644 --- a/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java +++ b/src/test/java/com/hierynomus/sshj/connection/channel/forwarded/RemotePortForwarderTest.java @@ -54,7 +54,7 @@ public class RemotePortForwarderTest { @Before public void setUp() throws IOException { - fixture.getServer().setTcpipForwardingFilter(new AcceptAllForwardingFilter()); + fixture.getServer().setForwardingFilter(new AcceptAllForwardingFilter()); File file = httpServer.getDocRoot().newFile("index.html"); FileUtil.writeToFile(file, "
Hi!
"); } diff --git a/src/test/java/com/hierynomus/sshj/sftp/RemoteFileTest.java b/src/test/java/com/hierynomus/sshj/sftp/RemoteFileTest.java index 30b84d1e..3436af42 100644 --- a/src/test/java/com/hierynomus/sshj/sftp/RemoteFileTest.java +++ b/src/test/java/com/hierynomus/sshj/sftp/RemoteFileTest.java @@ -71,9 +71,7 @@ public class RemoteFileTest { n += rs.read(test, n, 3072 - n); } - if (test[3072] != 0) { - System.err.println("buffer overrun!"); - } + assertThat("buffer overrun", test[3072] == 0); n += rs.read(test, n, test.length - n); // --> ArrayIndexOutOfBoundsException diff --git a/src/test/java/com/hierynomus/sshj/test/BaseAlgorithmTest.java b/src/test/java/com/hierynomus/sshj/test/BaseAlgorithmTest.java index 7567ac24..975f7d9e 100644 --- a/src/test/java/com/hierynomus/sshj/test/BaseAlgorithmTest.java +++ b/src/test/java/com/hierynomus/sshj/test/BaseAlgorithmTest.java @@ -18,6 +18,8 @@ package com.hierynomus.sshj.test; import net.schmizz.sshj.Config; import net.schmizz.sshj.DefaultConfig; import net.schmizz.sshj.SSHClient; +import net.schmizz.sshj.transport.random.JCERandom; +import net.schmizz.sshj.transport.random.SingletonRandomFactory; import org.apache.sshd.server.SshServer; import org.junit.After; import org.junit.Rule; @@ -32,6 +34,8 @@ import static org.hamcrest.MatcherAssert.assertThat; public abstract class BaseAlgorithmTest { private final Logger logger = LoggerFactory.getLogger(this.getClass()); + private SingletonRandomFactory randomFactory = new SingletonRandomFactory(new JCERandom.Factory()); + private DefaultConfig config = new DefaultConfig(); @Rule public SshFixture fixture = new SshFixture(false); @@ -42,11 +46,12 @@ public abstract class BaseAlgorithmTest { @Test public void shouldVerifyAlgorithm() throws IOException { - for (int i = 0; i < 100; i++) { + for (int i = 0; i < 10; i++) { logger.info("--> Attempt {}", i); configureServer(fixture.getServer()); fixture.start(); - Config config = getClientConfig(new DefaultConfig()); + config.setRandomFactory(randomFactory); + Config config = getClientConfig(this.config); SSHClient sshClient = fixture.connectClient(fixture.setupClient(config)); assertThat("should be connected", sshClient.isConnected()); sshClient.disconnect(); diff --git a/src/test/java/com/hierynomus/sshj/test/SshFixture.java b/src/test/java/com/hierynomus/sshj/test/SshFixture.java index 93cbb3fb..0db60596 100644 --- a/src/test/java/com/hierynomus/sshj/test/SshFixture.java +++ b/src/test/java/com/hierynomus/sshj/test/SshFixture.java @@ -20,12 +20,11 @@ import net.schmizz.sshj.DefaultConfig; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.util.gss.BogusGSSAuthenticator; import org.apache.sshd.common.NamedFactory; -import org.apache.sshd.common.keyprovider.AbstractClassLoadableResourceKeyPairProvider; -import org.apache.sshd.common.util.SecurityUtils; -import org.apache.sshd.server.Command; -import org.apache.sshd.server.CommandFactory; +import org.apache.sshd.common.keyprovider.ClassLoadableResourceKeyPairProvider; import org.apache.sshd.server.SshServer; import org.apache.sshd.server.auth.password.PasswordAuthenticator; +import org.apache.sshd.server.command.Command; +import org.apache.sshd.server.command.CommandFactory; import org.apache.sshd.server.scp.ScpCommandFactory; import org.apache.sshd.server.session.ServerSession; import org.apache.sshd.server.shell.ProcessShellFactory; @@ -35,7 +34,6 @@ import org.junit.rules.ExternalResource; import java.io.IOException; import java.net.ServerSocket; import java.util.Arrays; -import java.util.Collections; import java.util.concurrent.atomic.AtomicBoolean; /** @@ -108,8 +106,7 @@ public class SshFixture extends ExternalResource { private SshServer defaultSshServer() { SshServer sshServer = SshServer.setUpDefaultServer(); sshServer.setPort(randomPort()); - AbstractClassLoadableResourceKeyPairProvider fileKeyPairProvider = SecurityUtils.createClassLoadableResourceKeyPairProvider(); - fileKeyPairProvider.setResources(Collections.singletonList(hostkey)); + ClassLoadableResourceKeyPairProvider fileKeyPairProvider = new ClassLoadableResourceKeyPairProvider(hostkey); sshServer.setKeyPairProvider(fileKeyPairProvider); sshServer.setPasswordAuthenticator(new PasswordAuthenticator() { @Override @@ -127,7 +124,7 @@ public class SshFixture extends ExternalResource { } }); sshServer.setCommandFactory(commandFactory); - + sshServer.setShellFactory(new ProcessShellFactory("ls")); return sshServer; } diff --git a/src/test/java/com/hierynomus/sshj/transport/DisconnectionTest.java b/src/test/java/com/hierynomus/sshj/transport/DisconnectionTest.java index 0d2d41d1..fce39036 100644 --- a/src/test/java/com/hierynomus/sshj/transport/DisconnectionTest.java +++ b/src/test/java/com/hierynomus/sshj/transport/DisconnectionTest.java @@ -16,13 +16,17 @@ package com.hierynomus.sshj.transport; import com.hierynomus.sshj.test.SshFixture; +import net.schmizz.sshj.DefaultConfig; import net.schmizz.sshj.SSHClient; import net.schmizz.sshj.common.DisconnectReason; +import net.schmizz.sshj.connection.channel.direct.Session; import net.schmizz.sshj.transport.DisconnectListener; import net.schmizz.sshj.transport.TransportException; +import net.schmizz.sshj.transport.verification.PromiscuousVerifier; import org.junit.Before; import org.junit.Rule; import org.junit.Test; +import org.junit.experimental.theories.suppliers.TestedOn; import java.io.IOException; import java.util.concurrent.TimeUnit; @@ -89,4 +93,14 @@ public class DisconnectionTest { assertFalse(joinToClientTransport(2)); } + @Test + public void shouldNotThrowTimeoutOnDisconnect() throws IOException { + fixture.getClient().authPassword("u", "u"); + Session session = fixture.getClient().startSession(); + session.allocateDefaultPTY(); + Session.Shell shell = session.startShell(); + + session.close(); + fixture.getClient().disconnect(); + } } diff --git a/src/test/java/com/hierynomus/sshj/transport/kex/KeyExchangeTest.java b/src/test/java/com/hierynomus/sshj/transport/kex/KeyExchangeTest.java index ad68318b..1a9e7a51 100644 --- a/src/test/java/com/hierynomus/sshj/transport/kex/KeyExchangeTest.java +++ b/src/test/java/com/hierynomus/sshj/transport/kex/KeyExchangeTest.java @@ -19,6 +19,7 @@ import com.hierynomus.sshj.test.BaseAlgorithmTest; import net.schmizz.sshj.Config; import net.schmizz.sshj.DefaultConfig; import net.schmizz.sshj.common.Factory; +import net.schmizz.sshj.transport.kex.Curve25519SHA256; import net.schmizz.sshj.transport.kex.DHGexSHA1; import net.schmizz.sshj.transport.kex.DHGexSHA256; import net.schmizz.sshj.transport.kex.ECDHNistP; @@ -38,15 +39,21 @@ import java.util.Collections; @RunWith(Parameterized.class) public class KeyExchangeTest extends BaseAlgorithmTest { - @Parameterized.Parameters + @Parameterized.Parameters(name = "algorithm={0}") public static Collection