diff --git a/.gitignore b/.gitignore index 1e8efe6c..86222cc6 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,5 @@ build out .DS_Store -local.properties \ No newline at end of file +local.properties +private.key diff --git a/library/src/main/java/com/pengrad/telegrambot/BotUtils.java b/library/src/main/java/com/pengrad/telegrambot/BotUtils.java index d2cd96c5..b89a4132 100644 --- a/library/src/main/java/com/pengrad/telegrambot/BotUtils.java +++ b/library/src/main/java/com/pengrad/telegrambot/BotUtils.java @@ -3,6 +3,9 @@ import com.google.gson.Gson; import com.pengrad.telegrambot.model.Update; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.Reader; /** @@ -21,4 +24,13 @@ public static Update parseUpdate(Reader reader) { return gson.fromJson(reader, Update.class); } + static byte[] getBytesFromInputStream(InputStream is) throws IOException { + ByteArrayOutputStream os = new ByteArrayOutputStream(); + byte[] buffer = new byte[0xFFFF]; + for (int len = is.read(buffer); len != -1; len = is.read(buffer)) { + os.write(buffer, 0, len); + } + return os.toByteArray(); + } + } diff --git a/library/src/main/java/com/pengrad/telegrambot/TelegramBot.java b/library/src/main/java/com/pengrad/telegrambot/TelegramBot.java index edc2129d..64e4d02b 100644 --- a/library/src/main/java/com/pengrad/telegrambot/TelegramBot.java +++ b/library/src/main/java/com/pengrad/telegrambot/TelegramBot.java @@ -8,6 +8,11 @@ import com.pengrad.telegrambot.request.BaseRequest; import com.pengrad.telegrambot.request.GetUpdates; import com.pengrad.telegrambot.response.BaseResponse; + +import java.io.InputStream; +import java.net.URL; +import java.net.URLConnection; + import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.logging.HttpLoggingInterceptor; @@ -44,6 +49,15 @@ public String getFullFilePath(File file) { return fileApi.getFullFilePath(file.filePath()); } + public byte[] getFileContent(File file) throws Exception { + String fileUrl = getFullFilePath(file); + URLConnection connection = new URL(fileUrl).openConnection(); + InputStream is = connection.getInputStream(); + byte[] data = BotUtils.getBytesFromInputStream(is); + is.close(); + return data; + } + public void setUpdatesListener(UpdatesListener listener) { setUpdatesListener(listener, new GetUpdates()); } diff --git a/library/src/main/java/com/pengrad/telegrambot/model/Message.java b/library/src/main/java/com/pengrad/telegrambot/model/Message.java index 99a12bdd..f22e4fb5 100644 --- a/library/src/main/java/com/pengrad/telegrambot/model/Message.java +++ b/library/src/main/java/com/pengrad/telegrambot/model/Message.java @@ -1,5 +1,7 @@ package com.pengrad.telegrambot.model; +import com.pengrad.telegrambot.passport.PassportData; + import java.io.Serializable; import java.util.Arrays; @@ -54,6 +56,7 @@ public class Message implements Serializable { private Invoice invoice; private SuccessfulPayment successful_payment; private String connected_website; + private PassportData passport_data; public Integer messageId() { return message_id; @@ -235,6 +238,10 @@ public String connectedWebsite() { return connected_website; } + public PassportData passportData() { + return passport_data; + } + @Override public boolean equals(Object o) { if (this == o) return true; @@ -307,7 +314,9 @@ public boolean equals(Object o) { if (invoice != null ? !invoice.equals(message.invoice) : message.invoice != null) return false; if (successful_payment != null ? !successful_payment.equals(message.successful_payment) : message.successful_payment != null) return false; - return connected_website != null ? connected_website.equals(message.connected_website) : message.connected_website == null; + if (connected_website != null ? !connected_website.equals(message.connected_website) : message.connected_website != null) + return false; + return passport_data != null ? passport_data.equals(message.passport_data) : message.passport_data == null; } @Override @@ -362,6 +371,7 @@ public String toString() { ", invoice=" + invoice + ", successful_payment=" + successful_payment + ", connected_website='" + connected_website + '\'' + + ", passport_data=" + passport_data + '}'; } } diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/Credentials.java b/library/src/main/java/com/pengrad/telegrambot/passport/Credentials.java new file mode 100644 index 00000000..b14f6ca0 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/Credentials.java @@ -0,0 +1,48 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 31 July 2018 + */ +public class Credentials implements Serializable { + private final static long serialVersionUID = 0L; + + private SecureData secure_data; + private String payload; + + public SecureData secureData() { + return secure_data; + } + + public String payload() { + return payload; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + Credentials that = (Credentials) o; + + if (secure_data != null ? !secure_data.equals(that.secure_data) : that.secure_data != null) return false; + return payload != null ? payload.equals(that.payload) : that.payload == null; + } + + @Override + public int hashCode() { + int result = secure_data != null ? secure_data.hashCode() : 0; + result = 31 * result + (payload != null ? payload.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "Credentials{" + + "secure_data=" + secure_data + + ", payload='" + payload + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/DataCredentials.java b/library/src/main/java/com/pengrad/telegrambot/passport/DataCredentials.java new file mode 100644 index 00000000..7a7708d6 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/DataCredentials.java @@ -0,0 +1,48 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 31 July 2018 + */ +public class DataCredentials implements Serializable { + private final static long serialVersionUID = 0L; + + private String data_hash; + private String secret; + + public String dataHash() { + return data_hash; + } + + public String secret() { + return secret; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + DataCredentials that = (DataCredentials) o; + + if (data_hash != null ? !data_hash.equals(that.data_hash) : that.data_hash != null) return false; + return secret != null ? secret.equals(that.secret) : that.secret == null; + } + + @Override + public int hashCode() { + int result = data_hash != null ? data_hash.hashCode() : 0; + result = 31 * result + (secret != null ? secret.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "DataCredentials{" + + "data_hash='" + data_hash + '\'' + + ", secret='" + secret + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/DecryptedData.java b/library/src/main/java/com/pengrad/telegrambot/passport/DecryptedData.java new file mode 100644 index 00000000..87b0e292 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/DecryptedData.java @@ -0,0 +1,11 @@ +package com.pengrad.telegrambot.passport; + +/** + * Stas Parshin + * 02 August 2018 + *

+ * Decrypted data from the data field in EncryptedPassportElement. + * Can be one of the following types: PersonalDetails, IdDocumentData, ResidentialAddress + */ +abstract public class DecryptedData { +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/EncryptedCredentials.java b/library/src/main/java/com/pengrad/telegrambot/passport/EncryptedCredentials.java new file mode 100644 index 00000000..01c8a2c5 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/EncryptedCredentials.java @@ -0,0 +1,62 @@ +package com.pengrad.telegrambot.passport; + +import com.pengrad.telegrambot.passport.decrypt.Decrypt; + +import java.io.Serializable; + +/** + * Stas Parshin + * 30 July 2018 + */ +public class EncryptedCredentials implements Serializable { + private final static long serialVersionUID = 0L; + + private String data; + private String hash; + private String secret; + + public Credentials decrypt(String privateKey) throws Exception { + return Decrypt.decryptCredentials(privateKey, data, hash, secret); + } + + public String data() { + return data; + } + + public String hash() { + return hash; + } + + public String secret() { + return secret; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + EncryptedCredentials that = (EncryptedCredentials) o; + + if (data != null ? !data.equals(that.data) : that.data != null) return false; + if (hash != null ? !hash.equals(that.hash) : that.hash != null) return false; + return secret != null ? secret.equals(that.secret) : that.secret == null; + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + (hash != null ? hash.hashCode() : 0); + result = 31 * result + (secret != null ? secret.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "EncryptedCredentials{" + + "data='" + data + '\'' + + ", hash='" + hash + '\'' + + ", secret='" + secret + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/EncryptedPassportElement.java b/library/src/main/java/com/pengrad/telegrambot/passport/EncryptedPassportElement.java new file mode 100644 index 00000000..a541945d --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/EncryptedPassportElement.java @@ -0,0 +1,157 @@ +package com.pengrad.telegrambot.passport; + +import com.google.gson.Gson; +import com.pengrad.telegrambot.TelegramBot; +import com.pengrad.telegrambot.model.File; +import com.pengrad.telegrambot.passport.decrypt.Decrypt; +import com.pengrad.telegrambot.request.GetFile; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * Stas Parshin + * 30 July 2018 + */ +public class EncryptedPassportElement implements Serializable { + private final static long serialVersionUID = 0L; + + public enum Type { + personal_details, passport, driver_license, identity_card, internal_passport, address, utility_bill, + bank_statement, rental_agreement, passport_registration, temporary_registration, phone_number, email + } + + private Type type; + private String data; + private String phone_number; + private String email; + private PassportFile[] files; + private PassportFile front_side; + private PassportFile reverse_side; + private PassportFile selfie; + + public DecryptedData decryptData(Credentials credentials) throws Exception { + Class clazz = dataClass(); + if (clazz == null || data == null) return null; + SecureValue secureValue = credentials.secureData().ofType(type); + DataCredentials dataCredentials = secureValue.data(); + String dataStr = Decrypt.decryptData(data, dataCredentials.dataHash(), dataCredentials.secret()); + return new Gson().fromJson(dataStr, clazz); + } + + public byte[] decryptFile(PassportFile passportFile, FileCredentials fileCredentials, TelegramBot bot) throws Exception { + File file = bot.execute(new GetFile(passportFile.fileId())).file(); + byte[] fileData = bot.getFileContent(file); + return decryptFile(fileData, fileCredentials); + } + + public byte[] decryptFile(PassportFile passportFile, Credentials credentials, TelegramBot bot) throws Exception { + FileCredentials fileCredentials = findFileCredentials(passportFile, credentials); + if (fileCredentials == null) { + throw new IllegalArgumentException("Don't have file credentials for " + passportFile); + } + return decryptFile(passportFile, fileCredentials, bot); + } + + public byte[] decryptFile(byte[] fileData, FileCredentials fileCredentials) throws Exception { + return Decrypt.decryptFile(fileData, fileCredentials.fileHash(), fileCredentials.secret()); + } + + private FileCredentials findFileCredentials(PassportFile passportFile, Credentials credentials) { + SecureValue secureValue = credentials.secureData().ofType(type); + if (passportFile.equals(front_side)) return secureValue.frontSide(); + if (passportFile.equals(reverse_side)) return secureValue.reverseSide(); + if (passportFile.equals(selfie)) return secureValue.selfie(); + for (int i = 0; i < files.length; i++) { + if (passportFile.equals(files[i])) return secureValue.files()[i]; + } + return null; + } + + private Class dataClass() { + if (Type.personal_details == type) return PersonalDetails.class; + if (Type.passport == type) return IdDocumentData.class; + if (Type.internal_passport == type) return IdDocumentData.class; + if (Type.driver_license == type) return IdDocumentData.class; + if (Type.identity_card == type) return IdDocumentData.class; + if (Type.address == type) return ResidentialAddress.class; + return null; + } + + public Type type() { + return type; + } + + public String data() { + return data; + } + + public String phoneNumber() { + return phone_number; + } + + public String email() { + return email; + } + + public PassportFile[] files() { + return files; + } + + public PassportFile frontSide() { + return front_side; + } + + public PassportFile reverseSide() { + return reverse_side; + } + + public PassportFile selfie() { + return selfie; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + EncryptedPassportElement that = (EncryptedPassportElement) o; + + if (type != that.type) return false; + if (data != null ? !data.equals(that.data) : that.data != null) return false; + if (phone_number != null ? !phone_number.equals(that.phone_number) : that.phone_number != null) return false; + if (email != null ? !email.equals(that.email) : that.email != null) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(files, that.files)) return false; + if (front_side != null ? !front_side.equals(that.front_side) : that.front_side != null) return false; + if (reverse_side != null ? !reverse_side.equals(that.reverse_side) : that.reverse_side != null) return false; + return selfie != null ? selfie.equals(that.selfie) : that.selfie == null; + } + + @Override + public int hashCode() { + int result = type != null ? type.hashCode() : 0; + result = 31 * result + (data != null ? data.hashCode() : 0); + result = 31 * result + (phone_number != null ? phone_number.hashCode() : 0); + result = 31 * result + (email != null ? email.hashCode() : 0); + result = 31 * result + Arrays.hashCode(files); + result = 31 * result + (front_side != null ? front_side.hashCode() : 0); + result = 31 * result + (reverse_side != null ? reverse_side.hashCode() : 0); + result = 31 * result + (selfie != null ? selfie.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "EncryptedPassportElement{" + + "type=" + type + + ", data='" + data + '\'' + + ", phone_number='" + phone_number + '\'' + + ", email='" + email + '\'' + + ", files=" + Arrays.toString(files) + + ", front_side=" + front_side + + ", reverse_side=" + reverse_side + + ", selfie=" + selfie + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/FileCredentials.java b/library/src/main/java/com/pengrad/telegrambot/passport/FileCredentials.java new file mode 100644 index 00000000..db2a1735 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/FileCredentials.java @@ -0,0 +1,48 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 31 July 2018 + */ +public class FileCredentials implements Serializable { + private final static long serialVersionUID = 0L; + + private String file_hash; + private String secret; + + public String fileHash() { + return file_hash; + } + + public String secret() { + return secret; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + FileCredentials that = (FileCredentials) o; + + if (file_hash != null ? !file_hash.equals(that.file_hash) : that.file_hash != null) return false; + return secret != null ? secret.equals(that.secret) : that.secret == null; + } + + @Override + public int hashCode() { + int result = file_hash != null ? file_hash.hashCode() : 0; + result = 31 * result + (secret != null ? secret.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "FileCredentials{" + + "file_hash='" + file_hash + '\'' + + ", secret='" + secret + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/IdDocumentData.java b/library/src/main/java/com/pengrad/telegrambot/passport/IdDocumentData.java new file mode 100644 index 00000000..8558ff15 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/IdDocumentData.java @@ -0,0 +1,48 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 02 August 2018 + */ +public class IdDocumentData extends DecryptedData implements Serializable { + private final static long serialVersionUID = 0L; + + private String document_no; + private String expiry_date; + + public String documentNo() { + return document_no; + } + + public String expiryDate() { + return expiry_date; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + IdDocumentData that = (IdDocumentData) o; + + if (document_no != null ? !document_no.equals(that.document_no) : that.document_no != null) return false; + return expiry_date != null ? expiry_date.equals(that.expiry_date) : that.expiry_date == null; + } + + @Override + public int hashCode() { + int result = document_no != null ? document_no.hashCode() : 0; + result = 31 * result + (expiry_date != null ? expiry_date.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "IdDocumentData{" + + "document_no='" + document_no + '\'' + + ", expiry_date='" + expiry_date + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/PassportData.java b/library/src/main/java/com/pengrad/telegrambot/passport/PassportData.java new file mode 100644 index 00000000..0092d15b --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/PassportData.java @@ -0,0 +1,50 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * Stas Parshin + * 30 July 2018 + */ +public class PassportData implements Serializable { + private final static long serialVersionUID = 0L; + + private EncryptedPassportElement[] data; + private EncryptedCredentials credentials; + + public EncryptedPassportElement[] data() { + return data; + } + + public EncryptedCredentials credentials() { + return credentials; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PassportData that = (PassportData) o; + + // Probably incorrect - comparing Object[] arrays with Arrays.equals + if (!Arrays.equals(data, that.data)) return false; + return credentials != null ? credentials.equals(that.credentials) : that.credentials == null; + } + + @Override + public int hashCode() { + int result = Arrays.hashCode(data); + result = 31 * result + (credentials != null ? credentials.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PassportData{" + + "data=" + Arrays.toString(data) + + ", credentials=" + credentials + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/PassportElementError.java b/library/src/main/java/com/pengrad/telegrambot/passport/PassportElementError.java new file mode 100644 index 00000000..5465a02f --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/PassportElementError.java @@ -0,0 +1,21 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 30 July 2018 + */ +public abstract class PassportElementError implements Serializable { + private final static long serialVersionUID = 0L; + + private final String source; + private final String type; + private final String message; + + public PassportElementError(String source, String type, String message) { + this.source = source; + this.type = type; + this.message = message; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/PassportElementErrorDataField.java b/library/src/main/java/com/pengrad/telegrambot/passport/PassportElementErrorDataField.java new file mode 100644 index 00000000..06410f02 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/PassportElementErrorDataField.java @@ -0,0 +1,20 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 30 July 2018 + */ +public class PassportElementErrorDataField extends PassportElementError implements Serializable { + private final static long serialVersionUID = 0L; + + private final String field_name; + private final String data_hash; + + public PassportElementErrorDataField(String type, String fieldName, String dataHash, String message) { + super("data", type, message); + field_name = fieldName; + data_hash = dataHash; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/PassportFile.java b/library/src/main/java/com/pengrad/telegrambot/passport/PassportFile.java new file mode 100644 index 00000000..5f4a7859 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/PassportFile.java @@ -0,0 +1,53 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 30 July 2018 + */ +public class PassportFile implements Serializable { + private final static long serialVersionUID = 0L; + + private String file_id; + private Integer file_size; + private Integer file_date; + + public String fileId() { + return file_id; + } + + public Integer fileSize() { + return file_size; + } + + public Integer fileDate() { + return file_date; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PassportFile that = (PassportFile) o; + + if (file_id != null ? !file_id.equals(that.file_id) : that.file_id != null) return false; + if (file_size != null ? !file_size.equals(that.file_size) : that.file_size != null) return false; + return file_date != null ? file_date.equals(that.file_date) : that.file_date == null; + } + + @Override + public int hashCode() { + return file_id != null ? file_id.hashCode() : 0; + } + + @Override + public String toString() { + return "PassportFile{" + + "file_id='" + file_id + '\'' + + ", file_size=" + file_size + + ", file_date=" + file_date + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/PersonalDetails.java b/library/src/main/java/com/pengrad/telegrambot/passport/PersonalDetails.java new file mode 100644 index 00000000..51dfc777 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/PersonalDetails.java @@ -0,0 +1,80 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 02 August 2018 + */ +public class PersonalDetails extends DecryptedData implements Serializable { + private final static long serialVersionUID = 0L; + + private String first_name; + private String last_name; + private String birth_date; + private String gender; + private String country_code; + private String residence_country_code; + + public String firstName() { + return first_name; + } + + public String lastName() { + return last_name; + } + + public String birthDate() { + return birth_date; + } + + public String gender() { + return gender; + } + + public String countryCode() { + return country_code; + } + + public String residenceCountryCode() { + return residence_country_code; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + PersonalDetails that = (PersonalDetails) o; + + if (first_name != null ? !first_name.equals(that.first_name) : that.first_name != null) return false; + if (last_name != null ? !last_name.equals(that.last_name) : that.last_name != null) return false; + if (birth_date != null ? !birth_date.equals(that.birth_date) : that.birth_date != null) return false; + if (gender != null ? !gender.equals(that.gender) : that.gender != null) return false; + if (country_code != null ? !country_code.equals(that.country_code) : that.country_code != null) return false; + return residence_country_code != null ? residence_country_code.equals(that.residence_country_code) : that.residence_country_code == null; + } + + @Override + public int hashCode() { + int result = first_name != null ? first_name.hashCode() : 0; + result = 31 * result + (last_name != null ? last_name.hashCode() : 0); + result = 31 * result + (birth_date != null ? birth_date.hashCode() : 0); + result = 31 * result + (gender != null ? gender.hashCode() : 0); + result = 31 * result + (country_code != null ? country_code.hashCode() : 0); + result = 31 * result + (residence_country_code != null ? residence_country_code.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "PersonalDetails{" + + "first_name='" + first_name + '\'' + + ", last_name='" + last_name + '\'' + + ", birth_date='" + birth_date + '\'' + + ", gender='" + gender + '\'' + + ", country_code='" + country_code + '\'' + + ", residence_country_code='" + residence_country_code + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/ResidentialAddress.java b/library/src/main/java/com/pengrad/telegrambot/passport/ResidentialAddress.java new file mode 100644 index 00000000..2f46559e --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/ResidentialAddress.java @@ -0,0 +1,80 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; + +/** + * Stas Parshin + * 02 August 2018 + */ +public class ResidentialAddress extends DecryptedData implements Serializable { + private final static long serialVersionUID = 0L; + + private String street_line1; + private String street_line2; + private String city; + private String state; + private String country_code; + private String post_code; + + public String streetLine1() { + return street_line1; + } + + public String streetLine2() { + return street_line2; + } + + public String city() { + return city; + } + + public String state() { + return state; + } + + public String countryCode() { + return country_code; + } + + public String postCode() { + return post_code; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + ResidentialAddress that = (ResidentialAddress) o; + + if (street_line1 != null ? !street_line1.equals(that.street_line1) : that.street_line1 != null) return false; + if (street_line2 != null ? !street_line2.equals(that.street_line2) : that.street_line2 != null) return false; + if (city != null ? !city.equals(that.city) : that.city != null) return false; + if (state != null ? !state.equals(that.state) : that.state != null) return false; + if (country_code != null ? !country_code.equals(that.country_code) : that.country_code != null) return false; + return post_code != null ? post_code.equals(that.post_code) : that.post_code == null; + } + + @Override + public int hashCode() { + int result = street_line1 != null ? street_line1.hashCode() : 0; + result = 31 * result + (street_line2 != null ? street_line2.hashCode() : 0); + result = 31 * result + (city != null ? city.hashCode() : 0); + result = 31 * result + (state != null ? state.hashCode() : 0); + result = 31 * result + (country_code != null ? country_code.hashCode() : 0); + result = 31 * result + (post_code != null ? post_code.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "ResidentialAddress{" + + "street_line1='" + street_line1 + '\'' + + ", street_line2='" + street_line2 + '\'' + + ", city='" + city + '\'' + + ", state='" + state + '\'' + + ", country_code='" + country_code + '\'' + + ", post_code='" + post_code + '\'' + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/SecureData.java b/library/src/main/java/com/pengrad/telegrambot/passport/SecureData.java new file mode 100644 index 00000000..e13c7744 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/SecureData.java @@ -0,0 +1,135 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; +import java.lang.reflect.Field; + +/** + * Stas Parshin + * 31 July 2018 + */ +public class SecureData implements Serializable { + private final static long serialVersionUID = 0L; + + private SecureValue + personal_details, + passport, + internal_passport, + driver_license, + identity_card, + address, + utility_bill, + bank_statement, + rental_agreement, + passport_registration, + temporary_registration; + + public SecureValue ofType(EncryptedPassportElement.Type type) { + try { + Field field = getClass().getDeclaredField(type.name()); + return (SecureValue) field.get(this); + } catch (Exception e) { + return null; + } + } + + public SecureValue personalDetails() { + return personal_details; + } + + public SecureValue passport() { + return passport; + } + + public SecureValue internalPassport() { + return internal_passport; + } + + public SecureValue driverLicense() { + return driver_license; + } + + public SecureValue identityCard() { + return identity_card; + } + + public SecureValue address() { + return address; + } + + public SecureValue utilityBill() { + return utility_bill; + } + + public SecureValue bankStatement() { + return bank_statement; + } + + public SecureValue rentalAgreement() { + return rental_agreement; + } + + public SecureValue passportRegistration() { + return passport_registration; + } + + public SecureValue temporaryRegistration() { + return temporary_registration; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SecureData that = (SecureData) o; + + if (personal_details != null ? !personal_details.equals(that.personal_details) : that.personal_details != null) + return false; + if (passport != null ? !passport.equals(that.passport) : that.passport != null) return false; + if (internal_passport != null ? !internal_passport.equals(that.internal_passport) : that.internal_passport != null) + return false; + if (driver_license != null ? !driver_license.equals(that.driver_license) : that.driver_license != null) return false; + if (identity_card != null ? !identity_card.equals(that.identity_card) : that.identity_card != null) return false; + if (address != null ? !address.equals(that.address) : that.address != null) return false; + if (utility_bill != null ? !utility_bill.equals(that.utility_bill) : that.utility_bill != null) return false; + if (bank_statement != null ? !bank_statement.equals(that.bank_statement) : that.bank_statement != null) return false; + if (rental_agreement != null ? !rental_agreement.equals(that.rental_agreement) : that.rental_agreement != null) + return false; + if (passport_registration != null ? !passport_registration.equals(that.passport_registration) : that.passport_registration != null) + return false; + return temporary_registration != null ? temporary_registration.equals(that.temporary_registration) : that.temporary_registration == null; + } + + @Override + public int hashCode() { + int result = personal_details != null ? personal_details.hashCode() : 0; + result = 31 * result + (passport != null ? passport.hashCode() : 0); + result = 31 * result + (internal_passport != null ? internal_passport.hashCode() : 0); + result = 31 * result + (driver_license != null ? driver_license.hashCode() : 0); + result = 31 * result + (identity_card != null ? identity_card.hashCode() : 0); + result = 31 * result + (address != null ? address.hashCode() : 0); + result = 31 * result + (utility_bill != null ? utility_bill.hashCode() : 0); + result = 31 * result + (bank_statement != null ? bank_statement.hashCode() : 0); + result = 31 * result + (rental_agreement != null ? rental_agreement.hashCode() : 0); + result = 31 * result + (passport_registration != null ? passport_registration.hashCode() : 0); + result = 31 * result + (temporary_registration != null ? temporary_registration.hashCode() : 0); + return result; + } + + @Override + public String toString() { + return "SecureData{" + + "personal_details=" + personal_details + + ", passport=" + passport + + ", internal_passport=" + internal_passport + + ", driver_license=" + driver_license + + ", identity_card=" + identity_card + + ", address=" + address + + ", utility_bill=" + utility_bill + + ", bank_statement=" + bank_statement + + ", rental_agreement=" + rental_agreement + + ", passport_registration=" + passport_registration + + ", temporary_registration=" + temporary_registration + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/SecureValue.java b/library/src/main/java/com/pengrad/telegrambot/passport/SecureValue.java new file mode 100644 index 00000000..e2bbc61c --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/SecureValue.java @@ -0,0 +1,74 @@ +package com.pengrad.telegrambot.passport; + +import java.io.Serializable; +import java.util.Arrays; + +/** + * Stas Parshin + * 31 July 2018 + */ +public class SecureValue implements Serializable { + private final static long serialVersionUID = 0L; + + private DataCredentials data; + private FileCredentials front_side; + private FileCredentials reverse_side; + private FileCredentials selfie; + private FileCredentials[] files; + + public DataCredentials data() { + return data; + } + + public FileCredentials frontSide() { + return front_side; + } + + public FileCredentials reverseSide() { + return reverse_side; + } + + public FileCredentials selfie() { + return selfie; + } + + public FileCredentials[] files() { + return files; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + SecureValue that = (SecureValue) o; + + if (data != null ? !data.equals(that.data) : that.data != null) return false; + if (front_side != null ? !front_side.equals(that.front_side) : that.front_side != null) return false; + if (reverse_side != null ? !reverse_side.equals(that.reverse_side) : that.reverse_side != null) return false; + if (selfie != null ? !selfie.equals(that.selfie) : that.selfie != null) return false; + // Probably incorrect - comparing Object[] arrays with Arrays.equals + return Arrays.equals(files, that.files); + } + + @Override + public int hashCode() { + int result = data != null ? data.hashCode() : 0; + result = 31 * result + (front_side != null ? front_side.hashCode() : 0); + result = 31 * result + (reverse_side != null ? reverse_side.hashCode() : 0); + result = 31 * result + (selfie != null ? selfie.hashCode() : 0); + result = 31 * result + Arrays.hashCode(files); + return result; + } + + @Override + public String toString() { + return "SecureValue{" + + "data=" + data + + ", front_side=" + front_side + + ", reverse_side=" + reverse_side + + ", selfie=" + selfie + + ", files=" + Arrays.toString(files) + + '}'; + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/SetPassportDataErrors.java b/library/src/main/java/com/pengrad/telegrambot/passport/SetPassportDataErrors.java new file mode 100644 index 00000000..9ff6a873 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/SetPassportDataErrors.java @@ -0,0 +1,16 @@ +package com.pengrad.telegrambot.passport; + +import com.pengrad.telegrambot.request.BaseRequest; +import com.pengrad.telegrambot.response.BaseResponse; + +/** + * Stas Parshin + * 30 July 2018 + */ +public class SetPassportDataErrors extends BaseRequest { + + public SetPassportDataErrors(int userId, PassportElementError... errors) { + super(BaseResponse.class); + add("user_id", userId).add("errors", serialize(errors)); + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Aes256Cbc.java b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Aes256Cbc.java new file mode 100644 index 00000000..37cd9588 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Aes256Cbc.java @@ -0,0 +1,711 @@ +package com.pengrad.telegrambot.passport.decrypt; + +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.OutputStream; + +/** + * Stas Parshin + * 31 July 2018 + */ +class Aes256Cbc { + + private final Cbc cbc; + private final ByteArrayOutputStream baos; + + public Aes256Cbc(byte[] key, byte[] iv) { + baos = new ByteArrayOutputStream(); + cbc = new Cbc(iv, key, baos); + } + + public byte[] decrypt(byte[] data) throws Exception { + cbc.decrypt(data); + cbc.finishDecryption(); + return baos.toByteArray(); + } + + static final class Aes256 { + + /** + * Number of {@code byte}s needed for 32 bit words. + */ + private static final int WORD_SIZE = 4; + + /** + * Number of {@code byte}s for a data block. The size is identical to the size of the internal + * state, that is needed in encryption or decryption. + *

+ * The state can be viewed as a square matrix, modeled as a list of column vectors. + *

+ */ + private static final int BLOCK_SIZE = 16; + + /** + * key size in {@code byte}s + */ + private static final int KEY_SIZE = 32; + + /** + * number of rounds + */ + private static final int ROUNDS = 14; + + /** + * Size of the expanded key. For each round a {@code BLOCK_SIZE} block will be needed. Before + * the first round another block will be needed. + */ + private static final int EXPANDED_KEY_SIZE = (ROUNDS + 1) * BLOCK_SIZE; + + /** + * expanded key + */ + private final byte[] _expandedKey; + + /** + * state needed between rounds in en- or decryption + */ + private final byte[] _tmp; + + /** + * Permutation of {@code byte}s. The s-box permutation was specified in the reference document. + */ + private final byte[] _sBox = {(byte) 0x63, (byte) 0x7c, (byte) 0x77, + (byte) 0x7b, (byte) 0xf2, (byte) 0x6b, (byte) 0x6f, (byte) 0xc5, + (byte) 0x30, (byte) 0x01, (byte) 0x67, (byte) 0x2b, (byte) 0xfe, + (byte) 0xd7, (byte) 0xab, (byte) 0x76, (byte) 0xca, (byte) 0x82, + (byte) 0xc9, (byte) 0x7d, (byte) 0xfa, (byte) 0x59, (byte) 0x47, + (byte) 0xf0, (byte) 0xad, (byte) 0xd4, (byte) 0xa2, (byte) 0xaf, + (byte) 0x9c, (byte) 0xa4, (byte) 0x72, (byte) 0xc0, (byte) 0xb7, + (byte) 0xfd, (byte) 0x93, (byte) 0x26, (byte) 0x36, (byte) 0x3f, + (byte) 0xf7, (byte) 0xcc, (byte) 0x34, (byte) 0xa5, (byte) 0xe5, + (byte) 0xf1, (byte) 0x71, (byte) 0xd8, (byte) 0x31, (byte) 0x15, + (byte) 0x04, (byte) 0xc7, (byte) 0x23, (byte) 0xc3, (byte) 0x18, + (byte) 0x96, (byte) 0x05, (byte) 0x9a, (byte) 0x07, (byte) 0x12, + (byte) 0x80, (byte) 0xe2, (byte) 0xeb, (byte) 0x27, (byte) 0xb2, + (byte) 0x75, (byte) 0x09, (byte) 0x83, (byte) 0x2c, (byte) 0x1a, + (byte) 0x1b, (byte) 0x6e, (byte) 0x5a, (byte) 0xa0, (byte) 0x52, + (byte) 0x3b, (byte) 0xd6, (byte) 0xb3, (byte) 0x29, (byte) 0xe3, + (byte) 0x2f, (byte) 0x84, (byte) 0x53, (byte) 0xd1, (byte) 0x00, + (byte) 0xed, (byte) 0x20, (byte) 0xfc, (byte) 0xb1, (byte) 0x5b, + (byte) 0x6a, (byte) 0xcb, (byte) 0xbe, (byte) 0x39, (byte) 0x4a, + (byte) 0x4c, (byte) 0x58, (byte) 0xcf, (byte) 0xd0, (byte) 0xef, + (byte) 0xaa, (byte) 0xfb, (byte) 0x43, (byte) 0x4d, (byte) 0x33, + (byte) 0x85, (byte) 0x45, (byte) 0xf9, (byte) 0x02, (byte) 0x7f, + (byte) 0x50, (byte) 0x3c, (byte) 0x9f, (byte) 0xa8, (byte) 0x51, + (byte) 0xa3, (byte) 0x40, (byte) 0x8f, (byte) 0x92, (byte) 0x9d, + (byte) 0x38, (byte) 0xf5, (byte) 0xbc, (byte) 0xb6, (byte) 0xda, + (byte) 0x21, (byte) 0x10, (byte) 0xff, (byte) 0xf3, (byte) 0xd2, + (byte) 0xcd, (byte) 0x0c, (byte) 0x13, (byte) 0xec, (byte) 0x5f, + (byte) 0x97, (byte) 0x44, (byte) 0x17, (byte) 0xc4, (byte) 0xa7, + (byte) 0x7e, (byte) 0x3d, (byte) 0x64, (byte) 0x5d, (byte) 0x19, + (byte) 0x73, (byte) 0x60, (byte) 0x81, (byte) 0x4f, (byte) 0xdc, + (byte) 0x22, (byte) 0x2a, (byte) 0x90, (byte) 0x88, (byte) 0x46, + (byte) 0xee, (byte) 0xb8, (byte) 0x14, (byte) 0xde, (byte) 0x5e, + (byte) 0x0b, (byte) 0xdb, (byte) 0xe0, (byte) 0x32, (byte) 0x3a, + (byte) 0x0a, (byte) 0x49, (byte) 0x06, (byte) 0x24, (byte) 0x5c, + (byte) 0xc2, (byte) 0xd3, (byte) 0xac, (byte) 0x62, (byte) 0x91, + (byte) 0x95, (byte) 0xe4, (byte) 0x79, (byte) 0xe7, (byte) 0xc8, + (byte) 0x37, (byte) 0x6d, (byte) 0x8d, (byte) 0xd5, (byte) 0x4e, + (byte) 0xa9, (byte) 0x6c, (byte) 0x56, (byte) 0xf4, (byte) 0xea, + (byte) 0x65, (byte) 0x7a, (byte) 0xae, (byte) 0x08, (byte) 0xba, + (byte) 0x78, (byte) 0x25, (byte) 0x2e, (byte) 0x1c, (byte) 0xa6, + (byte) 0xb4, (byte) 0xc6, (byte) 0xe8, (byte) 0xdd, (byte) 0x74, + (byte) 0x1f, (byte) 0x4b, (byte) 0xbd, (byte) 0x8b, (byte) 0x8a, + (byte) 0x70, (byte) 0x3e, (byte) 0xb5, (byte) 0x66, (byte) 0x48, + (byte) 0x03, (byte) 0xf6, (byte) 0x0e, (byte) 0x61, (byte) 0x35, + (byte) 0x57, (byte) 0xb9, (byte) 0x86, (byte) 0xc1, (byte) 0x1d, + (byte) 0x9e, (byte) 0xe1, (byte) 0xf8, (byte) 0x98, (byte) 0x11, + (byte) 0x69, (byte) 0xd9, (byte) 0x8e, (byte) 0x94, (byte) 0x9b, + (byte) 0x1e, (byte) 0x87, (byte) 0xe9, (byte) 0xce, (byte) 0x55, + (byte) 0x28, (byte) 0xdf, (byte) 0x8c, (byte) 0xa1, (byte) 0x89, + (byte) 0x0d, (byte) 0xbf, (byte) 0xe6, (byte) 0x42, (byte) 0x68, + (byte) 0x41, (byte) 0x99, (byte) 0x2d, (byte) 0x0f, (byte) 0xb0, + (byte) 0x54, (byte) 0xbb, (byte) 0x16}; + + /** + * Inverse of the s-box permutation. + */ + private final byte[] _invSBox = {(byte) 0x52, (byte) 0x09, (byte) 0x6a, + (byte) 0xd5, (byte) 0x30, (byte) 0x36, (byte) 0xa5, (byte) 0x38, + (byte) 0xbf, (byte) 0x40, (byte) 0xa3, (byte) 0x9e, (byte) 0x81, + (byte) 0xf3, (byte) 0xd7, (byte) 0xfb, (byte) 0x7c, (byte) 0xe3, + (byte) 0x39, (byte) 0x82, (byte) 0x9b, (byte) 0x2f, (byte) 0xff, + (byte) 0x87, (byte) 0x34, (byte) 0x8e, (byte) 0x43, (byte) 0x44, + (byte) 0xc4, (byte) 0xde, (byte) 0xe9, (byte) 0xcb, (byte) 0x54, + (byte) 0x7b, (byte) 0x94, (byte) 0x32, (byte) 0xa6, (byte) 0xc2, + (byte) 0x23, (byte) 0x3d, (byte) 0xee, (byte) 0x4c, (byte) 0x95, + (byte) 0x0b, (byte) 0x42, (byte) 0xfa, (byte) 0xc3, (byte) 0x4e, + (byte) 0x08, (byte) 0x2e, (byte) 0xa1, (byte) 0x66, (byte) 0x28, + (byte) 0xd9, (byte) 0x24, (byte) 0xb2, (byte) 0x76, (byte) 0x5b, + (byte) 0xa2, (byte) 0x49, (byte) 0x6d, (byte) 0x8b, (byte) 0xd1, + (byte) 0x25, (byte) 0x72, (byte) 0xf8, (byte) 0xf6, (byte) 0x64, + (byte) 0x86, (byte) 0x68, (byte) 0x98, (byte) 0x16, (byte) 0xd4, + (byte) 0xa4, (byte) 0x5c, (byte) 0xcc, (byte) 0x5d, (byte) 0x65, + (byte) 0xb6, (byte) 0x92, (byte) 0x6c, (byte) 0x70, (byte) 0x48, + (byte) 0x50, (byte) 0xfd, (byte) 0xed, (byte) 0xb9, (byte) 0xda, + (byte) 0x5e, (byte) 0x15, (byte) 0x46, (byte) 0x57, (byte) 0xa7, + (byte) 0x8d, (byte) 0x9d, (byte) 0x84, (byte) 0x90, (byte) 0xd8, + (byte) 0xab, (byte) 0x00, (byte) 0x8c, (byte) 0xbc, (byte) 0xd3, + (byte) 0x0a, (byte) 0xf7, (byte) 0xe4, (byte) 0x58, (byte) 0x05, + (byte) 0xb8, (byte) 0xb3, (byte) 0x45, (byte) 0x06, (byte) 0xd0, + (byte) 0x2c, (byte) 0x1e, (byte) 0x8f, (byte) 0xca, (byte) 0x3f, + (byte) 0x0f, (byte) 0x02, (byte) 0xc1, (byte) 0xaf, (byte) 0xbd, + (byte) 0x03, (byte) 0x01, (byte) 0x13, (byte) 0x8a, (byte) 0x6b, + (byte) 0x3a, (byte) 0x91, (byte) 0x11, (byte) 0x41, (byte) 0x4f, + (byte) 0x67, (byte) 0xdc, (byte) 0xea, (byte) 0x97, (byte) 0xf2, + (byte) 0xcf, (byte) 0xce, (byte) 0xf0, (byte) 0xb4, (byte) 0xe6, + (byte) 0x73, (byte) 0x96, (byte) 0xac, (byte) 0x74, (byte) 0x22, + (byte) 0xe7, (byte) 0xad, (byte) 0x35, (byte) 0x85, (byte) 0xe2, + (byte) 0xf9, (byte) 0x37, (byte) 0xe8, (byte) 0x1c, (byte) 0x75, + (byte) 0xdf, (byte) 0x6e, (byte) 0x47, (byte) 0xf1, (byte) 0x1a, + (byte) 0x71, (byte) 0x1d, (byte) 0x29, (byte) 0xc5, (byte) 0x89, + (byte) 0x6f, (byte) 0xb7, (byte) 0x62, (byte) 0x0e, (byte) 0xaa, + (byte) 0x18, (byte) 0xbe, (byte) 0x1b, (byte) 0xfc, (byte) 0x56, + (byte) 0x3e, (byte) 0x4b, (byte) 0xc6, (byte) 0xd2, (byte) 0x79, + (byte) 0x20, (byte) 0x9a, (byte) 0xdb, (byte) 0xc0, (byte) 0xfe, + (byte) 0x78, (byte) 0xcd, (byte) 0x5a, (byte) 0xf4, (byte) 0x1f, + (byte) 0xdd, (byte) 0xa8, (byte) 0x33, (byte) 0x88, (byte) 0x07, + (byte) 0xc7, (byte) 0x31, (byte) 0xb1, (byte) 0x12, (byte) 0x10, + (byte) 0x59, (byte) 0x27, (byte) 0x80, (byte) 0xec, (byte) 0x5f, + (byte) 0x60, (byte) 0x51, (byte) 0x7f, (byte) 0xa9, (byte) 0x19, + (byte) 0xb5, (byte) 0x4a, (byte) 0x0d, (byte) 0x2d, (byte) 0xe5, + (byte) 0x7a, (byte) 0x9f, (byte) 0x93, (byte) 0xc9, (byte) 0x9c, + (byte) 0xef, (byte) 0xa0, (byte) 0xe0, (byte) 0x3b, (byte) 0x4d, + (byte) 0xae, (byte) 0x2a, (byte) 0xf5, (byte) 0xb0, (byte) 0xc8, + (byte) 0xeb, (byte) 0xbb, (byte) 0x3c, (byte) 0x83, (byte) 0x53, + (byte) 0x99, (byte) 0x61, (byte) 0x17, (byte) 0x2b, (byte) 0x04, + (byte) 0x7e, (byte) 0xba, (byte) 0x77, (byte) 0xd6, (byte) 0x26, + (byte) 0xe1, (byte) 0x69, (byte) 0x14, (byte) 0x63, (byte) 0x55, + (byte) 0x21, (byte) 0x0c, (byte) 0x7d}; + + /** + * Substitutes all {@code byte}s in a word. The word array will be changed. + * + * @param value array in which the first {@code WORD_SIZE} {@code byte}s will be substituted. + * This array will be modified. + * @return returns the modified {@code value} + */ + private byte[] substituteWord(byte[] value) { + for (int i = 0; i < WORD_SIZE; ++i) { + value[i] = this._sBox[value[i] & 0xff]; + } + return value; + } + + /** + * Rotate the {@code byte}'s in a word. The {@code byte}'s will be cycled left by one + * {@code byte}. The modification will be in place, so the original argument is changed after + * the method invocation. + * + * @param value Array in which the first {@code WORD_SIZE} {@code byte}'s will be changed due to + * the rotation. The contents of this array is changed by this invocation. + */ + private byte[] rotate(byte[] value) { + byte tmp = value[0]; + for (int i = 1; i < WORD_SIZE; ++i) { + value[i - 1] = value[i]; + } + value[WORD_SIZE - 1] = tmp; + return value; + } + + /** + * Expands the key. The incoming key is {@code KEY_SIZE} {@code byte}s long. It will be expanded + * to a length of {@code EXPANDED_KEY_SIZE} {@code byte}s. The expanded key will be stored in + * {@link Aes256#_expandedKey}. + *

+ * The encryption and decryption will use the expanded key. + *

+ * + * @param key key for the AES algorithm + */ + public Aes256(byte[] key) { + this._expandedKey = new byte[EXPANDED_KEY_SIZE]; + this._tmp = new byte[BLOCK_SIZE]; + + System.arraycopy(key, 0, this._expandedKey, 0, KEY_SIZE); + + for (int i = KEY_SIZE; i < EXPANDED_KEY_SIZE; i += WORD_SIZE) { + System.arraycopy(this._expandedKey, i - WORD_SIZE, this._tmp, 0, WORD_SIZE); + + if (i % KEY_SIZE == 0) { + substituteWord(rotate(this._tmp)); + this._tmp[0] ^= 1 << (i / KEY_SIZE - 1); + } else if (i % KEY_SIZE == BLOCK_SIZE) { + substituteWord(this._tmp); + } + + for (int j = 0; j < WORD_SIZE; ++j) { + this._expandedKey[i + j] = (byte) (this._expandedKey[i - KEY_SIZE + j] ^ this._tmp[j]); + } + } + } + + /** + * Combines the state with the expanded key. The {@code byte}s will be combined by {@code XOR}. + * + * @param index start of the part of the expanded key, that will be used for the combination + */ + private void addRoundKey(int index) { + for (int i = 0; i < BLOCK_SIZE; ++i) { + this._tmp[i] = (byte) (this._tmp[i] ^ this._expandedKey[index + i]); + } + } + + /** + * The polynomial represented by {@code b} will be multiplied by its free variable. This + * multiplication takes place in a finite field. The resulting polynomial can still be represented + * in one {@code byte}. + *

+ * The bits {@code 0} to {@code 7} are the coefficients of the powers {@code x} to {@code x**8}. + *

+ * + * @param b origin polynomial + * @return multiplied polynomial + */ + private int times2(int b) { + int result = b << 1; + if ((b & 0x80) != 0) { + result ^= 0x1b; + } + return result & 0xff; + } + + /** + * Two polynomial will be multiplied with each other. The representation of the polynomial is + * described in {@link Aes256#times2}. + *

+ * The multiplication will be performed by successive invocations of {@link Aes256#times2}. + *

+ * + * @param a first polynomial + * @param b second polynomial + * @return result of the multiplication + */ + private byte mul(int a, byte b) { + int result = 0; + int first = a; + int current = b & 0xff; + while (first != 0) { + if ((first & 0x01) != 0) { + result ^= current; + } + first >>= 1; + current = times2(current); + } + return (byte) (result & 0xff); + } + + /** + * Changes all {@code byte}s in the state by the s-box. + */ + private void substituteState() { + for (int i = 0; i < BLOCK_SIZE; ++i) { + this._tmp[i] = this._sBox[this._tmp[i] & 0xff]; + } + } + + /** + * Rotates the last three rows of the state. + */ + private void shiftRows() { + byte tmp = this._tmp[1]; + this._tmp[1] = this._tmp[5]; + this._tmp[5] = this._tmp[9]; + this._tmp[9] = this._tmp[13]; + this._tmp[13] = tmp; + + tmp = this._tmp[2]; + this._tmp[2] = this._tmp[10]; + this._tmp[10] = tmp; + tmp = this._tmp[6]; + this._tmp[6] = this._tmp[14]; + this._tmp[14] = tmp; + + tmp = this._tmp[3]; + this._tmp[3] = this._tmp[15]; + this._tmp[15] = this._tmp[11]; + this._tmp[11] = this._tmp[7]; + this._tmp[7] = tmp; + } + + /** + * Mixes one column of the state. + * + * @param index position of the first element of the column + */ + private void mixColumn(int index) { + int s0 = mul(2, this._tmp[index]) ^ mul(3, this._tmp[index + 1]) + ^ (this._tmp[index + 2] & 0xff) ^ (this._tmp[index + 3] & 0xff); + int s1 = (this._tmp[index] & 0xff) ^ mul(2, this._tmp[index + 1]) + ^ mul(3, this._tmp[index + 2]) ^ (this._tmp[index + 3] & 0xff); + int s2 = (this._tmp[index] & 0xff) ^ (this._tmp[index + 1] & 0xff) + ^ mul(2, this._tmp[index + 2]) ^ mul(3, this._tmp[index + 3]); + int s3 = mul(3, this._tmp[index]) ^ (this._tmp[index + 1] & 0xff) + ^ (this._tmp[index + 2] & 0xff) ^ mul(2, this._tmp[index + 3]); + this._tmp[index] = (byte) (s0 & 0xff); + this._tmp[index + 1] = (byte) (s1 & 0xff); + this._tmp[index + 2] = (byte) (s2 & 0xff); + this._tmp[index + 3] = (byte) (s3 & 0xff); + } + + /** + * Mixes all columns of the state. + */ + private void mixColumns() { + mixColumn(0); + mixColumn(4); + mixColumn(8); + mixColumn(12); + } + + /** + * Encrypts one block. The input block lies in {@code inBlock} starting at the position + * {@code inIndex}. The {@code inBlock} won't be modified by this method. The encrypted block + * will be stored in {@code outBlock} starting at position {@code outIndex}. + * + * @param inBlock array containing the input block + * @param inIndex starting of the input block in {@code inBlock} + * @param outBlock array to store the encrypted block + * @param outIndex starting of the encrypted block in {@code outBlock} + */ + public void encrypt(byte[] inBlock, int inIndex, byte[] outBlock, + int outIndex) { + System.arraycopy(inBlock, inIndex, this._tmp, 0, BLOCK_SIZE); + + addRoundKey(0); + for (int round = 1; round < ROUNDS; ++round) { + substituteState(); + shiftRows(); + mixColumns(); + addRoundKey(round * BLOCK_SIZE); + } + + substituteState(); + shiftRows(); + addRoundKey(ROUNDS * BLOCK_SIZE); + + System.arraycopy(this._tmp, 0, outBlock, outIndex, BLOCK_SIZE); + } + + /** + * Rotates the last three rows of the state. This method inverses {@link Aes256#shiftRows}. + */ + private void invShiftRows() { + byte tmp = this._tmp[13]; + this._tmp[13] = this._tmp[9]; + this._tmp[9] = this._tmp[5]; + this._tmp[5] = this._tmp[1]; + this._tmp[1] = tmp; + + tmp = this._tmp[2]; + this._tmp[2] = this._tmp[10]; + this._tmp[10] = tmp; + tmp = this._tmp[6]; + this._tmp[6] = this._tmp[14]; + this._tmp[14] = tmp; + + tmp = this._tmp[3]; + this._tmp[3] = this._tmp[7]; + this._tmp[7] = this._tmp[11]; + this._tmp[11] = this._tmp[15]; + this._tmp[15] = tmp; + } + + /** + * Changes all {@code byte}s of the state. This method is the inverse of + * {@link Aes256#shiftRows}. + */ + private void invSubstituteState() { + for (int i = 0; i < BLOCK_SIZE; ++i) { + this._tmp[i] = this._invSBox[this._tmp[i] & 0xff]; + } + } + + /** + * Mixes a column of the state. This method inverses {@link Aes256#mixColumn}. + * + * @param index position of the first entry of the row + */ + private void invMixColumn(int index) { + int s0 = mul(0x0e, this._tmp[index]) ^ mul(0x0b, this._tmp[index + 1]) + ^ mul(0x0d, this._tmp[index + 2]) ^ mul(0x09, this._tmp[index + 3]); + int s1 = mul(0x09, this._tmp[index]) ^ mul(0x0e, this._tmp[index + 1]) + ^ mul(0x0b, this._tmp[index + 2]) ^ mul(0x0d, this._tmp[index + 3]); + int s2 = mul(0x0d, this._tmp[index]) ^ mul(0x09, this._tmp[index + 1]) + ^ mul(0x0e, this._tmp[index + 2]) ^ mul(0x0b, this._tmp[index + 3]); + int s3 = mul(0x0b, this._tmp[index]) ^ mul(0x0d, this._tmp[index + 1]) + ^ mul(0x09, this._tmp[index + 2]) ^ mul(0x0e, this._tmp[index + 3]); + this._tmp[index] = (byte) (s0 & 0xff); + this._tmp[index + 1] = (byte) (s1 & 0xff); + this._tmp[index + 2] = (byte) (s2 & 0xff); + this._tmp[index + 3] = (byte) (s3 & 0xff); + } + + /** + * Mixes all columns of the state. This method inverses {@link Aes256#mixColumns}. + */ + private void invMixColumns() { + invMixColumn(0); + invMixColumn(4); + invMixColumn(8); + invMixColumn(12); + } + + /** + * Decrypts a block. The encrypted block starts at {@code inIndex} in {@code inBlock}. + * {@code inBlock} won't be modified by this method. The decrypted block will be stored at + * {@code outIndex} in {@code outBlock}. + * + * @param inBlock array containing the encrypted block + * @param inIndex starting point of the encrypted block + * @param outBlock array to store the decrypted block + * @param outIndex position of the decrypted block + */ + public void decrypt(byte[] inBlock, int inIndex, byte[] outBlock, + int outIndex) { + System.arraycopy(inBlock, inIndex, this._tmp, 0, BLOCK_SIZE); + + addRoundKey(ROUNDS * BLOCK_SIZE); + for (int round = ROUNDS - 1; round > 0; --round) { + invShiftRows(); + invSubstituteState(); + addRoundKey(round * BLOCK_SIZE); + invMixColumns(); + } + invShiftRows(); + invSubstituteState(); + addRoundKey(0); + + System.arraycopy(this._tmp, 0, outBlock, outIndex, BLOCK_SIZE); + } + } + + static class Cbc { + + /** + * size of a block in {@code byte}s + */ + private static final int BLOCK_SIZE = 16; + + /** + * cipher + */ + private final Aes256 _cipher; + + /** + * last calculated block + */ + private final byte[] _current; + + /** + * temporary block. It will only be used for decryption. + */ + private byte[] _buffer = null; + + /** + * temporary block. + */ + private final byte[] _tmp; + + /** + * buffer of the last output block. It will only be used for decryption. + */ + private byte[] _outBuffer = null; + + /** + * Is the output buffer filled? + */ + private boolean _outBufferUsed = false; + + /** + * temporary buffer to accumulate whole blocks of data + */ + private final byte[] _overflow; + + /** + * How many {@code byte}s of {@link Cbc#_overflow} are used? + */ + private int _overflowUsed; + + private final OutputStream _output; + + /** + * Creates the temporary buffers. + * + * @param iv initial value of {@link Cbc#_tmp} + * @param key key for {@link Cbc#_cipher} + * @param output stream where the encrypted or decrypted data is written + */ + public Cbc(byte[] iv, byte[] key, OutputStream output) { + this._cipher = new Aes256(key); + this._current = new byte[BLOCK_SIZE]; + System.arraycopy(iv, 0, this._current, 0, BLOCK_SIZE); + this._tmp = new byte[BLOCK_SIZE]; + this._buffer = new byte[BLOCK_SIZE]; + this._outBuffer = new byte[BLOCK_SIZE]; + this._outBufferUsed = false; + this._overflow = new byte[BLOCK_SIZE]; + this._overflowUsed = 0; + this._output = output; + } + + /** + * Encrypts a block. {@link Cbc#_current} will be modified. + * + * @param inBuffer array containing the input block + * @param outBuffer storage of the encrypted block + */ + private void encryptBlock(byte[] inBuffer, byte[] outBuffer) { + for (int i = 0; i < BLOCK_SIZE; ++i) { + this._current[i] ^= inBuffer[i]; + } + this._cipher.encrypt(this._current, 0, this._current, 0); + System.arraycopy(this._current, 0, outBuffer, 0, BLOCK_SIZE); + } + + /** + * Decrypts a block. {@link Cbc#_current} will be modified. + * + * @param inBuffer storage of the encrypted block + * @param outBuffer storage of the decrypted block + */ + private void decryptBlock(byte[] inBuffer) { + System.arraycopy(inBuffer, 0, this._buffer, 0, BLOCK_SIZE); + this._cipher.decrypt(this._buffer, 0, this._tmp, 0); + for (int i = 0; i < BLOCK_SIZE; ++i) { + this._tmp[i] ^= this._current[i]; + this._current[i] = this._buffer[i]; + this._outBuffer[i] = this._tmp[i]; + } + } + + /** + * Encrypts the array. The whole array will be encrypted. + * + * @param data {@code byte}s that should be encrypted + * @throws IOException if the writing fails + */ + public void encrypt(byte[] data) throws IOException { + if (data != null) { + encrypt(data, data.length); + } + } + + /** + * Decrypts the array. The whole array will be decrypted. + * + * @param data {@code byte}s that should be decrypted + * @throws IOException if the writing fails + */ + public void decrypt(byte[] data) throws IOException { + if (data != null) { + decrypt(data, data.length); + } + } + + /** + * Encrypts a part of the array. Only the first {@code length} {@code byte}s of the array will + * be encrypted. + * + * @param data {@code byte}s that should be encrypted + * @param length number of {@code byte}s that should be encrypted + * @throws IOException if the writing fails + */ + public void encrypt(byte[] data, int length) throws IOException { + if (data == null || length <= 0) { + return; + } + + for (int i = 0; i < length; ++i) { + this._overflow[this._overflowUsed++] = data[i]; + if (this._overflowUsed == BLOCK_SIZE) { + encryptBlock(this._overflow, this._outBuffer); + this._output.write(this._outBuffer); + this._overflowUsed = 0; + } + } + } + + /** + * Decrypts a part of the array. Only the first {@code length} {@code byte}s of the array will + * be decrypted. + * + * @param data {@code byte}s that should be decrypted + * @param length number of {@code byte}s that should be decrypted + * @throws IOException if the writing fails + */ + public void decrypt(byte[] data, int length) throws IOException { + if (data == null || length <= 0) { + return; + } + + for (int i = 0; i < length; ++i) { + this._overflow[this._overflowUsed++] = data[i]; + if (this._overflowUsed == BLOCK_SIZE) { + if (this._outBufferUsed) { + this._output.write(this._outBuffer); + } + decryptBlock(this._overflow); + this._outBufferUsed = true; + this._overflowUsed = 0; + } + } + } + + /** + * Finishes the encryption process. + * + * @throws IOException if the writing fails + */ + public void finishEncryption() throws IOException { + byte pad = (byte) (BLOCK_SIZE - this._overflowUsed); + while (this._overflowUsed < BLOCK_SIZE) { + this._overflow[this._overflowUsed++] = pad; + } + + encryptBlock(this._overflow, this._outBuffer); + this._output.write(this._outBuffer); + this._output.close(); + } + + /** + * Finishes the decryption process. + * + * @throws DecryptException if the last block is no legal conclusion of the stream + * @throws IOException if the writing fails + */ + public void finishDecryption() throws DecryptException, IOException { + if (this._overflowUsed != 0) { +// throw new DecryptException(); + } + if (!this._outBufferUsed) { + return; + } + + int pad = this._outBuffer[BLOCK_SIZE - 1] & 0xff; + if (pad <= 0 || pad > BLOCK_SIZE) { +// throw new DecryptException(); + } + +// int left = BLOCK_SIZE - pad; +// if (left > 0) { + this._output.write(this._outBuffer, 0, 16); +// } + this._output.close(); + } + } + + static final class DecryptException extends Exception { + + private static final long serialVersionUID = -935882404526228391L; + + /** + * Creates the exception. + */ + public DecryptException() { + super("Decryption failed."); + } + } + +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Base64.java b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Base64.java new file mode 100644 index 00000000..58a77307 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Base64.java @@ -0,0 +1,992 @@ +package com.pengrad.telegrambot.passport.decrypt; + +/* + * + * Copyright (c) 2012, 2013, Oracle and/or its affiliates. All rights reserved. + * ORACLE PROPRIETARY/CONFIDENTIAL. Use is subject to license terms. + * + */ + +import java.io.FilterOutputStream; +import java.io.InputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; +import java.util.Arrays; + +/** + *

+ * Clone of java.util.Base64 in Java 8 SE. + *

+ * + * This class consists exclusively of static methods for obtaining encoders and + * decoders for the Base64 encoding scheme. The implementation of this class + * supports the following types of Base64 as specified in RFC 4648 and RFC 2045. + * + * + * + *

+ * Unless otherwise noted, passing a {@code null} argument to a method of this + * class will cause a {@link java.lang.NullPointerException + * NullPointerException} to be thrown. + * + * @author Xueming Shen + * @since 1.8 + */ +public class Base64 { + + private Base64() { + } + + /** + * Returns a {@link Encoder} that encodes using the Basic type base64 encoding scheme. + * + * @return A Base64 encoder. + */ + public static Encoder getEncoder() { + return Encoder.RFC4648; + } + + /** + * Returns a {@link Encoder} that encodes using the URL and + * Filename safe type base64 encoding scheme. + * + * @return A Base64 encoder. + */ + public static Encoder getUrlEncoder() { + return Encoder.RFC4648_URLSAFE; + } + + /** + * Returns a {@link Encoder} that encodes using the MIME + * type base64 encoding scheme. + * + * @return A Base64 encoder. + */ + public static Encoder getMimeEncoder() { + return Encoder.RFC2045; + } + + /** + * Returns a {@link Encoder} that encodes using the MIME + * type base64 encoding scheme with specified line length and line + * separators. + * + * @param lineLength the length of each output line (rounded down to nearest + * multiple of 4). If {@code lineLength <= 0} the output will not be + * separated in lines + * @param lineSeparator the line separator for each output line + * + * @return A Base64 encoder. + * + * @throws IllegalArgumentException if {@code lineSeparator} includes any + * character of "The Base64 Alphabet" as specified in Table 1 of RFC 2045. + */ + public static Encoder getMimeEncoder(int lineLength, byte[] lineSeparator) { + if (lineSeparator == null) { + throw new NullPointerException(); + } + int[] base64 = Decoder.fromBase64; + for (byte b : lineSeparator) { + if (base64[b & 0xff] != -1) { + throw new IllegalArgumentException("Illegal base64 line separator character 0x" + Integer.toString(b, 16)); + } + } + if (lineLength <= 0) { + return Encoder.RFC4648; + } + return new Encoder(false, lineSeparator, lineLength >> 2 << 2, true); + } + + /** + * Returns a {@link Decoder} that decodes using the Basic type base64 encoding scheme. + * + * @return A Base64 decoder. + */ + public static Decoder getDecoder() { + return Decoder.RFC4648; + } + + /** + * Returns a {@link Decoder} that decodes using the URL and + * Filename safe type base64 encoding scheme. + * + * @return A Base64 decoder. + */ + public static Decoder getUrlDecoder() { + return Decoder.RFC4648_URLSAFE; + } + + /** + * Returns a {@link Decoder} that decodes using the MIME + * type base64 decoding scheme. + * + * @return A Base64 decoder. + */ + public static Decoder getMimeDecoder() { + return Decoder.RFC2045; + } + + /** + * This class implements an encoder for encoding byte data using the Base64 + * encoding scheme as specified in RFC 4648 and RFC 2045. + * + *

+ * Instances of {@link Encoder} class are safe for use by multiple + * concurrent threads. + * + *

+ * Unless otherwise noted, passing a {@code null} argument to a method of + * this class will cause a {@link java.lang.NullPointerException + * NullPointerException} to be thrown. + * + * @see Decoder + * @since 1.8 + */ + public static class Encoder { + + private final byte[] newline; + private final int linemax; + private final boolean isURL; + private final boolean doPadding; + + private Encoder(boolean isURL, byte[] newline, int linemax, boolean doPadding) { + this.isURL = isURL; + this.newline = newline; + this.linemax = linemax; + this.doPadding = doPadding; + } + + /** + * This array is a lookup table that translates 6-bit positive integer + * index values into their "Base64 Alphabet" equivalents as specified in + * "Table 1: The Base64 Alphabet" of RFC 2045 (and RFC 4648). + */ + private static final char[] toBase64 = {'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', '+', '/'}; + + /** + * It's the lookup table for "URL and Filename safe Base64" as specified + * in Table 2 of the RFC 4648, with the '+' and '/' changed to '-' and + * '_'. This table is used when BASE64_URL is specified. + */ + private static final char[] toBase64URL = {'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', '-', '_'}; + + private static final int MIMELINEMAX = 76; + private static final byte[] CRLF = new byte[]{'\r', '\n'}; + + static final Encoder RFC4648 = new Encoder(false, null, -1, true); + static final Encoder RFC4648_URLSAFE = new Encoder(true, null, -1, true); + static final Encoder RFC2045 = new Encoder(false, CRLF, MIMELINEMAX, true); + + private final int outLength(int srclen) { + int len = 0; + if (doPadding) { + len = 4 * ((srclen + 2) / 3); + } else { + int n = srclen % 3; + len = 4 * (srclen / 3) + (n == 0 ? 0 : n + 1); + } + if (linemax > 0) // line separators + { + len += (len - 1) / linemax * newline.length; + } + return len; + } + + /** + * Encodes all bytes from the specified byte array into a + * newly-allocated byte array using the {@link Base64} encoding scheme. + * The returned byte array is of the length of the resulting bytes. + * + * @param src the byte array to encode + * @return A newly-allocated byte array containing the resulting encoded + * bytes. + */ + public byte[] encode(byte[] src) { + int len = outLength(src.length); // dst array size + byte[] dst = new byte[len]; + int ret = encode0(src, 0, src.length, dst); + if (ret != dst.length) { + return Arrays.copyOf(dst, ret); + } + return dst; + } + + /** + * Encodes all bytes from the specified byte array using the + * {@link Base64} encoding scheme, writing the resulting bytes to the + * given output byte array, starting at offset 0. + * + *

+ * It is the responsibility of the invoker of this method to make sure + * the output byte array {@code dst} has enough space for encoding all + * bytes from the input byte array. No bytes will be written to the + * output byte array if the output byte array is not big enough. + * + * @param src the byte array to encode + * @param dst the output byte array + * @return The number of bytes written to the output byte array + * + * @throws IllegalArgumentException if {@code dst} does not have enough + * space for encoding all input bytes. + */ + public int encode(byte[] src, byte[] dst) { + int len = outLength(src.length); // dst array size + if (dst.length < len) { + throw new IllegalArgumentException("Output byte array is too small for encoding all input bytes"); + } + return encode0(src, 0, src.length, dst); + } + + /** + * Encodes the specified byte array into a String using the + * {@link Base64} encoding scheme. + * + *

+ * This method first encodes all input bytes into a base64 encoded byte + * array and then constructs a new String by using the encoded byte + * array and the {@link java.nio.charset.StandardCharsets#ISO_8859_1 + * ISO-8859-1} charset. + * + *

+ * In other words, an invocation of this method has exactly the same + * effect as invoking + * {@code new String(encode(src), StandardCharsets.ISO_8859_1)}. + * + * @param src the byte array to encode + * @return A String containing the resulting Base64 encoded characters + */ + @SuppressWarnings("deprecation") + public String encodeToString(byte[] src) { + byte[] encoded = encode(src); + return new String(encoded, 0, 0, encoded.length); + } + + /** + * Encodes all remaining bytes from the specified byte buffer into a + * newly-allocated ByteBuffer using the {@link Base64} encoding scheme. + * + * Upon return, the source buffer's position will be updated to its + * limit; its limit will not have been changed. The returned output + * buffer's position will be zero and its limit will be the number of + * resulting encoded bytes. + * + * @param buffer the source ByteBuffer to encode + * @return A newly-allocated byte buffer containing the encoded bytes. + */ + public ByteBuffer encode(ByteBuffer buffer) { + int len = outLength(buffer.remaining()); + byte[] dst = new byte[len]; + int ret = 0; + if (buffer.hasArray()) { + ret = encode0(buffer.array(), buffer.arrayOffset() + buffer.position(), buffer.arrayOffset() + buffer.limit(), dst); + buffer.position(buffer.limit()); + } else { + byte[] src = new byte[buffer.remaining()]; + buffer.get(src); + ret = encode0(src, 0, src.length, dst); + } + if (ret != dst.length) { + dst = Arrays.copyOf(dst, ret); + } + return ByteBuffer.wrap(dst); + } + + /** + * Wraps an output stream for encoding byte data using the + * {@link Base64} encoding scheme. + * + *

+ * It is recommended to promptly close the returned output stream after + * use, during which it will flush all possible leftover bytes to the + * underlying output stream. Closing the returned output stream will + * close the underlying output stream. + * + * @param os the output stream. + * @return the output stream for encoding the byte data into the + * specified Base64 encoded format + */ + public OutputStream wrap(OutputStream os) { + if (os == null) { + throw new NullPointerException(); + } + return new EncOutputStream(os, isURL ? toBase64URL : toBase64, newline, linemax, doPadding); + } + + /** + * Returns an encoder instance that encodes equivalently to this one, + * but without adding any padding character at the end of the encoded + * byte data. + * + *

+ * The encoding scheme of this encoder instance is unaffected by this + * invocation. The returned encoder instance should be used for + * non-padding encoding operation. + * + * @return an equivalent encoder that encodes without adding any padding + * character at the end + */ + public Encoder withoutPadding() { + if (!doPadding) { + return this; + } + return new Encoder(isURL, newline, linemax, false); + } + + private int encode0(byte[] src, int off, int end, byte[] dst) { + char[] base64 = isURL ? toBase64URL : toBase64; + int sp = off; + int slen = (end - off) / 3 * 3; + int sl = off + slen; + if (linemax > 0 && slen > linemax / 4 * 3) { + slen = linemax / 4 * 3; + } + int dp = 0; + while (sp < sl) { + int sl0 = Math.min(sp + slen, sl); + for (int sp0 = sp, dp0 = dp; sp0 < sl0;) { + int bits = (src[sp0++] & 0xff) << 16 | (src[sp0++] & 0xff) << 8 | (src[sp0++] & 0xff); + dst[dp0++] = (byte) base64[(bits >>> 18) & 0x3f]; + dst[dp0++] = (byte) base64[(bits >>> 12) & 0x3f]; + dst[dp0++] = (byte) base64[(bits >>> 6) & 0x3f]; + dst[dp0++] = (byte) base64[bits & 0x3f]; + } + int dlen = (sl0 - sp) / 3 * 4; + dp += dlen; + sp = sl0; + if (dlen == linemax && sp < end) { + for (byte b : newline) { + dst[dp++] = b; + } + } + } + if (sp < end) { // 1 or 2 leftover bytes + int b0 = src[sp++] & 0xff; + dst[dp++] = (byte) base64[b0 >> 2]; + if (sp == end) { + dst[dp++] = (byte) base64[(b0 << 4) & 0x3f]; + if (doPadding) { + dst[dp++] = '='; + dst[dp++] = '='; + } + } else { + int b1 = src[sp++] & 0xff; + dst[dp++] = (byte) base64[(b0 << 4) & 0x3f | (b1 >> 4)]; + dst[dp++] = (byte) base64[(b1 << 2) & 0x3f]; + if (doPadding) { + dst[dp++] = '='; + } + } + } + return dp; + } + } + + /** + * This class implements a decoder for decoding byte data using the Base64 + * encoding scheme as specified in RFC 4648 and RFC 2045. + * + *

+ * The Base64 padding character {@code '='} is accepted and interpreted as + * the end of the encoded byte data, but is not required. So if the final + * unit of the encoded byte data only has two or three Base64 characters + * (without the corresponding padding character(s) padded), they are decoded + * as if followed by padding character(s). If there is a padding character + * present in the final unit, the correct number of padding character(s) + * must be present, otherwise {@code IllegalArgumentException} ( + * {@code IOException} when reading from a Base64 stream) is thrown during + * decoding. + * + *

+ * Instances of {@link Decoder} class are safe for use by multiple + * concurrent threads. + * + *

+ * Unless otherwise noted, passing a {@code null} argument to a method of + * this class will cause a {@link java.lang.NullPointerException + * NullPointerException} to be thrown. + * + * @see Encoder + * @since 1.8 + */ + public static class Decoder { + + private final boolean isURL; + private final boolean isMIME; + + private Decoder(boolean isURL, boolean isMIME) { + this.isURL = isURL; + this.isMIME = isMIME; + } + + /** + * Lookup table for decoding unicode characters drawn from the "Base64 + * Alphabet" (as specified in Table 1 of RFC 2045) into their 6-bit + * positive integer equivalents. Characters that are not in the Base64 + * alphabet but fall within the bounds of the array are encoded to -1. + * + */ + private static final int[] fromBase64 = new int[256]; + + static { + Arrays.fill(fromBase64, -1); + for (int i = 0; i < Encoder.toBase64.length; i++) { + fromBase64[Encoder.toBase64[i]] = i; + } + fromBase64['='] = -2; + } + + /** + * Lookup table for decoding "URL and Filename safe Base64 Alphabet" as + * specified in Table2 of the RFC 4648. + */ + private static final int[] fromBase64URL = new int[256]; + + static { + Arrays.fill(fromBase64URL, -1); + for (int i = 0; i < Encoder.toBase64URL.length; i++) { + fromBase64URL[Encoder.toBase64URL[i]] = i; + } + fromBase64URL['='] = -2; + } + + static final Decoder RFC4648 = new Decoder(false, false); + static final Decoder RFC4648_URLSAFE = new Decoder(true, false); + static final Decoder RFC2045 = new Decoder(false, true); + + /** + * Decodes all bytes from the input byte array using the {@link Base64} + * encoding scheme, writing the results into a newly-allocated output + * byte array. The returned byte array is of the length of the resulting + * bytes. + * + * @param src the byte array to decode + * + * @return A newly-allocated byte array containing the decoded bytes. + * + * @throws IllegalArgumentException if {@code src} is not in valid + * Base64 scheme + */ + public byte[] decode(byte[] src) { + byte[] dst = new byte[outLength(src, 0, src.length)]; + int ret = decode0(src, 0, src.length, dst); + if (ret != dst.length) { + dst = Arrays.copyOf(dst, ret); + } + return dst; + } + + /** + * Decodes a Base64 encoded String into a newly-allocated byte array + * using the {@link Base64} encoding scheme. + * + *

+ * An invocation of this method has exactly the same effect as invoking + * {@code decode(src.getBytes(StandardCharsets.ISO_8859_1))} + * + * @param src the string to decode + * + * @return A newly-allocated byte array containing the decoded bytes. + * + * @throws IllegalArgumentException if {@code src} is not in valid + * Base64 scheme + */ + public byte[] decode(String src) { + return decode(src.getBytes(Charset.forName("ISO-8859-1"))); + } + + /** + * Decodes all bytes from the input byte array using the {@link Base64} + * encoding scheme, writing the results into the given output byte + * array, starting at offset 0. + * + *

+ * It is the responsibility of the invoker of this method to make sure + * the output byte array {@code dst} has enough space for decoding all + * bytes from the input byte array. No bytes will be be written to the + * output byte array if the output byte array is not big enough. + * + *

+ * If the input byte array is not in valid Base64 encoding scheme then + * some bytes may have been written to the output byte array before + * IllegalargumentException is thrown. + * + * @param src the byte array to decode + * @param dst the output byte array + * + * @return The number of bytes written to the output byte array + * + * @throws IllegalArgumentException if {@code src} is not in valid + * Base64 scheme, or {@code dst} does not have enough space for decoding + * all input bytes. + */ + public int decode(byte[] src, byte[] dst) { + int len = outLength(src, 0, src.length); + if (dst.length < len) { + throw new IllegalArgumentException("Output byte array is too small for decoding all input bytes"); + } + return decode0(src, 0, src.length, dst); + } + + /** + * Decodes all bytes from the input byte buffer using the {@link Base64} + * encoding scheme, writing the results into a newly-allocated + * ByteBuffer. + * + *

+ * Upon return, the source buffer's position will be updated to its + * limit; its limit will not have been changed. The returned output + * buffer's position will be zero and its limit will be the number of + * resulting decoded bytes + * + *

+ * {@code IllegalArgumentException} is thrown if the input buffer is not + * in valid Base64 encoding scheme. The position of the input buffer + * will not be advanced in this case. + * + * @param buffer the ByteBuffer to decode + * + * @return A newly-allocated byte buffer containing the decoded bytes + * + * @throws IllegalArgumentException if {@code src} is not in valid + * Base64 scheme. + */ + public ByteBuffer decode(ByteBuffer buffer) { + int pos0 = buffer.position(); + try { + byte[] src; + int sp, sl; + if (buffer.hasArray()) { + src = buffer.array(); + sp = buffer.arrayOffset() + buffer.position(); + sl = buffer.arrayOffset() + buffer.limit(); + buffer.position(buffer.limit()); + } else { + src = new byte[buffer.remaining()]; + buffer.get(src); + sp = 0; + sl = src.length; + } + byte[] dst = new byte[outLength(src, sp, sl)]; + return ByteBuffer.wrap(dst, 0, decode0(src, sp, sl, dst)); + } catch (IllegalArgumentException iae) { + buffer.position(pos0); + throw iae; + } + } + + /** + * Returns an input stream for decoding {@link Base64} encoded byte + * stream. + * + *

+ * The {@code read} methods of the returned {@code InputStream} will + * throw {@code IOException} when reading bytes that cannot be decoded. + * + *

+ * Closing the returned input stream will close the underlying input + * stream. + * + * @param is the input stream + * + * @return the input stream for decoding the specified Base64 encoded + * byte stream + */ + public InputStream wrap(InputStream is) { + if (is == null) { + throw new NullPointerException(); + } + return new DecInputStream(is, isURL ? fromBase64URL : fromBase64, isMIME); + } + + private int outLength(byte[] src, int sp, int sl) { + int[] base64 = isURL ? fromBase64URL : fromBase64; + int paddings = 0; + int len = sl - sp; + if (len == 0) { + return 0; + } + if (len < 2) { + if (isMIME && base64[0] == -1) { + return 0; + } + throw new IllegalArgumentException("Input byte[] should at least have 2 bytes for base64 bytes"); + } + if (isMIME) { + // scan all bytes to fill out all non-alphabet. a performance + // trade-off of pre-scan or Arrays.copyOf + int n = 0; + while (sp < sl) { + int b = src[sp++] & 0xff; + if (b == '=') { + len -= (sl - sp + 1); + break; + } + if ((b = base64[b]) == -1) { + n++; + } + } + len -= n; + } else if (src[sl - 1] == '=') { + paddings++; + if (src[sl - 2] == '=') { + paddings++; + } + } + if (paddings == 0 && (len & 0x3) != 0) { + paddings = 4 - (len & 0x3); + } + return 3 * ((len + 3) / 4) - paddings; + } + + private int decode0(byte[] src, int sp, int sl, byte[] dst) { + int[] base64 = isURL ? fromBase64URL : fromBase64; + int dp = 0; + int bits = 0; + int shiftto = 18; // pos of first byte of 4-byte atom + while (sp < sl) { + int b = src[sp++] & 0xff; + if ((b = base64[b]) < 0) { + if (b == -2) { // padding byte '=' + // = shiftto==18 unnecessary padding + // x= shiftto==12 a dangling single x + // x to be handled together with non-padding case + // xx= shiftto==6&&sp==sl missing last = + // xx=y shiftto==6 last is not = + if (shiftto == 6 && (sp == sl || src[sp++] != '=') || shiftto == 18) { + throw new IllegalArgumentException("Input byte array has wrong 4-byte ending unit"); + } + break; + } + if (isMIME) // skip if for rfc2045 + { + continue; + } else { + throw new IllegalArgumentException("Illegal base64 character " + Integer.toString(src[sp - 1], 16)); + } + } + bits |= (b << shiftto); + shiftto -= 6; + if (shiftto < 0) { + dst[dp++] = (byte) (bits >> 16); + dst[dp++] = (byte) (bits >> 8); + dst[dp++] = (byte) (bits); + shiftto = 18; + bits = 0; + } + } + // reached end of byte array or hit padding '=' characters. + if (shiftto == 6) { + dst[dp++] = (byte) (bits >> 16); + } else if (shiftto == 0) { + dst[dp++] = (byte) (bits >> 16); + dst[dp++] = (byte) (bits >> 8); + } else if (shiftto == 12) { + // dangling single "x", incorrectly encoded. + throw new IllegalArgumentException("Last unit does not have enough valid bits"); + } + // anything left is invalid, if is not MIME. + // if MIME, ignore all non-base64 character + while (sp < sl) { + if (isMIME && base64[src[sp++]] < 0) { + continue; + } + throw new IllegalArgumentException("Input byte array has incorrect ending byte at " + sp); + } + return dp; + } + } + + /* + * An output stream for encoding bytes into the Base64. + */ + private static class EncOutputStream extends FilterOutputStream { + + private int leftover = 0; + private int b0, b1, b2; + private boolean closed = false; + + private final char[] base64; // byte->base64 mapping + private final byte[] newline; // line separator, if needed + private final int linemax; + private final boolean doPadding;// whether or not to pad + private int linepos = 0; + + EncOutputStream(OutputStream os, char[] base64, byte[] newline, int linemax, boolean doPadding) { + super(os); + this.base64 = base64; + this.newline = newline; + this.linemax = linemax; + this.doPadding = doPadding; + } + + @Override + public void write(int b) throws IOException { + byte[] buf = new byte[1]; + buf[0] = (byte) (b & 0xff); + write(buf, 0, 1); + } + + private void checkNewline() throws IOException { + if (linepos == linemax) { + out.write(newline); + linepos = 0; + } + } + + @Override + public void write(byte[] b, int off, int len) throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } + if (off < 0 || len < 0 || off + len > b.length) { + throw new ArrayIndexOutOfBoundsException(); + } + if (len == 0) { + return; + } + if (leftover != 0) { + if (leftover == 1) { + b1 = b[off++] & 0xff; + len--; + if (len == 0) { + leftover++; + return; + } + } + b2 = b[off++] & 0xff; + len--; + checkNewline(); + out.write(base64[b0 >> 2]); + out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]); + out.write(base64[(b1 << 2) & 0x3f | (b2 >> 6)]); + out.write(base64[b2 & 0x3f]); + linepos += 4; + } + int nBits24 = len / 3; + leftover = len - (nBits24 * 3); + while (nBits24-- > 0) { + checkNewline(); + int bits = (b[off++] & 0xff) << 16 | (b[off++] & 0xff) << 8 | (b[off++] & 0xff); + out.write(base64[(bits >>> 18) & 0x3f]); + out.write(base64[(bits >>> 12) & 0x3f]); + out.write(base64[(bits >>> 6) & 0x3f]); + out.write(base64[bits & 0x3f]); + linepos += 4; + } + if (leftover == 1) { + b0 = b[off++] & 0xff; + } else if (leftover == 2) { + b0 = b[off++] & 0xff; + b1 = b[off++] & 0xff; + } + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + if (leftover == 1) { + checkNewline(); + out.write(base64[b0 >> 2]); + out.write(base64[(b0 << 4) & 0x3f]); + if (doPadding) { + out.write('='); + out.write('='); + } + } else if (leftover == 2) { + checkNewline(); + out.write(base64[b0 >> 2]); + out.write(base64[(b0 << 4) & 0x3f | (b1 >> 4)]); + out.write(base64[(b1 << 2) & 0x3f]); + if (doPadding) { + out.write('='); + } + } + leftover = 0; + out.close(); + } + } + } + + /* + * An input stream for decoding Base64 bytes + */ + private static class DecInputStream extends InputStream { + + private final InputStream is; + private final boolean isMIME; + private final int[] base64; // base64 -> byte mapping + private int bits = 0; // 24-bit buffer for decoding + private int nextin = 18; // next available "off" in "bits" for input; + // -> 18, 12, 6, 0 + private int nextout = -8; // next available "off" in "bits" for output; + // -> 8, 0, -8 (no byte for output) + private boolean eof = false; + private boolean closed = false; + + DecInputStream(InputStream is, int[] base64, boolean isMIME) { + this.is = is; + this.base64 = base64; + this.isMIME = isMIME; + } + + private byte[] sbBuf = new byte[1]; + + @Override + public int read() throws IOException { + return read(sbBuf, 0, 1) == -1 ? -1 : sbBuf[0] & 0xff; + } + + @Override + public int read(byte[] b, int off, int len) throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } + if (eof && nextout < 0) // eof and no leftover + { + return -1; + } + if (off < 0 || len < 0 || len > b.length - off) { + throw new IndexOutOfBoundsException(); + } + int oldOff = off; + if (nextout >= 0) { // leftover output byte(s) in bits buf + do { + if (len == 0) { + return off - oldOff; + } + b[off++] = (byte) (bits >> nextout); + len--; + nextout -= 8; + } while (nextout >= 0); + bits = 0; + } + while (len > 0) { + int v = is.read(); + if (v == -1) { + eof = true; + if (nextin != 18) { + if (nextin == 12) { + throw new IOException("Base64 stream has one un-decoded dangling byte."); + } + // treat ending xx/xxx without padding character legal. + // same logic as v == '=' below + b[off++] = (byte) (bits >> (16)); + len--; + if (nextin == 0) { // only one padding byte + if (len == 0) { // no enough output space + bits >>= 8; // shift to lowest byte + nextout = 0; + } else { + b[off++] = (byte) (bits >> 8); + } + } + } + if (off == oldOff) { + return -1; + } else { + return off - oldOff; + } + } + if (v == '=') { // padding byte(s) + // = shiftto==18 unnecessary padding + // x= shiftto==12 dangling x, invalid unit + // xx= shiftto==6 && missing last '=' + // xx=y or last is not '=' + if (nextin == 18 || nextin == 12 || nextin == 6 && is.read() != '=') { + throw new IOException("Illegal base64 ending sequence:" + nextin); + } + b[off++] = (byte) (bits >> (16)); + len--; + if (nextin == 0) { // only one padding byte + if (len == 0) { // no enough output space + bits >>= 8; // shift to lowest byte + nextout = 0; + } else { + b[off++] = (byte) (bits >> 8); + } + } + eof = true; + break; + } + if ((v = base64[v]) == -1) { + if (isMIME) // skip if for rfc2045 + { + continue; + } else { + throw new IOException("Illegal base64 character " + Integer.toString(v, 16)); + } + } + bits |= (v << nextin); + if (nextin == 0) { + nextin = 18; // clear for next + nextout = 16; + while (nextout >= 0) { + b[off++] = (byte) (bits >> nextout); + len--; + nextout -= 8; + if (len == 0 && nextout >= 0) { // don't clean "bits" + return off - oldOff; + } + } + bits = 0; + } else { + nextin -= 6; + } + } + return off - oldOff; + } + + @Override + public int available() throws IOException { + if (closed) { + throw new IOException("Stream is closed"); + } + return is.available(); // TBD: + } + + @Override + public void close() throws IOException { + if (!closed) { + closed = true; + is.close(); + } + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Decrypt.java b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Decrypt.java new file mode 100644 index 00000000..036ee70e --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/Decrypt.java @@ -0,0 +1,49 @@ +package com.pengrad.telegrambot.passport.decrypt; + +import com.google.gson.Gson; +import com.pengrad.telegrambot.passport.Credentials; + +import java.util.Arrays; + +/** + * Stas Parshin + * 31 July 2018 + */ + +public class Decrypt { + + public static Credentials decryptCredentials(String privateKey, String data, String hash, String secret) throws Exception { + byte[] s = base64(secret); + byte[] encryptedSecret = RsaOaep.decrypt(privateKey, s); + + byte[] h = base64(hash); + SecretHash secretHash = new SecretHash(encryptedSecret, h); + + byte[] d = base64(data); + byte[] encryptedData = decryptAes256Cbc(secretHash.key(), secretHash.iv(), d); + String credStr = new String(encryptedData); + return new Gson().fromJson(credStr, Credentials.class); + } + + public static String decryptData(String data, String dataHash, String secret) throws Exception { + byte[] d = base64(data); + byte[] encryptedData = decryptFile(d, dataHash, secret); + return new String(encryptedData); + } + + public static byte[] decryptFile(byte[] data, String fileHash, String secret) throws Exception { + SecretHash secretHash = new SecretHash(base64(secret), base64(fileHash)); + return decryptAes256Cbc(secretHash.key(), secretHash.iv(), data); + } + + private static byte[] decryptAes256Cbc(byte[] key, byte[] iv, byte[] data) throws Exception { + byte[] encryptedData = new Aes256Cbc(key, iv).decrypt(data); + int padding = encryptedData[0] & 0xFF; + encryptedData = Arrays.copyOfRange(encryptedData, padding, encryptedData.length); + return encryptedData; + } + + private static byte[] base64(String str) { + return Base64.getMimeDecoder().decode(str); + } +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/RsaOaep.java b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/RsaOaep.java new file mode 100644 index 00000000..aaf349bd --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/RsaOaep.java @@ -0,0 +1,324 @@ +package com.pengrad.telegrambot.passport.decrypt; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.KeyFactory; +import java.security.PrivateKey; +import java.security.spec.RSAPrivateCrtKeySpec; + +import javax.crypto.Cipher; + +/** + * Stas Parshin + * 02 August 2018 + */ +class RsaOaep { + + static byte[] decrypt(String privateKey, byte[] secret) throws Exception { + String pkcs8Pem = privateKey; + pkcs8Pem = pkcs8Pem.replace("-----BEGIN RSA PRIVATE KEY-----", ""); + pkcs8Pem = pkcs8Pem.replace("-----END RSA PRIVATE KEY-----", ""); + pkcs8Pem = pkcs8Pem.replaceAll("\\s+", ""); + byte[] pkcs8EncodedBytes = Base64.getMimeDecoder().decode(pkcs8Pem); + + KeyFactory kf = KeyFactory.getInstance("RSA"); + PrivateKey privKey = kf.generatePrivate(getRSAKeySpec(pkcs8EncodedBytes)); + + Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-1AndMGF1Padding"); + cipher.init(Cipher.DECRYPT_MODE, privKey); + return cipher.doFinal(secret); + } + + private static RSAPrivateCrtKeySpec getRSAKeySpec(byte[] keyBytes) throws IOException { + + DerParser parser = new DerParser(keyBytes); + + Asn1Object sequence = parser.read(); + if (sequence.getType() != DerParser.SEQUENCE) + throw new IOException("Invalid DER: not a sequence"); //$NON-NLS-1$ + + // Parse inside the sequence + parser = sequence.getParser(); + + parser.read(); // Skip version + BigInteger modulus = parser.read().getInteger(); + BigInteger publicExp = parser.read().getInteger(); + BigInteger privateExp = parser.read().getInteger(); + BigInteger prime1 = parser.read().getInteger(); + BigInteger prime2 = parser.read().getInteger(); + BigInteger exp1 = parser.read().getInteger(); + BigInteger exp2 = parser.read().getInteger(); + BigInteger crtCoef = parser.read().getInteger(); + + RSAPrivateCrtKeySpec keySpec = new RSAPrivateCrtKeySpec( + modulus, publicExp, privateExp, prime1, prime2, + exp1, exp2, crtCoef); + + return keySpec; + } + + private static class DerParser { + + // Classes + public final static int UNIVERSAL = 0x00; + public final static int APPLICATION = 0x40; + public final static int CONTEXT = 0x80; + public final static int PRIVATE = 0xC0; + + // Constructed Flag + public final static int CONSTRUCTED = 0x20; + + // Tag and data types + public final static int ANY = 0x00; + public final static int BOOLEAN = 0x01; + public final static int INTEGER = 0x02; + public final static int BIT_STRING = 0x03; + public final static int OCTET_STRING = 0x04; + public final static int NULL = 0x05; + public final static int OBJECT_IDENTIFIER = 0x06; + public final static int REAL = 0x09; + public final static int ENUMERATED = 0x0a; + public final static int RELATIVE_OID = 0x0d; + + public final static int SEQUENCE = 0x10; + public final static int SET = 0x11; + + public final static int NUMERIC_STRING = 0x12; + public final static int PRINTABLE_STRING = 0x13; + public final static int T61_STRING = 0x14; + public final static int VIDEOTEX_STRING = 0x15; + public final static int IA5_STRING = 0x16; + public final static int GRAPHIC_STRING = 0x19; + public final static int ISO646_STRING = 0x1A; + public final static int GENERAL_STRING = 0x1B; + + public final static int UTF8_STRING = 0x0C; + public final static int UNIVERSAL_STRING = 0x1C; + public final static int BMP_STRING = 0x1E; + + public final static int UTC_TIME = 0x17; + public final static int GENERALIZED_TIME = 0x18; + + protected InputStream in; + + /** + * Create a new DER decoder from an input stream. + * + * @param in The DER encoded stream + */ + public DerParser(InputStream in) throws IOException { + this.in = in; + } + + /** + * Create a new DER decoder from a byte array. + * + * @param The encoded bytes + * @throws IOException + */ + public DerParser(byte[] bytes) throws IOException { + this(new ByteArrayInputStream(bytes)); + } + + /** + * Read next object. If it's constructed, the value holds + * encoded content and it should be parsed by a new + * parser from Asn1Object.getParser. + * + * @return A object + * @throws IOException + */ + public Asn1Object read() throws IOException { + int tag = in.read(); + + if (tag == -1) + throw new IOException("Invalid DER: stream too short, missing tag"); //$NON-NLS-1$ + + int length = getLength(); + + byte[] value = new byte[length]; + int n = in.read(value); + if (n < length) + throw new IOException("Invalid DER: stream too short, missing value"); //$NON-NLS-1$ + + Asn1Object o = new Asn1Object(tag, length, value); + + return o; + } + + /** + * Decode the length of the field. Can only support length + * encoding up to 4 octets. + *

+ *

In BER/DER encoding, length can be encoded in 2 forms, + *

+ * + * @return The length as integer + * @throws IOException + */ + private int getLength() throws IOException { + + int i = in.read(); + if (i == -1) + throw new IOException("Invalid DER: length missing"); //$NON-NLS-1$ + + // A single byte short length + if ((i & ~0x7F) == 0) + return i; + + int num = i & 0x7F; + + // We can't handle length longer than 4 bytes + if (i >= 0xFF || num > 4) + throw new IOException("Invalid DER: length field too big (" //$NON-NLS-1$ + + i + ")"); //$NON-NLS-1$ + + byte[] bytes = new byte[num]; + int n = in.read(bytes); + if (n < num) + throw new IOException("Invalid DER: length too short"); //$NON-NLS-1$ + + return new BigInteger(1, bytes).intValue(); + } + + } + + /** + * An ASN.1 TLV. The object is not parsed. It can + * only handle integers and strings. + * + * @author zhang + */ + private static class Asn1Object { + + protected final int type; + protected final int length; + protected final byte[] value; + protected final int tag; + + /** + * Construct a ASN.1 TLV. The TLV could be either a + * constructed or primitive entity. + *

+ *

The first byte in DER encoding is made of following fields, + *

+         * -------------------------------------------------
+         * |Bit 8|Bit 7|Bit 6|Bit 5|Bit 4|Bit 3|Bit 2|Bit 1|
+         * -------------------------------------------------
+         * |  Class    | CF  |     +      Type             |
+         * -------------------------------------------------
+         * 
+ * + * + * @param tag Tag or Identifier + * @param length Length of the field + * @param value Encoded octet string for the field. + */ + public Asn1Object(int tag, int length, byte[] value) { + this.tag = tag; + this.type = tag & 0x1F; + this.length = length; + this.value = value; + } + + public int getType() { + return type; + } + + public int getLength() { + return length; + } + + public byte[] getValue() { + return value; + } + + public boolean isConstructed() { + return (tag & DerParser.CONSTRUCTED) == DerParser.CONSTRUCTED; + } + + /** + * For constructed field, return a parser for its content. + * + * @return A parser for the construct. + * @throws IOException + */ + public DerParser getParser() throws IOException { + if (!isConstructed()) + throw new IOException("Invalid DER: can't parse primitive entity"); //$NON-NLS-1$ + + return new DerParser(value); + } + + /** + * Get the value as integer + * + * @return BigInteger + * @throws IOException + */ + public BigInteger getInteger() throws IOException { + if (type != DerParser.INTEGER) + throw new IOException("Invalid DER: object is not integer"); //$NON-NLS-1$ + + return new BigInteger(value); + } + + /** + * Get value as string. Most strings are treated + * as Latin-1. + * + * @return Java string + * @throws IOException + */ + public String getString() throws IOException { + + String encoding; + + switch (type) { + + // Not all are Latin-1 but it's the closest thing + case DerParser.NUMERIC_STRING: + case DerParser.PRINTABLE_STRING: + case DerParser.VIDEOTEX_STRING: + case DerParser.IA5_STRING: + case DerParser.GRAPHIC_STRING: + case DerParser.ISO646_STRING: + case DerParser.GENERAL_STRING: + encoding = "ISO-8859-1"; //$NON-NLS-1$ + break; + + case DerParser.BMP_STRING: + encoding = "UTF-16BE"; //$NON-NLS-1$ + break; + + case DerParser.UTF8_STRING: + encoding = "UTF-8"; //$NON-NLS-1$ + break; + + case DerParser.UNIVERSAL_STRING: + throw new IOException("Invalid DER: can't handle UCS-4 string"); //$NON-NLS-1$ + + default: + throw new IOException("Invalid DER: object is not a string"); //$NON-NLS-1$ + } + + return new String(value, encoding); + } + } + +} diff --git a/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/SecretHash.java b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/SecretHash.java new file mode 100644 index 00000000..1e50ad50 --- /dev/null +++ b/library/src/main/java/com/pengrad/telegrambot/passport/decrypt/SecretHash.java @@ -0,0 +1,47 @@ +package com.pengrad.telegrambot.passport.decrypt; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Arrays; + +/** + * Stas Parshin + * 31 July 2018 + */ +class SecretHash { + + private final byte[] secretHash; + + public SecretHash(byte[] secret, byte[] hash) throws Exception { + secretHash = sha512(concat(secret, hash)); + } + + public byte[] key() { + return Arrays.copyOfRange(secretHash, 0, 32); + } + + public byte[] iv() { + return Arrays.copyOfRange(secretHash, 32, 48); + } + + private byte[] sha512(byte[] string) throws NoSuchAlgorithmException { + MessageDigest md = MessageDigest.getInstance("SHA-512"); + return md.digest(string); + } + + private byte[] concat(byte[]... arrays) { + int length = 0; + for (byte[] array : arrays) { + length += array.length; + } + byte[] result = new byte[length]; + int pos = 0; + for (byte[] array : arrays) { + for (byte element : array) { + result[pos] = element; + pos++; + } + } + return result; + } +} diff --git a/library/src/test/java/com/pengrad/telegrambot/TelegramBotTest.java b/library/src/test/java/com/pengrad/telegrambot/TelegramBotTest.java index 48c22ba7..bd19b85d 100644 --- a/library/src/test/java/com/pengrad/telegrambot/TelegramBotTest.java +++ b/library/src/test/java/com/pengrad/telegrambot/TelegramBotTest.java @@ -2,6 +2,7 @@ import com.pengrad.telegrambot.model.*; import com.pengrad.telegrambot.model.request.*; +import com.pengrad.telegrambot.passport.*; import com.pengrad.telegrambot.request.*; import com.pengrad.telegrambot.response.*; import okhttp3.OkHttpClient; @@ -11,6 +12,7 @@ import java.io.File; import java.io.FileInputStream; +import java.io.FileOutputStream; import java.io.IOException; import java.net.MalformedURLException; import java.net.URISyntaxException; @@ -18,9 +20,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; -import java.util.Collections; -import java.util.List; -import java.util.Properties; +import java.util.*; import static com.pengrad.telegrambot.request.ContentTypes.VIDEO_MIME_TYPE; import static org.junit.Assert.*; @@ -39,6 +39,8 @@ public class TelegramBotTest { String channelName = "@bottest"; Long channelId = -1001002720332L; Integer memberBot = 215003245; + String privateKey; + String testPassportData; Path resourcePath = Paths.get("src/test/resources"); File imageFile = resourcePath.resolve("image.jpg").toFile(); @@ -70,7 +72,7 @@ public class TelegramBotTest { byte[] gifBytes = Files.readAllBytes(gifFile.toPath()); public TelegramBotTest() throws IOException { - String token, chat, group; + String token, chat, group, Private; try { Properties properties = new Properties(); @@ -79,16 +81,20 @@ public TelegramBotTest() throws IOException { token = properties.getProperty("TEST_TOKEN"); chat = properties.getProperty("CHAT_ID"); group = properties.getProperty("GROUP_ID"); + Private = properties.getProperty("PRIVATE_KEY"); + testPassportData = properties.getProperty("TEST_PASSPORT_DATA"); } catch (Exception e) { token = System.getenv("TEST_TOKEN"); chat = System.getenv("CHAT_ID"); group = System.getenv("GROUP_ID"); + Private = System.getenv("PRIVATE_KEY"); } bot = TelegramBotAdapter.buildDebug(token); chatId = Integer.parseInt(chat); groupId = Long.parseLong(group); + privateKey = Private; } @Test @@ -101,11 +107,11 @@ public void getMe() { @Test public void getUpdates() { GetUpdates getUpdates = new GetUpdates() - .offset(864855330) + .offset(864855364) .allowedUpdates("") .timeout(0) - .limit(10); - assertEquals(10, getUpdates.getLimit()); + .limit(100); + assertEquals(100, getUpdates.getLimit()); GetUpdatesResponse response = bot.execute(getUpdates); UpdateTest.check(response.updates()); } @@ -1166,4 +1172,55 @@ public void sendAnimation() { assertEquals((Integer) 128, animation.width()); assertEquals((Integer) 128, animation.height()); } + + @Test + public void setPassportDataErrors() { + BaseResponse response = bot.execute(new SetPassportDataErrors(chatId, + new PassportElementErrorDataField("personal_details", "first_name", + "TueU2/SswOD5wgQ6uXQ62mJrr0Jdf30r/QQ/jyETHFM=", + "error in page 1") + )); + System.out.println(response); + assertTrue(response.isOk()); + } + + @Test + public void decryptPassport() throws Exception { + List updates = bot.execute(new GetUpdates()).updates(); + Collections.reverse(updates); + PassportData passportData = null; + for (Update update : updates) { + if (update.message() != null && update.message().passportData() != null) { + passportData = update.message().passportData(); + break; + } + } + if (passportData == null) { + passportData = BotUtils.parseUpdate(testPassportData).message().passportData(); + } + assertNotNull(passportData); + + Credentials credentials = passportData.credentials().decrypt(privateKey); + System.out.println(credentials); + + for (EncryptedPassportElement encElement : passportData.data()) { + System.out.println(encElement.decryptData(credentials)); + + List files = new ArrayList(); + files.add(encElement.frontSide()); + files.add(encElement.reverseSide()); + files.add(encElement.selfie()); + if (encElement.files() != null) { + files.addAll(Arrays.asList(encElement.files())); + } + + System.out.println("files: " + Arrays.toString(files.toArray())); + for (int i = 0; i < files.size(); i++) { + PassportFile file = files.get(i); + if (file == null) continue; + byte[] data = encElement.decryptFile(file, credentials, bot); + new FileOutputStream(Paths.get("build/" + encElement.type() + i + ".jpg").toFile()).write(data); + } + } + } }