//********************************************************************* // // Copyright (c) 2016 by Teradata Corporation // All Rights Reserved // //********************************************************************* // // File: TJEncryptPassword.java // Purpose: Encrypts a password, saves the encryption key in one file, and saves the encrypted password in a second file // // This program accepts eight command-line arguments: // // 1. Transformation - transformation argument for the Cipher.getInstance method. // Example: AES/CBC/NoPadding // // 2. KeySizeInBits - keysize argument for the KeyGenerator.init method. // Specify -default to use the transformation's default key size. // Example: -default // // To use AES with a 192-bit or 256-bit key, the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files // must be downloaded from Oracle and installed in your JRE. // Example: 256 // // 3. MAC - algorithm argument for the Mac.getInstance method. // MAC algorithm HmacSHA256 is available with JDK 5 and later. MAC algorithm HmacSHA1 is available with JDK 1.4.2. // Example: HmacSHA256 // // 4. PasswordEncryptionKeyFileName - a filename in the current directory, a relative pathname, or an absolute pathname. // The file is created by this program. If the file already exists, it will be overwritten by the new file. // Example: PassKey.properties // // 5. EncryptedPasswordFileName - a filename in the current directory, a relative pathname, or an absolute pathname. // The filename or pathname that must differ from the PasswordEncryptionKeyFileName. // The file is created by this program. If the file already exists, it will be overwritten by the new file. // Example: EncPass.properties // // 6. Hostname - the Teradata Database hostname. // Example: whomooz // // 7. Username - the Teradata Database username. // Example: guest // // 8. Password - the Teradata Database password to be encrypted. // Unicode characters in the password can be specified with the backslash uXXXX escape sequence. // Example: please // // Overview // -------- // // Teradata JDBC Driver Stored Password Protection enables an application to provide a JDBC connection password in encrypted form to // the Teradata JDBC Driver, and also enables an application to provide the NEW_PASSWORD connection parameter's value in encrypted form. // // Stored Password Protection is available beginning with Teradata JDBC Driver 16.00.00.24. // // There are several different ways that an application may specify a password to the Teradata JDBC Driver, all of which may use an // encrypted password: // 1. A login password specified as the third argument to the DriverManager.getConnection(String,String,String) method. // 2. A login password specified as the "password" property to the DriverManager.getConnection(String,Properties) method. // 3. A login password specified as the PASSWORD connection URL parameter with the DriverManager.getConnection(String) method. // 4. A login password specified within the LOGDATA connection URL parameter with any variant of the DriverManager.getConnection method. // 5. A login password specified as the DataSource or ConnectionPoolDataSource password parameter. // 6. A login password specified within the DataSource or ConnectionPoolDataSource LOGDATA parameter. // 7. A new password specified as the NEW_PASSWORD connection URL parameter with any variant of the DriverManager.getConnection method. // 8. A new password specified as the DataSource or ConnectionPoolDataSource NEW_PASSWORD parameter. // // If the password, however specified, begins with the prefix "ENCRYPTED_PASSWORD(" then the specified password must follow this format: // // ENCRYPTED_PASSWORD(PasswordEncryptionKeyResourceName,EncryptedPasswordResourceName) // // The PasswordEncryptionKeyResourceName must be separated from the EncryptedPasswordResourceName by a single comma. // The PasswordEncryptionKeyResourceName specifies the name of a resource that contains the password encryption key and associated information. // The EncryptedPasswordResourceName specifies the name of a resource that contains the encrypted password and associated information. // The two resources are described below. // // When an encrypted password is specified for the PASSWORD, NEW_PASSWORD, and/or LOGDATA connection URL parameters, the value must be // enclosed in single quotes, to enclose the "ENCRYPTED_PASSWORD(" syntax's comma separator for the resource names, otherwise that comma // would be interpreted as a separator for the next connection URL parameter. // // This program works in conjunction with Teradata JDBC Driver Stored Password Protection. // This program creates the files containing the password encryption key and encrypted password, which can be subsequently specified to // the Teradata JDBC Driver via the "ENCRYPTED_PASSWORD(" syntax. // // You are not required to use this program to create the files containing the password encryption key and encrypted password. // You can develop your own software to create the necessary files. // The only requirement is that the files must match the format expected by the Teradata JDBC Driver, which is documented below. // // This program encrypts the password and then immediately decrypts the password, in order to verify that the password can be // successfully decrypted. This program mimics the implementation of the Teradata JDBC Driver's password decryption, and is // intended to openly illustrate its operation and enable scrutiny by the community. // // The encrypted password is only as safe as the two files. You are responsible for restricting access to the files containing the // password encryption key and encrypted password. If an attacker obtains both files, the password can be decrypted. // The operating system file permissions for the two files should be as limited and restrictive as possible, to ensure that only the // intended operating system userid has access to the files. // // The two files can be kept on separate physical volumes, to reduce the risk that both files might be lost at the same time. // If either or both of the files are located on a network volume, then an encrypted wire protocol can be used to access the // network volume, such as sshfs, encrypted NFSv4, or encrypted SMB 3.0. // // Password Encryption Key File Format // ----------------------------------- // // The password encryption key file is a text file in Java Properties file format, using the ISO 8859-1 character encoding. // The file must contain the following string properties: // // version=1 // // The version number must be 1. // This property is required. // // transformation=TransformationName // // This value must be a valid transformation argument for the Cipher.getInstance method. // This property is required. // // algorithm=AlgorithmName // // This value must correspond to the algorithm portion of the transformation. // This value must be a valid algorithm argument for the KeyGenerator.getInstance method. // This property is required. // // match=MatchValue // // The password encryption key and encrypted password files must contain the same match value. // The match values are compared to ensure that the two specified files are related to each other, // serving as a "sanity check" to help avoid configuration errors. // This property is required. // // This program uses a timestamp as a shared match value, but a timestamp is not required. // Any shared string can serve as a match value. The timestamp is not related in any way to the encryption of the // password, and the timestamp cannot be used to decrypt the password. // // key=HexDigits // // This value is the password encryption key, encoded as hex digits. // This property is required. // // mac=AlgorithmName // // This value must be a valid algorithm argument for the Mac.getInstance method. // Teradata JDBC Driver Stored Password Protection performs Encrypt-then-MAC for protection from a padding oracle attack. // This property is required. // // mackey=HexDigits // // This value is the MAC key, encoded as hex digits. // This property is required. // // Encrypted Password File Format // ------------------------------ // // The encrypted password file is a text file in Java Properties file format, using the ISO 8859-1 character encoding. // The file must contain the following string properties: // // version=1 // // The version number must be 1. // This property is required. // // match=MatchValue // // The password encryption key and encrypted password files must contain the same match value. // The match values are compared to ensure that the two specified files are related to each other, // serving as a "sanity check" to help avoid configuration errors. // This property is required. // // This program uses a timestamp as a shared match value, but a timestamp is not required. // Any shared string can serve as a match value. The timestamp is not related in any way to the encryption of the // password, and the timestamp cannot be used to decrypt the password. // // password=HexDigits // // This value is the encrypted password, encoded as hex digits. // This property is required. // // params=HexDigits // // This value contains the cipher algorithm parameters, if any, encoded as hex digits. // Some ciphers need algorithm parameters that cannot be derived from the key, such as an initialization vector. // This property is optional, depending on whether the cipher algorithm has associated parameters. // // hash=HexDigits // // This value is the expected message authentication code (MAC), encoded as hex digits. // After encryption, the expected MAC is calculated using the ciphertext, transformation name, and algorithm parameters if any. // Before decryption, the Teradata JDBC Driver calculates the MAC using the ciphertext, transformation name, and algorithm // parameters if any, and verifies that the calculated MAC matches the expected MAC. // If the calculated MAC differs from the expected MAC, then either or both of the files may have been tampered with. // This property is required. // // Transformation, Key Size, and MAC // --------------------------------- // // A transformation is a string that describes the set of operations to be performed on the given input, to produce transformed output. // A transformation always includes the name of a cryptographic algorithm such as DES or AES, and may optionally be followed by a feedback mode // and padding scheme. // // The JDK 7 javadoc for javax.crypto.Cipher indicates that every Java implementation must support the following transformations: // // AES/CBC/NoPadding // AES/CBC/PKCS5Padding // AES/ECB/NoPadding // AES/ECB/PKCS5Padding // DES/CBC/NoPadding // DES/CBC/PKCS5Padding // DES/ECB/NoPadding // DES/ECB/PKCS5Padding // DESede/CBC/NoPadding // DESede/CBC/PKCS5Padding // DESede/ECB/NoPadding // DESede/ECB/PKCS5Padding // RSA/ECB/PKCS1Padding // RSA/ECB/OAEPWithSHA-1AndMGF1Padding // RSA/ECB/OAEPWithSHA-256AndMGF1Padding // // Teradata JDBC Driver Stored Password Protection uses a symmetric encryption algorithm such as DES or AES, in which the same secret key is used // for encryption and decryption of the password. // Teradata JDBC Driver Stored Password Protection does not use an asymmetric encryption algorithm such as RSA, with separate public and private keys. // // ECB (Electronic Codebook) is the simplest block cipher encryption mode. The input is divided into blocks, and each block is encrypted separately. // ECB is unsuitable for encrypting data whose total byte count exceeds the algorithm's block size, and is therefore unsuitable for use with // Teradata JDBC Driver Stored Password Protection. // CBC (Cipher Block Chaining) is a more complex block cipher encryption mode. With CBC, each ciphertext block is dependent on all plaintext blocks // processed up to that point. CBC is suitable for encrypting data whose total byte count exceeds the algorithm's block size, and is therefore // suitable for use with Teradata JDBC Driver Stored Password Protection. // // Teradata JDBC Driver Stored Password Protection hides the password length in the encrypted password file by extending the length of the // UTF8-encoded password with trailing null bytes. The length is extended to the next 512-byte boundary. // A block cipher with no padding, such as AES/CBC/NoPadding, may only be used to encrypt data whose byte count after extension is a multiple // of the algorithm's block size. // The 512-byte boundary is compatible with many block ciphers. AES, for example, has a block size of 128 bits (16 bytes), and is therefore // compatible with the 512-byte boundary. // // A block cipher with padding, such as AES/CBC/PKCS5Padding, can be used to encrypt data of any length. // However, CBC with padding is vulnerable to a "padding oracle attack", so Teradata JDBC Driver Stored Password Protection performs Encrypt-then-MAC // for protection from a padding oracle attack. // MAC algorithm HmacSHA256 is available with JDK 5 and later. MAC algorithm HmacSHA1 is available with JDK 1.4.2. // // A block cipher can encrypt data in units smaller than the cipher's actual block size when using a mode such as CFB (Cipher Feedback) // or OFB (Output Feedback). // A block cipher can be used as a byte-oriented cipher by specifying an 8-bit mode such as CFB8 or OFB8, so a transformation such // as AES/CFB8/NoPadding can be used to encrypt data of any length. // // The strength of the encryption depends on your choice of cipher alogrithm and key size. // AES uses a 128-bit (16 byte), 192-bit (24 byte), or 256-bit (32 byte) key. Specify 128, 192, or 256 as the keysize argument for // the KeyGenerator.init method. // To use AES with a 192-bit or 256-bit key, the Java Cryptography Extension (JCE) Unlimited Strength Jurisdiction Policy Files must be downloaded // from Oracle and installed in your JRE. // DESede uses a 192-bit (24 byte) key that is effectively either a 112-bit or 168-bit key. Specify 112 or 168 as the keysize argument for // the KeyGenerator.init method. // // Resource Names // -------------- // // This program has command-line arguments PasswordEncryptionKeyFileName and EncryptedPasswordFileName to specify filenames. // In contrast, the Teradata JDBC Driver's "ENCRYPTED_PASSWORD(" syntax uses resource names, rather than filenames, in order to offer // more flexibility for file storage location and access. // // ENCRYPTED_PASSWORD(PasswordEncryptionKeyResourceName,EncryptedPasswordResourceName) // // Files created by this program are subsequently accessed as resources by the Teradata JDBC Driver. // The resource names include a prefix to indicate how the resource must be accessed. // If the resource name begins with the "classpath:" prefix, then the Teradata JDBC Driver loads the resource from the classpath. // If you specify a resource name with the "classpath:" prefix, then you must ensure the resource is available on the classpath for the Teradata JDBC Driver. // For security, classpath resources are required to have specific resource name prefixes. // The PasswordEncryptionKeyResourceName must begin with "PassKey" and the EncryptedPasswordResourceName must begin with "EncPass". // // Example: // // ENCRYPTED_PASSWORD(classpath:PassKeyJohnDoe.properties,classpath:EncPassJohnDoe.properties) // // If the resource name begins with a prefix other than "classpath:", then the Teradata JDBC Driver loads the resource via the // new URL(resourcename).openStream() method. Non-classpath resources are not required to have specific resource name prefixes. // You must ensure that non-classpath resources are accessible by the Teradata JDBC Driver. // // The resource name can begin with the "file:" prefix and specify a relative pathname for the Teradata JDBC Driver to load the resource // from a relative-pathname file. // // Example with files in current directory: // // ENCRYPTED_PASSWORD(file:JohnDoeKey.properties,file:JohnDoePass.properties) // // Example with relative paths: // // ENCRYPTED_PASSWORD(file:../dir1/JohnDoeKey.properties,file:../dir2/JohnDoePass.properties) // // The resource name can begin with the "file:" prefix and specify an absolute pathname for the Teradata JDBC Driver to load the resource // from an absolute-pathname file. // // Example with absolute paths on Windows: // // ENCRYPTED_PASSWORD(file:c:/dir1/JohnDoeKey.properties,file:c:/dir2/JohnDoePass.properties) // // Example with absolute paths on Linux: // // ENCRYPTED_PASSWORD(file:/dir1/JohnDoeKey.properties,file:/dir2/JohnDoePass.properties) // //********************************************************************* import java.io.* ; import java.security.* ; import java.sql.* ; import java.text.SimpleDateFormat ; import java.util.* ; import javax.crypto.* ; import javax.crypto.spec.* ; public class TJEncryptPassword { private static final int BOUNDARY = 512 ; public static void main (String [] args) throws ClassNotFoundException , GeneralSecurityException , IOException , SQLException { if (args.length != 8) throw new IllegalArgumentException ("Parameters: Transformation KeySizeInBits MAC PasswordEncryptionKeyFileName EncryptedPasswordFileName Hostname Username Password") ; String sTransformation = args [0] ; // transformation argument for the Cipher.getInstance method String sKeySizeInBits = args [1] ; // keysize argument for the KeyGenerator.init method String sMac = args [2] ; // algorithm argument for the Mac.getInstance method String sPasswordEncryptionKeyFileName = args [3] ; String sEncryptedPasswordFileName = args [4] ; String sHostname = args [5] ; String sUsername = args [6] ; String sPassword = args [7] ; createPasswordEncryptionKeyFile (sTransformation, sKeySizeInBits, sMac, sPasswordEncryptionKeyFileName) ; createEncryptedPasswordFile (sPasswordEncryptionKeyFileName, sEncryptedPasswordFileName, sPassword) ; decryptPassword (sPasswordEncryptionKeyFileName, sEncryptedPasswordFileName) ; String sResource1 = "file:" + sPasswordEncryptionKeyFileName ; String sResource2 = "file:" + sEncryptedPasswordFileName ; String sEncryptedPassword = "ENCRYPTED_PASSWORD(" + sResource1 + "," + sResource2 + ")" ; Class.forName ("com.teradata.jdbc.TeraDriver") ; String sURL = "jdbc:teradata://" + sHostname + "/TMODE=ANSI,CHARSET=UTF8" ; System.out.println ("Connecting to " + sURL + " with user " + sUsername + " and password " + sEncryptedPassword) ; Connection con = DriverManager.getConnection (sURL, sUsername, sEncryptedPassword) ; DatabaseMetaData dbmd = con.getMetaData () ; System.out.println ("Teradata JDBC Driver " + dbmd.getDriverVersion () + " and " + dbmd.getDatabaseProductVersion ()) ; con.close () ; } // end main private static void createPasswordEncryptionKeyFile (String sTransformation, String sKeySizeInBits, String sMac, String sPasswordEncryptionKeyFileName) throws GeneralSecurityException , IOException { String sAlgorithm = sTransformation.replaceFirst ("/.*", "") ; KeyGenerator kgCipher = KeyGenerator.getInstance (sAlgorithm) ; if (! "-default".equals (sKeySizeInBits)) kgCipher.init (Integer.parseInt (sKeySizeInBits)) ; SecretKey keyCipher = kgCipher.generateKey () ; KeyGenerator kgMac = KeyGenerator.getInstance (sMac) ; SecretKey keyMac = kgMac.generateKey () ; Properties props = new Properties () ; props.setProperty ("version", "1") ; props.setProperty ("match", new SimpleDateFormat ("yyyyMMdd.HHmmss.SSS").format (new java.util.Date ())) ; props.setProperty ("transformation", sTransformation) ; props.setProperty ("algorithm", sAlgorithm) ; props.setProperty ("key", formatAsHexDigits (keyCipher.getEncoded ())) ; props.setProperty ("mac", sMac) ; props.setProperty ("mackey", formatAsHexDigits (keyMac.getEncoded ())) ; storePropertiesToFile (props, sPasswordEncryptionKeyFileName, "Teradata JDBC Driver password encryption key file") ; } // end createPasswordEncryptionKeyFile private static void createEncryptedPasswordFile (String sPasswordEncryptionKeyFileName, String sEncryptedPasswordFileName, String sPassword) throws GeneralSecurityException , IOException { if (sPassword.length () == 0) throw new IllegalArgumentException ("Password cannot be a zero-length string") ; sPassword = decodeUnicodeEscapeSequences (sPassword) ; Properties propsKey = loadPropertiesFromFile (sPasswordEncryptionKeyFileName) ; String sVersionNumber = propsKey.getProperty ("version") ; String sMatchValue = propsKey.getProperty ("match") ; String sTransformation = propsKey.getProperty ("transformation") ; String sAlgorithm = propsKey.getProperty ("algorithm") ; byte [] abyKey = parseFromHexDigits (propsKey.getProperty ("key")) ; String sMac = propsKey.getProperty ("mac") ; byte [] abyMacKey = parseFromHexDigits (propsKey.getProperty ("mackey")) ; if (! "1".equals (sVersionNumber)) throw new IllegalArgumentException ("Properties file " + sPasswordEncryptionKeyFileName + " has unexpected or nonexistent version " + sVersionNumber) ; System.out.println (sPasswordEncryptionKeyFileName + " specifies " + sTransformation + " with " + (abyKey.length * 8) + "-bit key" + ", then " + sMac ) ; byte [] abyPassword = sPassword.getBytes ("UTF-8") ; int nPlaintextByteCount = ((abyPassword.length / BOUNDARY) + 1) * BOUNDARY ; // pad up to the next boundary int nTrailerByteCount = nPlaintextByteCount - abyPassword.length ; ByteArrayOutputStream osPlaintext = new ByteArrayOutputStream () ; osPlaintext.write (abyPassword) ; osPlaintext.write (new byte [nTrailerByteCount]) ; // null bytes byte [] abyPlaintext = osPlaintext.toByteArray () ; SecretKeySpec keyCipher = new SecretKeySpec (abyKey, sAlgorithm) ; Cipher cipher = Cipher.getInstance (sTransformation) ; cipher.init (Cipher.ENCRYPT_MODE, keyCipher) ; byte [] abyCiphertext = cipher.doFinal (abyPlaintext) ; AlgorithmParameters params = cipher.getParameters () ; byte [] abyParams = params != null ? params.getEncoded () : null ; ByteArrayOutputStream osContent = new ByteArrayOutputStream () ; osContent.write (abyCiphertext) ; osContent.write (sTransformation.getBytes ("UTF-8")) ; if (abyParams != null) osContent.write (abyParams) ; SecretKeySpec keyMac = new SecretKeySpec (abyMacKey, sMac) ; Mac mac = Mac.getInstance (sMac) ; mac.init (keyMac) ; byte [] abyHash = mac.doFinal (osContent.toByteArray ()) ; Properties propsPassword = new Properties () ; propsPassword.setProperty ("version", sVersionNumber) ; propsPassword.setProperty ("match", sMatchValue) ; propsPassword.setProperty ("password", formatAsHexDigits (abyCiphertext)) ; propsPassword.setProperty ("hash", formatAsHexDigits (abyHash)) ; if (abyParams != null) propsPassword.setProperty ("params", formatAsHexDigits (abyParams)) ; storePropertiesToFile (propsPassword, sEncryptedPasswordFileName, "Teradata JDBC Driver encrypted password file") ; } // end createEncryptedPasswordFile private static void decryptPassword (String sPasswordEncryptionKeyFileName, String sEncryptedPasswordFileName) throws IOException , GeneralSecurityException { Properties propsKey = loadPropertiesFromFile (sPasswordEncryptionKeyFileName) ; Properties propsPassword = loadPropertiesFromFile (sEncryptedPasswordFileName) ; String sVersionNumberA = propsKey.getProperty ("version") ; String sMatchValueA = propsKey.getProperty ("match") ; String sTransformation = propsKey.getProperty ("transformation") ; String sAlgorithm = propsKey.getProperty ("algorithm") ; byte [] abyKey = parseFromHexDigits (propsKey.getProperty ("key")) ; String sMac = propsKey.getProperty ("mac") ; byte [] abyMacKey = parseFromHexDigits (propsKey.getProperty ("mackey")) ; String sVersionNumberB = propsPassword.getProperty ("version") ; String sMatchValueB = propsPassword.getProperty ("match") ; byte [] abyCiphertext = parseFromHexDigits (propsPassword.getProperty ("password")) ; byte [] abyExpectedHash = parseFromHexDigits (propsPassword.getProperty ("hash")) ; String sParams = propsPassword.getProperty ("params") ; byte [] abyParams = sParams != null ? parseFromHexDigits (sParams) : null ; if (! "1".equals (sVersionNumberA)) throw new IllegalArgumentException ("Properties file " + sPasswordEncryptionKeyFileName + " has unexpected or nonexistent version " + sVersionNumberA) ; if (! "1".equals (sVersionNumberB)) throw new IllegalArgumentException ("Properties file " + sEncryptedPasswordFileName + " has unexpected or nonexistent version " + sVersionNumberB) ; if (sMatchValueA == null) throw new IllegalArgumentException ("Properties file " + sPasswordEncryptionKeyFileName + " is missing a match value") ; if (sMatchValueB == null) throw new IllegalArgumentException ("Properties file " + sEncryptedPasswordFileName + " is missing a match value") ; if (! sMatchValueA.equals (sMatchValueB)) throw new IllegalArgumentException ("Properties file " + sPasswordEncryptionKeyFileName + " match value " + sMatchValueA + " differs from properties file " + sEncryptedPasswordFileName + " match value " + sMatchValueB) ; ByteArrayOutputStream osContent = new ByteArrayOutputStream () ; osContent.write (abyCiphertext) ; osContent.write (sTransformation.getBytes ("UTF-8")) ; if (abyParams != null) osContent.write (abyParams) ; SecretKeySpec keyMac = new SecretKeySpec (abyMacKey, sMac) ; Mac mac = Mac.getInstance (sMac) ; mac.init (keyMac) ; byte [] abyActualHash = mac.doFinal (osContent.toByteArray ()) ; if (! Arrays.equals (abyExpectedHash, abyActualHash)) throw new IllegalArgumentException ("Hash mismatch indicates possible tampering with properties file " + sPasswordEncryptionKeyFileName + " and/or " + sEncryptedPasswordFileName) ; Cipher cipher = Cipher.getInstance (sTransformation) ; SecretKeySpec keyCipher = new SecretKeySpec (abyKey, sAlgorithm) ; AlgorithmParameters params = abyParams != null ? AlgorithmParameters.getInstance (sAlgorithm) : null ; if (params != null) params.init (abyParams) ; cipher.init (Cipher.DECRYPT_MODE, keyCipher, params) ; byte [] abyPlaintext = cipher.doFinal (abyCiphertext) ; int nDecodeCount = 0 ; while (abyPlaintext [nDecodeCount] != 0) // find the null byte nDecodeCount++ ; String sDecryptedPassword = new String (abyPlaintext, 0, nDecodeCount, "UTF-8") ; System.out.println ("Decrypted password: " + sDecryptedPassword) ; } // end decryptPassword private static void storePropertiesToFile (Properties props, String sFileName, String sTitle) throws IOException { OutputStream os = new FileOutputStream (sFileName) ; try { props.store (os, sTitle) ; } finally { os.close () ; } System.out.println ("Created " + sFileName) ; } // end storePropertiesToFile private static Properties loadPropertiesFromFile (String sFileName) throws IOException { Properties props = new Properties () ; InputStream is = new FileInputStream (sFileName) ; try { props.load (is) ; } finally { is.close () ; } return props ; } // end loadPropertiesFromFile private static String formatAsHexDigits (byte [] aby) { String sOutput = "" ; for (int i = 0 ; i < aby.length ; i++) { String s = Integer.toHexString (aby [i] & 0xFF) ; sOutput += (s.length () < 2 ? "0" : "") + s ; } return sOutput ; } // end formatAsHexDigits private static byte [] parseFromHexDigits (String s) { if ((s.length () & 1) != 0) throw new IllegalArgumentException ("Odd number of characters: " + s.length ()) ; byte [] aby = new byte [s.length () / 2] ; for (int i = 0 ; i < aby.length ; i++) aby [i] = (byte) Integer.parseInt (s.substring (i * 2, i * 2 + 2), 16) ; return aby ; } // end parseFromHexDigits private static String decodeUnicodeEscapeSequences (String s) { String sOutput = "" ; for (int i = 0 ; i < s.length () ; ) { if (s.regionMatches (i, "\\u", 0, 2)) { String sHexDigits = s.substring (i + 2, i + 6) ; char c = (char) Integer.parseInt (sHexDigits, 16) ; sOutput += c ; i += 6 ; } else { sOutput += s.charAt (i) ; i++ ; } } return sOutput ; } // end decodeUnicodeEscapeSequences } // end class TJEncryptPassword