From 88ccf24895415fa60283cfd1c5576d6bc639e941 Mon Sep 17 00:00:00 2001 From: nathan Date: Sun, 22 Nov 2015 21:37:55 +0100 Subject: [PATCH] Extracted out crypto classes. Added PGP handling --- .../src/dorkbox/util/crypto/BCrypt.java | 15 + .../src/dorkbox/util/crypto/Crypto.java | 533 ++++++------ .../src/dorkbox/util/crypto/CryptoAES.java | 15 + .../src/dorkbox/util/crypto/CryptoDSA.java | 15 + .../src/dorkbox/util/crypto/CryptoECC.java | 15 + .../src/dorkbox/util/crypto/CryptoPGP.java | 815 ++++++++++++++++++ .../src/dorkbox/util/crypto/CryptoRSA.java | 15 + .../src/dorkbox/util/crypto/CryptoSCrypt.java | 19 +- 8 files changed, 1187 insertions(+), 255 deletions(-) create mode 100644 Dorkbox-Util/src/dorkbox/util/crypto/CryptoPGP.java diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java b/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java index 12abf12..7d73942 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/BCrypt.java @@ -1,3 +1,18 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.util.crypto; // Copyright (c) 2006 Damien Miller diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java b/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java index b383fbc..f2e9bfc 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/Crypto.java @@ -16,11 +16,14 @@ package dorkbox.util.crypto; +import com.twmacinta.util.MD5; import dorkbox.util.OS; import dorkbox.util.bytes.LittleEndian; import org.bouncycastle.crypto.Digest; import org.bouncycastle.crypto.PBEParametersGenerator; +import org.bouncycastle.crypto.digests.SHA1Digest; import org.bouncycastle.crypto.digests.SHA256Digest; +import org.bouncycastle.crypto.digests.SHA512Digest; import org.bouncycastle.crypto.generators.PKCS5S2ParametersGenerator; import org.bouncycastle.crypto.params.KeyParameter; import org.bouncycastle.jce.provider.BouncyCastleProvider; @@ -57,6 +60,15 @@ import java.util.jar.JarFile; public final class Crypto { + private + Crypto() { + } + + // CUSTOM_HEADER USE + // check to see if our extra data is OURS. if so, process it + // cafeʞ, as UN signed bytes is: [254, 202, 202, 158], or as hex: FECA CA9E + // cafeʞ, as signed bytes is: [-2, -54, -54, -98] + private static final byte[] CUSTOM_HEADER = new byte[] {(byte) -2, (byte) -54, (byte) -54, (byte) -98}; public static void addProvider() { @@ -67,293 +79,308 @@ class Crypto { } } - private - Crypto() { + + public static + byte[] hashFileMD5(File file) { + try { + return MD5.getHash(file); + } catch (IOException e) { + e.printStackTrace(); + return null; + } } + public static + byte[] hashFileSHA1(File file) { + SHA1Digest digest = new SHA1Digest(); + return hashFile(file, digest, null); + } - public static final - class Util { + public static + byte[] hashFileSHA256(File file) { + SHA256Digest digest = new SHA256Digest(); + return hashFile(file, digest, null); + } - // CUSTOM_HEADER USE - private static final byte[] CUSTOM_HEADER = new byte[] {(byte) -2, (byte) -54, (byte) -54, (byte) -98}; + public static + byte[] hashFileSHA512(File file) { + SHA512Digest digest = new SHA512Digest(); + return hashFile(file, digest, null); + } - /** - * Return the hash of the file or NULL if file is invalid - * - * @param logger - * may be null, if no log output is necessary - */ - public static - byte[] hashFile(File file, Digest digest, Logger logger) { - return hashFile(file, digest, 0L, logger); - } + /** + * Return the hash of the file or NULL if file is invalid + * + * @param logger + * may be null, if no log output is necessary + */ + public static + byte[] hashFile(File file, Digest digest, Logger logger) { + return hashFile(file, digest, 0L, logger); + } - /** - * Return the hash of the file or NULL if file is invalid - * - * @param logger - * may be null, if no log output is necessary - */ - public static - byte[] hashFile(File file, Digest digest, long lengthFromEnd, Logger logger) { - return hashFile(file, digest, lengthFromEnd, null, logger); - } - - /** - * Return the hash of the file or NULL if the file is invalid. ALSO includes the hash of the 'extraData' if specified. - * - * @param logger - * may be null, if no log output is necessary - */ - public static - byte[] hashFile(File file, Digest digest, long lengthFromEnd, byte[] extraData, Logger logger) { - if (file.isFile() && file.canRead()) { - InputStream inputStream = null; - try { - inputStream = new FileInputStream(file); - long size = file.length(); - - if (lengthFromEnd > 0 && lengthFromEnd < size) { - size -= lengthFromEnd; - } - - int bufferSize = 4096; - byte[] buffer = new byte[bufferSize]; - - int readBytes; - digest.reset(); - - while (size > 0) { - //noinspection NumericCastThatLosesPrecision - int maxToRead = (int) Math.min(bufferSize, size); - readBytes = inputStream.read(buffer, 0, maxToRead); - size -= readBytes; - - if (readBytes == 0) { - //wtf. finally still gets called. - return null; - } - - digest.update(buffer, 0, readBytes); - } - } catch (Exception e) { - if (logger != null) { - logger.error("Error hashing file: {}", file.getAbsolutePath(), e); - } - } finally { - if (inputStream != null) { - try { - inputStream.close(); - } catch (IOException e) { - e.printStackTrace(); - } - } - } - - if (extraData != null) { - digest.update(extraData, 0, extraData.length); - } - - byte[] digestBytes = new byte[digest.getDigestSize()]; - - digest.doFinal(digestBytes, 0); - return digestBytes; - - } - else { - return null; - } - } - - /** - * Specifically, to return the hash of the ALL files/directories inside the jar, minus the action specified (LGPL) files. - */ - public static - byte[] hashJarContentsExcludeAction(File jarDestFilename, Digest digest, int action) throws IOException { - JarFile jarDestFile = new JarFile(jarDestFilename); + /** + * Return the hash of the file or NULL if file is invalid + * + * @param logger + * may be null, if no log output is necessary + */ + public static + byte[] hashFile(File file, Digest digest, long lengthFromEnd, Logger logger) { + return hashFile(file, digest, lengthFromEnd, null, logger); + } + /** + * Return the hash of the file or NULL if the file is invalid. ALSO includes the hash of the 'extraData' if specified. + * + * @param logger + * may be null, if no log output is necessary + */ + public static + byte[] hashFile(File file, Digest digest, long lengthFromEnd, byte[] extraData, Logger logger) { + if (file.isFile() && file.canRead()) { + InputStream inputStream = null; try { - Enumeration jarElements = jarDestFile.entries(); + inputStream = new FileInputStream(file); + long size = file.length(); - boolean okToHash; - boolean hasAction; - byte[] buffer = new byte[2048]; - int read; + if (lengthFromEnd > 0 && lengthFromEnd < size) { + size -= lengthFromEnd; + } + + int bufferSize = 4096; + byte[] buffer = new byte[bufferSize]; + + int readBytes; digest.reset(); - while (jarElements.hasMoreElements()) { - JarEntry jarEntry = jarElements.nextElement(); - String name = jarEntry.getName(); - okToHash = !jarEntry.isDirectory(); + while (size > 0) { + //noinspection NumericCastThatLosesPrecision + int maxToRead = (int) Math.min(bufferSize, size); + readBytes = inputStream.read(buffer, 0, maxToRead); + size -= readBytes; - if (!okToHash) { - continue; + if (readBytes == 0) { + //wtf. finally still gets called. + return null; } - // data with NO extra data will NOT BE HASHED - // data that matches our action bitmask WILL NOT BE HASHED - - okToHash = false; - hasAction = false; - - byte[] extraData = jarEntry.getExtra(); - if (extraData == null || extraData.length == 0) { - okToHash = false; - } - else if (extraData.length >= 4) { - for (int i = 0; i < CUSTOM_HEADER.length; i++) { - if (extraData[i] != CUSTOM_HEADER[i]) { - throw new RuntimeException("Unexpected extra data in zip assigned. Aborting"); - } - } - - // this means we matched our header - if (extraData[4] > 0) { - hasAction = true; - - // we have an ACTION describing how it was compressed, etc - int fileAction = LittleEndian.Int_.from(new byte[] {extraData[5], extraData[6], extraData[7], extraData[8]}); - - if ((fileAction & action) != action) { - okToHash = true; - } - } - else { - okToHash = true; - } - } - else { - throw new RuntimeException("Unexpected extra data in zip assigned. Aborting"); - } - - // skips hashing lgpl files. (technically, whatever our action bitmask is...) - // we want to hash everything BY DEFAULT. we ALSO want to hash the NAME, LOAD ACTION TYPE, and the contents - if (okToHash) { - // System.err.println("HASHING: " + name); - // hash the file name - byte[] bytes = name.getBytes(OS.US_ASCII); - digest.update(bytes, 0, bytes.length); - - if (hasAction) { - // hash the action - since we don't want to permit anyone to change this after we sign the file - digest.update(extraData, 5, 4); - } - - // hash the contents - InputStream inputStream = jarDestFile.getInputStream(jarEntry); - while ((read = inputStream.read(buffer)) > 0) { - digest.update(buffer, 0, read); - } - inputStream.close(); - } - //else { - // System.err.println("Skipping: " + name); - //} + digest.update(buffer, 0, readBytes); } } catch (Exception e) { - throw new RuntimeException("Unexpected extra data in zip assigned. Aborting"); + if (logger != null) { + logger.error("Error hashing file: {}", file.getAbsolutePath(), e); + } else { + e.printStackTrace(); + } } finally { - jarDestFile.close(); + if (inputStream != null) { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + } + + if (extraData != null) { + digest.update(extraData, 0, extraData.length); } byte[] digestBytes = new byte[digest.getDigestSize()]; digest.doFinal(digestBytes, 0); return digestBytes; + } + else { + return null; + } + } - /** - * Hash an input stream, based on the specified digest - */ - public static - byte[] hashStream(Digest digest, InputStream inputStream) throws IOException { + /** + * Specifically, to return the hash of the ALL files/directories inside the jar, minus the action specified (LGPL) files. + */ + public static + byte[] hashJarContentsExcludeAction(File jarDestFilename, Digest digest, int action) throws IOException { + JarFile jarDestFile = new JarFile(jarDestFilename); + try { + Enumeration jarElements = jarDestFile.entries(); + + boolean okToHash; + boolean hasAction; byte[] buffer = new byte[2048]; int read; digest.reset(); + while (jarElements.hasMoreElements()) { + JarEntry jarEntry = jarElements.nextElement(); + String name = jarEntry.getName(); + okToHash = !jarEntry.isDirectory(); - while ((read = inputStream.read(buffer)) > 0) { - digest.update(buffer, 0, read); + if (!okToHash) { + continue; + } + + // data with NO extra data will NOT BE HASHED + // data that matches our action bitmask WILL NOT BE HASHED + + okToHash = false; + hasAction = false; + + byte[] extraData = jarEntry.getExtra(); + if (extraData == null || extraData.length == 0) { + okToHash = false; + } + else if (extraData.length >= 4) { + for (int i = 0; i < CUSTOM_HEADER.length; i++) { + if (extraData[i] != CUSTOM_HEADER[i]) { + throw new RuntimeException("Unexpected extra data in zip assigned. Aborting"); + } + } + + // this means we matched our header + if (extraData[4] > 0) { + hasAction = true; + + // we have an ACTION describing how it was compressed, etc + int fileAction = LittleEndian.Int_.from(new byte[] {extraData[5], extraData[6], extraData[7], extraData[8]}); + + if ((fileAction & action) != action) { + okToHash = true; + } + } + else { + okToHash = true; + } + } + else { + throw new RuntimeException("Unexpected extra data in zip assigned. Aborting"); + } + + // skips hashing lgpl files. (technically, whatever our action bitmask is...) + // we want to hash everything BY DEFAULT. we ALSO want to hash the NAME, LOAD ACTION TYPE, and the contents + if (okToHash) { + // System.err.println("HASHING: " + name); + // hash the file name + byte[] bytes = name.getBytes(OS.US_ASCII); + digest.update(bytes, 0, bytes.length); + + if (hasAction) { + // hash the action - since we don't want to permit anyone to change this after we sign the file + digest.update(extraData, 5, 4); + } + + // hash the contents + InputStream inputStream = jarDestFile.getInputStream(jarEntry); + while ((read = inputStream.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + inputStream.close(); + } + //else { + // System.err.println("Skipping: " + name); + //} } - inputStream.close(); - - byte[] digestBytes = new byte[digest.getDigestSize()]; - - digest.doFinal(digestBytes, 0); - return digestBytes; + } catch (Exception e) { + throw new RuntimeException("Unexpected extra data in zip assigned. Aborting"); + } finally { + jarDestFile.close(); } - /** - * Secure way to generate an AES key based on a password. Will '*' out the passed-in password - * - * @param password - * will be filled with '*' - * @param salt - * should be a RANDOM number, at least 256bits (32 bytes) in size. - * @param iterationCount - * should be a lot, like 10,000 - * - * @return the secure key to use - */ - public static - byte[] PBKDF2(char[] password, byte[] salt, int iterationCount) { - // will also zero out the password. - byte[] charToBytes = Crypto.Util.charToBytesPassword(password); + byte[] digestBytes = new byte[digest.getDigestSize()]; - return PBKDF2(charToBytes, salt, iterationCount); - } - - /** - * Secure way to generate an AES key based on a password. - * - * @param password - * The password that you want to mix - * @param salt - * should be a RANDOM number, at least 256bits (32 bytes) in size. - * @param iterationCount - * should be a lot, like 10,000 - * - * @return the secure key to use - */ - public static - byte[] PBKDF2(byte[] password, byte[] salt, int iterationCount) { - SHA256Digest digest = new SHA256Digest(); - PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(digest); - pGen.init(password, salt, iterationCount); - - KeyParameter key = (KeyParameter) pGen.generateDerivedMacParameters(digest.getDigestSize() * 8); // *8 for bit length. - - // zero out the password. - Arrays.fill(password, (byte) 0); - - return key.getKey(); - } - - /** - * this saves the char array in UTF-16 format of bytes and BLANKS out the password char array. - */ - public static - byte[] charToBytesPassword(char[] password) { - // note: this saves the char array in UTF-16 format of bytes. - byte[] passwordBytes = new byte[password.length * 2]; - for (int i = 0; i < password.length; i++) { - //noinspection NumericCastThatLosesPrecision - passwordBytes[2 * i] = (byte) (((int) password[i] & 0xFF00) >> 8); - //noinspection NumericCastThatLosesPrecision - passwordBytes[2 * i + 1] = (byte) ((int) password[i] & 0x00FF); - } - - // asterisk out the password - Arrays.fill(password, '*'); - - return passwordBytes; - } - - - private - Util() { - } + digest.doFinal(digestBytes, 0); + return digestBytes; } + + /** + * Hash an input stream, based on the specified digest + */ + public static + byte[] hashStream(Digest digest, InputStream inputStream) throws IOException { + + byte[] buffer = new byte[2048]; + int read; + digest.reset(); + + + while ((read = inputStream.read(buffer)) > 0) { + digest.update(buffer, 0, read); + } + inputStream.close(); + + byte[] digestBytes = new byte[digest.getDigestSize()]; + + digest.doFinal(digestBytes, 0); + return digestBytes; + } + + /** + * Secure way to generate an AES key based on a password. Will '*' out the passed-in password + * + * @param password + * will be filled with '*' + * @param salt + * should be a RANDOM number, at least 256bits (32 bytes) in size. + * @param iterationCount + * should be a lot, like 10,000 + * + * @return the secure key to use + */ + public static + byte[] PBKDF2(char[] password, byte[] salt, int iterationCount) { + // will also zero out the password. + byte[] charToBytes = Crypto.charToBytesPassword_UTF16(password); + + return PBKDF2(charToBytes, salt, iterationCount); + } + + /** + * Secure way to generate an AES key based on a password. + * + * @param password + * The password that you want to mix + * @param salt + * should be a RANDOM number, at least 256bits (32 bytes) in size. + * @param iterationCount + * should be a lot, like 10,000 + * + * @return the secure key to use + */ + public static + byte[] PBKDF2(byte[] password, byte[] salt, int iterationCount) { + SHA256Digest digest = new SHA256Digest(); + PBEParametersGenerator pGen = new PKCS5S2ParametersGenerator(digest); + pGen.init(password, salt, iterationCount); + + KeyParameter key = (KeyParameter) pGen.generateDerivedMacParameters(digest.getDigestSize() * 8); // *8 for bit length. + + // zero out the password. + Arrays.fill(password, (byte) 0); + + return key.getKey(); + } + + /** + * this saves the char array in UTF-16 format of bytes and BLANKS out the password char array. + */ + public static + byte[] charToBytesPassword_UTF16(char[] password) { + // note: this saves the char array in UTF-16 format of bytes. + byte[] passwordBytes = new byte[password.length * 2]; + for (int i = 0; i < password.length; i++) { + //noinspection NumericCastThatLosesPrecision + passwordBytes[2 * i] = (byte) (((int) password[i] & 0xFF00) >> 8); + //noinspection NumericCastThatLosesPrecision + passwordBytes[2 * i + 1] = (byte) ((int) password[i] & 0x00FF); + } + + // asterisk out the password + Arrays.fill(password, '*'); + + return passwordBytes; + } + } diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoAES.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoAES.java index f7b5281..7f79dd3 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoAES.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoAES.java @@ -1,3 +1,18 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.util.crypto; import org.bouncycastle.crypto.BufferedBlockCipher; diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoDSA.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoDSA.java index 91a3e31..cca4562 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoDSA.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoDSA.java @@ -1,3 +1,18 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.util.crypto; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoECC.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoECC.java index 22a4139..0ffdeba 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoECC.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoECC.java @@ -1,3 +1,18 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.util.crypto; import org.bouncycastle.crypto.AsymmetricCipherKeyPair; diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoPGP.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoPGP.java new file mode 100644 index 0000000..c2f198f --- /dev/null +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoPGP.java @@ -0,0 +1,815 @@ +/* + * Copyright 2015 dorkbox, llc + * + * 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 dorkbox.util.crypto; + +import dorkbox.util.OS; +import dorkbox.util.Sys; +import org.bouncycastle.bcpg.ArmoredOutputStream; +import org.bouncycastle.bcpg.BCPGOutputStream; +import org.bouncycastle.bcpg.CompressionAlgorithmTags; +import org.bouncycastle.openpgp.*; +import org.bouncycastle.openpgp.operator.PBESecretKeyDecryptor; +import org.bouncycastle.openpgp.operator.bc.BcKeyFingerprintCalculator; +import org.bouncycastle.openpgp.operator.bc.BcPBESecretKeyDecryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPContentSignerBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDataEncryptorBuilder; +import org.bouncycastle.openpgp.operator.bc.BcPGPDigestCalculatorProvider; +import org.bouncycastle.openpgp.operator.bc.BcPublicKeyKeyEncryptionMethodGenerator; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.security.NoSuchProviderException; +import java.security.SecureRandom; +import java.util.ArrayList; +import java.util.Date; +import java.util.Iterator; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * PGP crypto related methods + */ +public final +class CryptoPGP { + private static final BcPGPDigestCalculatorProvider digestCalculatorProvider = new BcPGPDigestCalculatorProvider(); + private static final BcKeyFingerprintCalculator fingerprintCalculator = new BcKeyFingerprintCalculator(); + + +// https://github.com/weiliatgithub/bouncycastle-gpg-exampleC +// https://gist.github.com/turingbirds/3df43f1920a98010667a +// http://sloanseaman.com/wordpress/2012/05/13/revisited-pgp-encryptiondecryption-in-java/ +// http://bouncycastle-pgp-cookbook.blogspot.de/ + + /** + * Sign a message using our private PGP key file, this matches gpg -ab "hello.txt" + * + * @param privateKeyInputStream + * this is an armored key file, not a binary stream + * @param userId + * this is the userID to get out of the private key + * @param password + * this is the password to unlock the private key + * @param messageAsUtf8Bytes + * this is the message, in bytes, to sign + */ + public static + byte[] signGpgCompatible(InputStream privateKeyInputStream, String userId, char[] password, byte[] messageAsUtf8Bytes) + throws PGPException { + + // the signature type (in gpg terms), is "sigclass". gpg is BINARY_DOC (0x00) + return sign(privateKeyInputStream, + userId, + password, + new ByteArrayInputStream(messageAsUtf8Bytes), + PGPSignature.BINARY_DOCUMENT, + false, + true, + false, + false, + false); + } + + /** + * Sign a message using our private PGP key file, this matches gpg -ab "hello.txt" + * + * @param privateKeyInputStream + * this is an armored key file, not a binary stream + * @param userId + * this is the userID to get out of the private key + * @param password + * this is the password to unlock the private key + * @param message + * this is the message to sign + */ + public static + byte[] signGpgCompatible(InputStream privateKeyInputStream, String userId, char[] password, InputStream message) + throws PGPException { + + // the signature type (in gpg terms), is "sigclass". gpg is BINARY_DOC (0x00) + return sign(privateKeyInputStream, + userId, + password, + message, + PGPSignature.BINARY_DOCUMENT, + false, + true, + false, + false, + false); + } + + /** + * Sign a message using our private PGP key file, this matches gpg -ab "hello.txt". This will save the signature of the passed-in + * file to file name + .asc + * + * @param privateKeyInputStream + * this is an armored key file, not a binary stream + * @param userId + * this is the userID to get out of the private key + * @param password + * this is the password to unlock the private key + * @param file + * this is the file to sign + */ + public static + void signGpgCompatible(InputStream privateKeyInputStream, String userId, char[] password, File file) + throws PGPException { + + // the signature type (in gpg terms), is "sigclass". gpg is BINARY_DOC (0x00) + final byte[] sign = sign(privateKeyInputStream, + userId, + password, + file, + PGPSignature.BINARY_DOCUMENT, + false, + true, + false, + false, + false); + + FileOutputStream fileOutputStream1 = null; + try { + fileOutputStream1 = new FileOutputStream(new File(file.getAbsolutePath() + ".asc")); + fileOutputStream1.write(sign); + fileOutputStream1.flush(); + } catch (FileNotFoundException e) { + throw new PGPException("Unable to save signature to file " + file.getAbsolutePath() + ".asc", e); + } catch (IOException e) { + throw new PGPException("Unable to save signature to file " + file.getAbsolutePath() + ".asc", e); + } finally { + Sys.close(fileOutputStream1); + } + } + + /** + * Sign a message using our private PGP key file, with a variety of options + */ + @SuppressWarnings("Duplicates") + public static + byte[] sign(InputStream privateKeyInputStream, + String userId, + char[] password, + InputStream message, + int signatureType, + boolean compressSignature, + boolean asciiArmoredOutput, + boolean includeDataInSignature, + boolean generateUserIdSubPacket, + boolean generateOnePassVersion) throws PGPException { + + List secretKeys = getSecretKeys(privateKeyInputStream, userId); + PGPSignatureGenerator signature = createSignature(secretKeys, password, signatureType, generateUserIdSubPacket); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + OutputStream outputStream = byteArrayOutputStream; + if (asciiArmoredOutput) { + outputStream = new ArmoredOutputStream(byteArrayOutputStream); + } + + PGPCompressedDataGenerator compressedDataGenerator = null; + BCPGOutputStream bcOutputStream; + + if (compressSignature) { + compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZLIB); + try { + bcOutputStream = new BCPGOutputStream(compressedDataGenerator.open(outputStream)); + } catch (IOException e) { + throw new PGPException("Unable to open compression stream in the signature", e); + } + } + else { + bcOutputStream = new BCPGOutputStream(outputStream); + } + + if (generateOnePassVersion) { + try { + signature.generateOnePassVersion(false) + .encode(bcOutputStream); + } catch (IOException e) { + throw new PGPException("Unable to generate OnePass signature header", e); + } + } + + PGPLiteralDataGenerator literalDataGenerator = null; + OutputStream literalDataOutput = null; + + if (includeDataInSignature) { + literalDataGenerator = new PGPLiteralDataGenerator(); + try { + literalDataOutput = literalDataGenerator.open(bcOutputStream, + PGPLiteralData.BINARY, + "_CONSOLE", + message.available(), + new Date()); + } catch (IOException e1) { + throw new PGPException("Unable to generate Literal Data signature header", e1); + } + } + + try { + byte[] buffer = new byte[4096]; + int read; + + // update bytes in the streams + if (literalDataOutput != null) { + while ((read = message.read(buffer)) > 0) { + literalDataOutput.write(buffer, 0, read); + signature.update(buffer, 0, read); + } + literalDataOutput.flush(); + } else { + + while ((read = message.read(buffer)) > 0) { + signature.update(buffer, 0, read); + } + } + + // close generators and update signature + if (literalDataGenerator != null) { + literalDataGenerator.close(); + } + + signature.generate() + .encode(bcOutputStream); + + + if (compressedDataGenerator != null) { + compressedDataGenerator.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Sys.close(bcOutputStream); + Sys.close(outputStream); + Sys.close(literalDataOutput); + } + + return byteArrayOutputStream.toByteArray(); + } + + /** + * Sign a message using our private PGP key file, with a variety of options + */ + @SuppressWarnings("Duplicates") + public static + byte[] sign(InputStream privateKeyInputStream, + String userId, + char[] password, + File fileMessage, + int signatureType, + boolean compressSignature, + boolean asciiArmoredOutput, + boolean includeDataInSignature, + boolean generateUserIdSubPacket, + boolean generateOnePassVersion) throws PGPException { + + List secretKeys = getSecretKeys(privateKeyInputStream, userId); + PGPSignatureGenerator signature = createSignature(secretKeys, password, signatureType, generateUserIdSubPacket); + + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + OutputStream outputStream = byteArrayOutputStream; + if (asciiArmoredOutput) { + outputStream = new ArmoredOutputStream(byteArrayOutputStream); + } + + PGPCompressedDataGenerator compressedDataGenerator = null; + BCPGOutputStream bcOutputStream; + + if (compressSignature) { + compressedDataGenerator = new PGPCompressedDataGenerator(PGPCompressedData.ZLIB); + try { + bcOutputStream = new BCPGOutputStream(compressedDataGenerator.open(outputStream)); + } catch (IOException e) { + throw new PGPException("Unable to open compression stream in the signature", e); + } + } + else { + bcOutputStream = new BCPGOutputStream(outputStream); + } + + if (generateOnePassVersion) { + try { + signature.generateOnePassVersion(false) + .encode(bcOutputStream); + } catch (IOException e) { + throw new PGPException("Unable to generate OnePass signature header", e); + } + } + + PGPLiteralDataGenerator literalDataGenerator = null; + OutputStream literalDataOutput = null; + + if (includeDataInSignature) { + literalDataGenerator = new PGPLiteralDataGenerator(); + try { + literalDataOutput = literalDataGenerator.open(bcOutputStream, + PGPLiteralData.BINARY, + fileMessage); + } catch (IOException e1) { + throw new PGPException("Unable to generate Literal Data signature header", e1); + } + } + + try { + final FileInputStream fileInputStream = new FileInputStream(fileMessage); + + byte[] buffer = new byte[4096]; + int read; + + // update bytes in the streams + if (literalDataOutput != null) { + while ((read = fileInputStream.read(buffer)) > 0) { + literalDataOutput.write(buffer, 0, read); + signature.update(buffer, 0, read); + } + literalDataOutput.flush(); + } else { + + while ((read = fileInputStream.read(buffer)) > 0) { + signature.update(buffer, 0, read); + } + } + + // close generators and update signature + if (literalDataGenerator != null) { + literalDataGenerator.close(); + } + + signature.generate() + .encode(bcOutputStream); + + + if (compressedDataGenerator != null) { + compressedDataGenerator.close(); + } + } catch (Exception e) { + e.printStackTrace(); + } finally { + Sys.close(bcOutputStream); + Sys.close(outputStream); + Sys.close(literalDataOutput); + } + + return byteArrayOutputStream.toByteArray(); + } + + + /** + * Find private gpg key in InputStream, also closes the input stream + * + * @param inputStream + * the inputStream that contains the private (secret) key + * @param userId + * the user id + * + * @return the PGP secret key + */ + public static + List getSecretKeys(InputStream inputStream, String userId) throws PGPException { + // iterate over every private key in the key ring + PGPSecretKeyRingCollection secretKeyRings; + try { + secretKeyRings = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(inputStream), fingerprintCalculator); + } catch (IOException e) { + throw new PGPException("No private key found in stream!", e); + } finally { + Sys.close(inputStream); + } + + // look for the key ring that is used to authenticate our reporting facilities + Iterator secretKeys = secretKeyRings.getKeyRings(userId); + List pgpSecretKeys = new ArrayList(); + + // iterate over every private key in the ring + while (secretKeys.hasNext()) { + PGPSecretKeyRing secretKeyRing = secretKeys.next(); + PGPSecretKey tmpKey = secretKeyRing.getSecretKey(); + + if (tmpKey != null) { + pgpSecretKeys.add(tmpKey); + } + } + + if (!pgpSecretKeys.isEmpty()) { + return pgpSecretKeys; + } + + throw new PGPException("No private key found in stream!"); + } + + /** + * Creates the signature that will be used to PGP sign data + * + * @param secretKeys + * these are the secret keys + * @param password + * this is the password to unlock the secret key + * + * @return the signature used to sign data + * + * @throws PGPException + */ + private static + PGPSignatureGenerator createSignature(List secretKeys, + char[] password, + int signatureType, + boolean generateUserIdSubPacket) throws PGPException { + + PGPSecretKey secretKey = null; + for (int i = 0; i < secretKeys.size(); i++) { + secretKey = secretKeys.get(i); + + // we ONLY want the signing master key + if (!secretKey.isSigningKey() || !secretKey.isMasterKey()) { + secretKey = null; + } + } + + if (secretKey == null) { + throw new PGPException("Secret key is not the signing master key"); + } + +// System.err.println("Signing key = " + tmpKey.isSigningKey() +", Master key = " + tmpKey.isMasterKey() + ", UserId = " + +// userId ); + + if (password == null) { + password = new char[0]; + } + + PBESecretKeyDecryptor build = new BcPBESecretKeyDecryptorBuilder(digestCalculatorProvider).build(password); + + SecureRandom random = new SecureRandom(); + BcPGPContentSignerBuilder bcPGPContentSignerBuilder = new BcPGPContentSignerBuilder(secretKey.getPublicKey() + .getAlgorithm(), + PGPUtil.SHA1).setSecureRandom(random); + + PGPSignatureGenerator signature = new PGPSignatureGenerator(bcPGPContentSignerBuilder); + signature.init(signatureType, secretKey.extractPrivateKey(build)); + + Iterator userIds = secretKey.getPublicKey() + .getUserIDs(); + + // use the first userId that matches + if (userIds.hasNext()) { + if (generateUserIdSubPacket) { + PGPSignatureSubpacketGenerator subpacketGenerator = new PGPSignatureSubpacketGenerator(); + subpacketGenerator.setSignerUserID(false, (String) userIds.next()); + signature.setHashedSubpackets(subpacketGenerator.generate()); + } + else { + signature.setHashedSubpackets(null); + } + + return signature; + } + else { + throw new PGPException("Did not find specified userId"); + } + } + + + + + + + + + + + /** + * Decode a PGP public key block and return the keyring it represents. + */ + public static + PGPPublicKeyRing getKeyring(InputStream keyBlockStream) throws IOException { + + BcKeyFingerprintCalculator keyfp = new BcKeyFingerprintCalculator(); + + // PGPUtil.getDecoderStream() will detect ASCII-armor automatically and decode it, + // the PGPObject factory then knows how to read all the data in the encoded stream + PGPObjectFactory factory = new PGPObjectFactory(PGPUtil.getDecoderStream(keyBlockStream), keyfp); + + // these files should really just have one object in them, and that object should be a PGPPublicKeyRing. + Object o = factory.nextObject(); + if (o instanceof PGPPublicKeyRing) { + return (PGPPublicKeyRing) o; + } + throw new IllegalArgumentException("Input stream does not contain a PGP Public Key"); + } + + /** + * Get the first encryption key from the given keyring. + */ + public static + PGPPublicKey getEncryptionKey(PGPPublicKeyRing keyRing) { + if (keyRing == null) { + return null; + } + + // iterate over the keys on the ring, look for one which is suitable for encryption. + Iterator keys = keyRing.getPublicKeys(); + PGPPublicKey key; + while (keys.hasNext()) { + key = (PGPPublicKey) keys.next(); + if (key.isEncryptionKey()) { + return key; + } + } + + return null; + } + + /** + * Get the first decryption key from the given keyring. + */ + public + PGPSecretKey getDecryptionKey(PGPSecretKeyRing keyRing) { + if (keyRing == null) { + return null; + } + + // iterate over the keys on the ring, look for one which is suitable for encryption. + Iterator keys = keyRing.getSecretKeys(); + PGPSecretKey key; + while (keys.hasNext()) { + key = (PGPSecretKey) keys.next(); + if (key.isMasterKey()) { + return key; + } + } + + return null; + } + + + /** + * Encrypt plaintext message using public key from publickeyFile. + * + * @param message + * the message + * + * @return the string + */ + private + String encrypt(InputStream publicKeyInputStream, String message) throws PGPException, IOException, NoSuchProviderException { + // find the PGP key in the file + PGPPublicKey publicKey = findPublicGPGKey(publicKeyInputStream); + + if (publicKey == null) { + System.err.println("Did not find public GPG key"); + return null; + } + + + // Encode the string into bytes using utf-8 + byte[] utf8Bytes = message.getBytes(OS.UTF_8); + + ByteArrayOutputStream compressedOutput = new ByteArrayOutputStream(); + + // compress bytes with zip + PGPLiteralDataGenerator literalDataGenerator = new PGPLiteralDataGenerator(); + + // the reason why we compress here is GPG not being able to decrypt our message input but if we do not compress. + // I guess pkzip compression also encodes only to GPG-friendly characters. + PGPCompressedDataGenerator compressedDataGenerator = new PGPCompressedDataGenerator(CompressionAlgorithmTags.ZIP); + try { + OutputStream literalDataOutput = literalDataGenerator.open(compressedOutput, + PGPLiteralData.BINARY, + "_CONSOLE", + utf8Bytes.length, + new Date()); + // update bytes in the stream + literalDataOutput.write(utf8Bytes); + } catch (IOException e) { + // catch but close the streams in finally + throw e; + } finally { + compressedDataGenerator.close(); + Sys.close(compressedOutput); + } + + SecureRandom random = new SecureRandom(); + + // now we have zip-compressed bytes + byte[] compressedBytes = compressedOutput.toByteArray(); + + BcPGPDataEncryptorBuilder bcPGPDataEncryptorBuilder = new BcPGPDataEncryptorBuilder(PGPEncryptedData.CAST5) + .setWithIntegrityPacket(true) + .setSecureRandom(random); + + PGPEncryptedDataGenerator encryptedDataGenerator = new PGPEncryptedDataGenerator(bcPGPDataEncryptorBuilder); + + // use public key to encrypt data + + BcPublicKeyKeyEncryptionMethodGenerator encKeyGen = new BcPublicKeyKeyEncryptionMethodGenerator(publicKey) + .setSecureRandom(random); + + encryptedDataGenerator.addMethod(encKeyGen); + + // literalDataOutput --> compressedOutput --> ArmoredOutputStream --> ByteArrayOutputStream + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + ArmoredOutputStream armoredOut = new ArmoredOutputStream(byteArrayOutputStream); + OutputStream encryptedOutput = null; + try { + encryptedOutput = encryptedDataGenerator.open(armoredOut, compressedBytes.length); + encryptedOutput.write(compressedBytes); + } catch (IOException e) { + throw e; + } catch (PGPException e) { + throw e; + } finally { + Sys.close(encryptedOutput); + Sys.close(armoredOut); + } + String encrypted = new String(byteArrayOutputStream.toByteArray()); + + System.err.println("Message: " + message); + System.err.println("Encrypted: " + encrypted); + + return encrypted; + } + + /** + * Find public gpg key in InputStream. + * + * @param inputStream + * the input stream + * + * @return the PGP public key + */ + private static + PGPPublicKey findPublicGPGKey(InputStream inputStream) throws IOException, PGPException { + + // get all key rings in the input stream + PGPPublicKeyRingCollection publicKeyRingCollection = new PGPPublicKeyRingCollection(PGPUtil.getDecoderStream(inputStream), fingerprintCalculator); + + System.err.println("key ring size: " + publicKeyRingCollection.size()); + + Iterator keyRingIter = publicKeyRingCollection.getKeyRings(); + + // iterate over keyrings + while (keyRingIter.hasNext()) { + PGPPublicKeyRing keyRing = keyRingIter.next(); + Iterator keyIter = keyRing.getPublicKeys(); + // iterate over public keys in the key ring + while (keyIter.hasNext()) { + PGPPublicKey tmpKey = keyIter.next(); + + if (tmpKey == null) { + break; + } + + Iterator userIDs = tmpKey.getUserIDs(); + ArrayList strings = new ArrayList(); + while (userIDs.hasNext()) { + String next = userIDs.next(); + strings.add(next); + } + + System.err.println( + "Encryption key = " + tmpKey.isEncryptionKey() + ", Master key = " + tmpKey.isMasterKey() + ", UserId = " + + strings); + + // we need a master encryption key + if (tmpKey.isEncryptionKey() && tmpKey.isMasterKey()) { + return tmpKey; + } + } + } + throw new PGPException("No public key found!"); + } + + + + private static + void verify(final InputStream publicKeyInputStream, final byte[] signature) throws Exception { + PGPPublicKey publicKey = findPublicGPGKey(publicKeyInputStream); + + String text = new String(signature); + + Pattern regex = Pattern.compile( + "-----BEGIN PGP SIGNED MESSAGE-----\\r?\\n.*?\\r?\\n\\r?\\n(.*)\\r?\\n(-----BEGIN PGP SIGNATURE-----\\r?\\n.*-----END PGP SIGNATURE-----)", + Pattern.CANON_EQ | Pattern.DOTALL); + Matcher regexMatcher = regex.matcher(text); + if (regexMatcher.find()) { + String dataText = regexMatcher.group(1); + String signText = regexMatcher.group(2); + + ByteArrayInputStream dataIn = new ByteArrayInputStream(dataText.getBytes("UTF8")); + ByteArrayInputStream signIn = new ByteArrayInputStream(signText.getBytes("UTF8")); + + + InputStream signIn2 = PGPUtil.getDecoderStream(signIn); + + PGPObjectFactory pgpFact = new PGPObjectFactory(signIn2, new BcKeyFingerprintCalculator()); + PGPSignatureList p3 = null; + + Object o; + + try { + o = pgpFact.nextObject(); + if (o == null) { + throw new Exception(); + } + } catch (Exception ex) { + throw new Exception("Invalid input data"); + } + + if (o instanceof PGPCompressedData) { + PGPCompressedData c1 = (PGPCompressedData) o; + + pgpFact = new PGPObjectFactory(c1.getDataStream(), new BcKeyFingerprintCalculator()); + + p3 = (PGPSignatureList) pgpFact.nextObject(); + } + else { + p3 = (PGPSignatureList) o; + } + + +// PGPSignature sig = p3.get(0); +// PGPPublicKey key = KeyRing.getPublicKeyByID(sig.getKeyID()); +// +// if (key == null) +// throw new Exception("Cannot find key 0x" + Integer.toHexString((int) sig.getKeyID()).toUpperCase() + " in the pubring"); +// +// sig.initVerify(key, "BC"); +// +// while ((ch = dataIn.read()) >= 0) { +// sig.update((byte) ch); //TODO migliorabile con byte[] +// } +// +// if (sig.verify()) +// return new PrintablePGPPublicKey(key).toString(); +// else +// return null; + +// return verifyFile(dataIn, signIn); + + } + } + + + private + CryptoPGP() { + } + + public static + void main(String[] args) throws Exception { + InputStream privateKeyInputStream = new FileInputStream(new File("/home/user/dorkbox/sonatype_private.key")); + + byte[] textBytes = "hello".getBytes(OS.UTF_8); + + byte[] bytes = CryptoPGP.signGpgCompatible(privateKeyInputStream, "Dorkbox ", new char[0], textBytes); + +// String s = new String(hello); +// String s1 = s.replaceAll("\n", "\r\n"); +// byte[] bytes = s1.getBytes(OS.UTF_8); + +// +// String signed = new String(bytes); +// +// System.err.println("Message: " + new String(messageAsUtf8Bytes)); +// System.err.println("Signature: " + signed); +// +// return bytes; + +// String s2 = new String(bytes); + + +// InputStream publicKeyInputStream = new FileInputStream(new File("/home/user/dorkbox/sonatype_public.key")); +// cryptoPGP.verify(publicKeyInputStream, hello); + + + FileOutputStream fileOutputStream = new FileOutputStream(new File("/home/user/dorkbox/hello2.txt")); + fileOutputStream.write(textBytes); + fileOutputStream.flush(); + Sys.close(fileOutputStream); + + + FileOutputStream fileOutputStream1 = new FileOutputStream(new File("/home/user/dorkbox/hello2.txt.asc")); + fileOutputStream1.write(bytes); + fileOutputStream1.flush(); + Sys.close(fileOutputStream1); + } +} diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoRSA.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoRSA.java index 8bfc341..59ea5e7 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoRSA.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoRSA.java @@ -1,3 +1,18 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.util.crypto; import org.bouncycastle.crypto.AsymmetricBlockCipher; diff --git a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoSCrypt.java b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoSCrypt.java index 0309aa0..1de4eec 100644 --- a/Dorkbox-Util/src/dorkbox/util/crypto/CryptoSCrypt.java +++ b/Dorkbox-Util/src/dorkbox/util/crypto/CryptoSCrypt.java @@ -1,3 +1,18 @@ +/* + * Copyright 2010 dorkbox, llc + * + * 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 dorkbox.util.crypto; import java.security.SecureRandom; @@ -86,7 +101,7 @@ class CryptoSCrypt { String encrypt(char[] password, byte[] salt, int N, int r, int p, int dkLen) { // Note: this saves the char array in UTF-16 format of bytes. // can't use password after this as it's been changed to '*' - byte[] passwordBytes = Crypto.Util.charToBytesPassword(password); + byte[] passwordBytes = Crypto.charToBytesPassword_UTF16(password); byte[] derived = encrypt(passwordBytes, salt, N, r, p, dkLen); @@ -118,7 +133,7 @@ class CryptoSCrypt { boolean verify(char[] password, String hashed) { // Note: this saves the char array in UTF-16 format of bytes. // can't use password after this as it's been changed to '*' - byte[] passwordBytes = Crypto.Util.charToBytesPassword(password); + byte[] passwordBytes = Crypto.charToBytesPassword_UTF16(password); String[] parts = hashed.split("\\$");