diff --git a/types_passport.go b/types_passport.go index a18a7ab..0ee2256 100644 --- a/types_passport.go +++ b/types_passport.go @@ -1,6 +1,152 @@ package telegram type ( + Fields struct { + PersonalDetails *PersonalDetails + Passport *Passport + InternalPassport *InternalPassport + DriverLicense *DriverLicense + IdentityCard *IdentityCard + Address *ResidentialAddress + UtilityBill *UtilityBill + BankStatement *BankStatement + RentalAgreement *RentalAgreement + PassportRegistration *PassportRegistration + TemporaryRegistration *TemporaryRegistration + PhoneNumber PhoneNumber + Email Email + } + + // Passport represent passport. + Passport struct { + Data *IdDocumentData + FrontSide *PassportFile + Selfie *PassportFile + Translation []PassportFile + } + + // InternalPassport represent internal passport. + InternalPassport struct { + Data *IdDocumentData + FrontSide *PassportFile + Selfie *PassportFile + Translation []PassportFile + } + + // DriverLicense represent driver license. + DriverLicense struct { + Passport + ReverseSide *PassportFile + } + + // IdentityCard represent identity card. + IdentityCard struct { + Data *IdDocumentData + FrontSide *PassportFile + ReverseSide *PassportFile + Selfie *PassportFile + Translation []PassportFile + } + + // UtilityBill represent utility bill. + UtilityBill struct { + Files []PassportFile + Translation []PassportFile + } + + // BankStatement represent bank statement. + BankStatement struct { + Files []PassportFile + Translation []PassportFile + } + + // RentalAgreement represent rental agreement. + RentalAgreement struct { + Files []PassportFile + Translation []PassportFile + } + + // PassportRegistration represent registration Page in the internal passport. + PassportRegistration struct { + Files []PassportFile + Translation []PassportFile + } + + // TemporaryRegistration represent temporary registration. + TemporaryRegistration struct { + Files []PassportFile + Translation []PassportFile + } + + // PhoneNumber represent phone number. + PhoneNumber string + + // Email represent email. + Email string + + // PersonalDetails represents personal details. + PersonalDetails struct { + // First Name + FirstName string `json:"first_name"` + + // Last Name + LastName string `json:"last_name"` + + // Middle Name + MiddleName string `json:"middle_name,omitempty"` + + // Date of birth in DD.MM.YYYY format + BirthDate string `json:"birth_date"` + + // Gender, male or female + Gender string `json:"gender"` + + // Citizenship (ISO 3166-1 alpha-2 country code) + CountryCode string `json:"country_code"` + + // Country of residence (ISO 3166-1 alpha-2 country code) + ResidenceCountryCode string `json:"residence_country_code"` + + // First Name in the language of the user's country of residence + FirstNameNative string `json:"first_name_native"` + + // Last Name in the language of the user's country of residence + LastNameNative string `json:"last_name_native"` + + // Middle Name in the language of the user's country of residence + MiddleNameNative string `json:"middle_name_native,omitempty"` + } + + // ResidentialAddress represents a residential address. + ResidentialAddress struct { + // First line for the address + StreetLine1 string `json:"street_line1"` + + // Second line for the address + StreetLine2 string `json:"street_line2,omitempty"` + + // City + City string `json:"city"` + + // State + State string `json:"state,omitempty"` + + // ISO 3166-1 alpha-2 country code + CountryCode string `json:"country_code"` + + // Address post code + PostCode string `json:"post_code"` + } + + // IdDocumentData represents the data of an identity document. + IdDocumentData struct { + // Document number + DocumentNo string `json:"document_no"` + + // Date of expiry, in DD.MM.YYYY format + ExpiryDate string `json:"expiry_date,omitempty"` + } + // PassportData contains information about Telegram Passport data shared with // the bot by the user. PassportData struct { @@ -26,6 +172,96 @@ type ( FileDate int64 `json:"file_date"` } + // Credentials is a JSON-serialized object. + Credentials struct { + // Credentials for encrypted data + SecureData *SecureData `json:"secure_data"` + + // Bot-specified nonce + Nonce string `json:"nonce"` + } + + // SecureData represents the credentials required to decrypt encrypted + // data. All fields are optional and depend on fields that were requested. + SecureData struct { + // Credentials for encrypted personal details + PersonalDetails *SecureValue `json:"personal_details,omitempty"` + + // Credentials for encrypted passport + Passport *SecureValue `json:"passport,omitempty"` + + // Credentials for encrypted internal passport + InternalPassport *SecureValue `json:"internal_passport,omitempty"` + + // Credentials for encrypted driver license + DriverLicense *SecureValue `json:"driver_license,omitempty"` + + // Credentials for encrypted ID card + IdentityCard *SecureValue `json:"identity_card,omitempty"` + + // Credentials for encrypted residential address + Address *SecureValue `json:"address,omitempty"` + + // Credentials for encrypted utility bill + UtilityBill *SecureValue `json:"utility_bill,omitempty"` + + // Credentials for encrypted bank statement + BankStatement *SecureValue `json:"bank_statement,omitempty"` + + // Credentials for encrypted rental agreement + RentalAgreement *SecureValue `json:"rental_agreement,omitempty"` + + // Credentials for encrypted registration from internal passport + PassportRegistration *SecureValue `json:"passport_registration,omitempty"` + + // Credentials for encrypted temporary registration + TemporaryRegistration *SecureValue `json:"temporary_registration,omitempty"` + } + + // SecureValue represents the credentials required to decrypt encrypted + // values. All fields are optional and depend on the type of fields that + // were requested. + SecureValue struct { + // Credentials for encrypted Telegram Passport data. + Data *DataCredentials `json:"data,omitempty"` + + // Credentials for an encrypted document's front side. + FrontSide *FileCredentials `json:"front_side,omitempty"` + + // Credentials for an encrypted document's reverse side. + ReverseSide *FileCredentials `json:"reverse_side,omitempty"` + + // Credentials for an encrypted selfie of the user with a document. + Selfie *FileCredentials `json:"selfie,omitempty"` + + // Credentials for an encrypted translation of the document. + Translation []FileCredentials `json:"translation,omitempty"` + + // Credentials for encrypted files. + Files []FileCredentials `json:"files,omitempty"` + } + + // DataCredentials can be used to decrypt encrypted data from the data + // field in EncryptedPassportElement. + DataCredentials struct { + // Checksum of encrypted data + DataHash string `json:"data_hash"` + + // Secret of encrypted data + Secret string `json:"secret"` + } + + // FileCredentials can be used to decrypt encrypted files from the + // front_side, reverse_side, selfie, files and translation fields in + // EncryptedPassportElement. + FileCredentials struct { + // Checksum of encrypted file + FileHash string `json:"file_hash"` + + // Secret of encrypted file + Secret string `json:"secret"` + } + // EncryptedPassportElement contains information about documents or other // Telegram Passport elements shared with the bot by the user. EncryptedPassportElement struct { @@ -67,6 +303,18 @@ type ( // "identity_card" and "internal_passport". The file can be decrypted // and verified using the accompanying EncryptedCredentials. Selfie *PassportFile `json:"selfie,omitempty"` + + // Array of encrypted files with translated versions of documents + // provided by the user. Available if requested for “passport”, + // “driver_license”, “identity_card”, “internal_passport”, + // “utility_bill”, “bank_statement”, “rental_agreement”, + // “passport_registration” and “temporary_registration” types. + // Files can be decrypted and verified using the accompanying + // EncryptedCredentials. + Translation []PassportFile `json:"translation,omitempty"` + + // Base64-encoded element hash for using in PassportElementErrorUnspecified + Hash string `json:"hash"` } // EncryptedCredentials contains data required for decrypting and diff --git a/utils_bot.go b/utils_bot.go index 0828023..0d0fed8 100644 --- a/utils_bot.go +++ b/utils_bot.go @@ -165,3 +165,41 @@ func (b *Bot) NewRedirectURL(param string, group bool) *http.URI { return link } + +func (b *Bot) DecryptPassportFile(pf *PassportFile, fc *FileCredentials) (data []byte, err error) { + secret, err := decodeField(fc.Secret) + if err != nil { + return + } + + hash, err := decodeField(fc.FileHash) + if err != nil { + return + } + + key, iv := decryptSecretHash(secret, hash) + file, err := b.GetFile(pf.FileID) + if err != nil { + return + } + + _, data, err = b.Client.Get(nil, b.NewFileURL(file.FilePath).String()) + if err != nil { + return + } + + data, err = decryptData(key, iv, data) + if err != nil { + return + } + + if !match(hash, data) { + err = ErrNotEqual + return + } + + offset := int(data[0]) + data = data[offset:] + + return +} diff --git a/utils_data_credentials.go b/utils_data_credentials.go new file mode 100644 index 0000000..d204c94 --- /dev/null +++ b/utils_data_credentials.go @@ -0,0 +1,41 @@ +package telegram + +import "errors" + +var ErrNotEqual = errors.New("credentials hash and credentials data hash is not equal") + +func (dc *DataCredentials) decrypt(d string) (data []byte, err error) { + secret, err := decodeField(dc.Secret) + if err != nil { + return + } + + hash, err := decodeField(dc.DataHash) + if err != nil { + return + } + + key, iv := decryptSecretHash(secret, hash) + if err != nil { + return + } + + data, err = decodeField(d) + if err != nil { + return + } + + data, err = decryptData(key, iv, data) + if err != nil { + return + } + + if !match(hash, data) { + err = ErrNotEqual + } + + offset := int(data[0]) + data = data[offset:] + + return +} diff --git a/utils_encrypted_credentials.go b/utils_encrypted_credentials.go new file mode 100644 index 0000000..72252f2 --- /dev/null +++ b/utils_encrypted_credentials.go @@ -0,0 +1,22 @@ +package telegram + +import ( + "crypto/rsa" + + json "github.com/pquerna/ffjson/ffjson" +) + +func (ec *EncryptedCredentials) Decrypt(pk *rsa.PrivateKey) (*Credentials, error) { + if ec == nil || pk == nil { + return nil, nil + } + + data, err := decrypt(pk, ec.Secret, ec.Hash, ec.Data) + if err != nil { + return nil, err + } + + var c Credentials + err = json.Unmarshal(data, &c) + return &c, err +} diff --git a/utils_encrypted_passport_element.go b/utils_encrypted_passport_element.go new file mode 100644 index 0000000..cf6f785 --- /dev/null +++ b/utils_encrypted_passport_element.go @@ -0,0 +1,117 @@ +package telegram + +import ( + "strings" + + json "github.com/pquerna/ffjson/ffjson" +) + +func (epe *EncryptedPassportElement) DecryptPersonalDetails(sv *SecureValue) (*PersonalDetails, error) { + if !epe.IsPersonalDetails() || !sv.HasData() { + return nil, nil + } + + body, err := sv.Data.decrypt(epe.Data) + if err != nil { + return nil, err + } + + var pd PersonalDetails + err = json.Unmarshal(body, &pd) + return &pd, err +} + +func (epe *EncryptedPassportElement) DecryptPassport(sv *SecureValue, b *Bot) (*Passport, error) { + if !epe.IsPassport() || !sv.HasData() || !sv.HasFrontSide() { + return nil, nil + } + + /* + var p Passport + + body, err := sv.Data.decrypt(epe.Data) + if err != nil { + return nil, err + } + + if err = json.Unmarshal(body, &p.Data); err != nil { + return nil, err + } + + p.FrontSide, err = b.DecryptPassportFile(epe.FrontSide, sv.FrontSide) + if err != nil { + return nil, err + } + + if sv.HasSelfie() { + p.Selfie, err = b.DecryptPassportFile(epe.Selfie, sv.Selfie) + if err != nil { + return nil, err + } + } + + if sv.HasTranslation() { + p.Translation = make([][]byte, len(sv.Translation)) + for i := range sv.Translation { + p.Translation[i], err = b.DecryptPassportFile(epe.Translation[i], sv.Translation[i]) + if err != nil { + return nil, err + } + } + } + */ + + return nil, nil +} + +func (epe *EncryptedPassportElement) IsAddress() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeAddress) +} + +func (epe *EncryptedPassportElement) IsBankStatement() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeBankStatement) +} + +func (epe *EncryptedPassportElement) IsDriverLicense() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeDriverLicense) +} + +func (epe *EncryptedPassportElement) IsEmail() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeEmail) +} + +func (epe *EncryptedPassportElement) IsIdentityCard() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeIdentityCard) +} + +func (epe *EncryptedPassportElement) IsInternalPassport() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeInternalPassport) +} + +func (epe *EncryptedPassportElement) IsPassport() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePassport) +} + +func (epe *EncryptedPassportElement) IsPassportRegistration() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePassportRegistration) +} + +func (epe *EncryptedPassportElement) IsPersonalDetails() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePersonalDetails) +} + +func (epe *EncryptedPassportElement) IsPhoneNumber() bool { + return epe != nil && strings.EqualFold(epe.Type, TypePhoneNumber) +} + +func (epe *EncryptedPassportElement) IsRentalAgreement() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeRentalAgreement) +} + +func (epe *EncryptedPassportElement) IsTemporaryRegistration() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeTemporaryRegistration) +} + +func (epe *EncryptedPassportElement) IsUtilityBill() bool { + return epe != nil && strings.EqualFold(epe.Type, TypeUtilityBill) +} diff --git a/utils_id_document_data.go b/utils_id_document_data.go new file mode 100644 index 0000000..aef1b85 --- /dev/null +++ b/utils_id_document_data.go @@ -0,0 +1,16 @@ +package telegram + +import "time" + +func (idd *IdDocumentData) ExpiryTime() *time.Time { + if idd == nil || idd.ExpiryDate == "" { + return nil + } + + et, err := time.Parse("02.01.2006", idd.ExpiryDate) + if err != nil { + return nil + } + + return &et +} diff --git a/utils_passport.go b/utils_passport.go new file mode 100644 index 0000000..c828ea2 --- /dev/null +++ b/utils_passport.go @@ -0,0 +1,106 @@ +package telegram + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "crypto/rsa" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "encoding/base64" +) + +func decrypt(pk *rsa.PrivateKey, s, h, d string) (obj []byte, err error) { + // Note that all base64-encoded fields should be decoded before use. + secret, err := decodeField(s) + if err != nil { + return + } + + hash, err := decodeField(h) + if err != nil { + return + } + + data, err := decodeField(d) + if err != nil { + return + } + + if pk != nil { + // Decrypt the credentials secret (secret field in EncryptedCredentials) + // using your private key + secret, err = decryptSecret(pk, secret) + if err != nil { + return + } + } + + // Use this secret and the credentials hash (hash field in + // EncryptedCredentials) to calculate credentials_key and credentials_iv + key, iv := decryptSecretHash(secret, hash) + if err != nil { + return + } + + // Decrypt the credentials data (data field in EncryptedCredentials) by + // AES256-CBC using these credentials_key and credentials_iv. + data, err = decryptData(key, iv, data) + if err != nil { + return + } + + // IMPORTANT: At this step, make sure that the credentials hash is equal + // to SHA256(credentials_data) + if !match(hash, data) { + err = ErrNotEqual + return + } + + // Credentials data is padded with 32 to 255 random padding bytes to make + // its length divisible by 16 bytes. The first byte contains the length + // of this padding (including this byte). Remove the padding to get the + // data. + offset := int(data[0]) + data = data[offset:] + + return +} + +func decodeField(rawField string) (field []byte, err error) { + return base64.StdEncoding.DecodeString(rawField) +} + +func decryptSecret(pk *rsa.PrivateKey, s []byte) (secret []byte, err error) { + return rsa.DecryptOAEP(sha1.New(), rand.Reader, pk, s, nil) +} + +func decryptSecretHash(s, h []byte) (key, iv []byte) { + hash := sha512.New() + hash.Write(s) + hash.Write(h) + sh := hash.Sum(nil) + + return sh[0:32], sh[32 : 32+16] +} + +func match(h, d []byte) bool { + dh := sha256.New() + dh.Write(d) + + return bytes.EqualFold(h, dh.Sum(nil)) +} + +func decryptData(key, iv, data []byte) (buf []byte, err error) { + block, err := aes.NewCipher(key) + if err != nil { + return + } + + buf = make([]byte, len(data)) + cipher.NewCBCDecrypter(block, iv).CryptBlocks(buf, data) + + return +} diff --git a/utils_personal_details.go b/utils_personal_details.go new file mode 100644 index 0000000..b454d84 --- /dev/null +++ b/utils_personal_details.go @@ -0,0 +1,32 @@ +package telegram + +import "time" + +func (pd *PersonalDetails) BirthTime() *time.Time { + if pd == nil || pd.BirthDate == "" { + return nil + } + + bt, err := time.Parse("02.01.2006", pd.BirthDate) + if err != nil { + return nil + } + + return &bt +} + +func (pd *PersonalDetails) FullName() string { + if pd == nil { + return "" + } + + return pd.FirstName + " " + pd.LastName +} + +func (pd *PersonalDetails) FullNameNative() string { + if pd == nil { + return "" + } + + return pd.FirstNameNative + " " + pd.LastNameNative +} diff --git a/utils_secure_value.go b/utils_secure_value.go new file mode 100644 index 0000000..608bdb8 --- /dev/null +++ b/utils_secure_value.go @@ -0,0 +1,25 @@ +package telegram + +func (sv *SecureValue) HasData() bool { + return sv != nil && sv.Data != nil +} + +func (sv *SecureValue) HasFiles() bool { + return sv != nil && len(sv.Files) > 0 +} + +func (sv *SecureValue) HasFrontSide() bool { + return sv != nil && sv.FrontSide != nil +} + +func (sv *SecureValue) HasReverseSide() bool { + return sv != nil && sv.ReverseSide != nil +} + +func (sv *SecureValue) HasSelfie() bool { + return sv != nil && sv.Selfie != nil +} + +func (sv *SecureValue) HasTranslation() bool { + return sv != nil && len(sv.Translation) > 0 +}