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 extends DecryptedData> 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 extends DecryptedData> 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
+ * The state can be viewed as a square matrix, modeled as a list of column vectors.
+ *
+ * The encryption and decryption will use the expanded key.
+ *
+ * The bits {@code 0} to {@code 7} are the coefficients of the powers {@code x} to {@code x**8}.
+ *
+ * The multiplication will be performed by successive invocations of {@link Aes256#times2}.
+ *
+ * Clone of java.util.Base64 in Java 8 SE.
+ *
+ * Uses "The Base64 Alphabet" as specified in Table 1 of RFC 4648 and RFC 2045
+ * for encoding and decoding operation. The encoder does not add any line feed
+ * (line separator) character. The decoder rejects data that contains characters
+ * outside the base64 alphabet.
+ *
+ * Uses the "URL and Filename safe Base64 Alphabet" as specified in Table 2 of
+ * RFC 4648 for encoding and decoding. The encoder does not add any line feed
+ * (line separator) character. The decoder rejects data that contains characters
+ * outside the base64 alphabet.
+ *
+ * Uses the "The Base64 Alphabet" as specified in Table 1 of RFC 2045 for
+ * encoding and decoding operation. The encoded output must be represented in
+ * lines of no more than 76 characters each and uses a carriage return
+ * {@code '\r'} followed immediately by a linefeed {@code '\n'} as the line
+ * separator. No line separator is added to the end of the encoded output. All
+ * line separators or other characters not found in the base64 alphabet table
+ * are ignored in decoding operation.
+ *
+ * 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.
+ *
+ *
+ *
+ * @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.
+ *
+ * -------------------------------------------------
+ * |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