UNIVERSITY OF THE PHILIPPINES MANILA COLLEGE OF ARTS AND SCIENCES DEPARTMENT OF PHYSICAL SCIENCES AND MATHEMATICS

RELIABLE AND ROBUST FILE TRANSFER SYSTEM OVER SHORT MESSAGING SERVICE

A special problem in partial fulfllment

of the requirements for the degree of

Bachelor of Science in Computer Science

Submitted by:

Axel Philip Advento

May 2018

Permission is given for the following people to have access to this SP:

Available to the general public Yes Available only after consultation with author/SP adviser No Available only to those bound by confdentiality agreement No ACCEPTANCE SHEET

The Special Problem entitled “Reliable and Robust File Transfer System over Short Messaging Service” prepared and submitted by Axel Philip Advento in partial fulfllment of the requirements for the degree of Bachelor of Science in Computer Science has been examined and is recommended for acceptance.

______Gregorio B. Baes, Ph.D. (candidate) Adviser

EXAMINERS: Approved Disapproved 1. Avegail D. Carpio, M.S. ______2. Richard Bryann L. Chua, Ph.D. (candidate) ______3. Perlita E. Gasmen, M.S. (candidate) ______4. Marvin John C. Ignacio, M.S. (candidate) ______5. Ma. Shiela A. Magboo, M.S. ______6. Vincent Peter C. Magboo, M.D., M.S. ______7. Geoffrey A. Solano, Ph.D. (candidate) ______

Accepted and approved as partial fulfllment of the requirements for the degree of Bachelor of Science in Computer Science.

______Ma. Shiela A. Magboo, M.S. Marcelina B. Lirazan, Ph.D. Unit Head Chair Mathematical and Computing Sciences Unit Department of Physical Sciences Department of Physical Sciences and Mathematics and Mathematics

______Leonardo R. Estacio, Jr., Ph.D. Dean College of Arts and Sciences

Abstract

This study presents a reliable and robust method for transferring files over Short Messaging Service. By testing the system against a set of repeated rigid test cases and fault injection methods, it has proven itself reliable and robust enough to survive problems found on an unreliable SMS service. Protection against adversaries are also implemented as a feature using security measures such as encryption and authentication.

Results show an effective alternative for sending and receiving files but show noticeable drawbacks in terms of speed. The work presented here has profound implications for future studies of utilizing low bandwidth but widely accessible means of communications such as Short Messaging Service and may help solve the problem of data transmission in places where Internet connectivity is unavailable or where other means of transmission of data is hazardous or impossible.

Keywords: File transfer, Short Messaging Service, Reliability, Robustness

ii Contents

Abstract ii

I. Introduction 1

A. Background of the Study ...... 1

B. Statement of the Problem ...... 4

C. Objectives of the Study ...... 6

D. Significance of the Study ...... 7

E. Scope, Limitations, and Assumptions ...... 8

II. Review of Related Literature 10

III. Theoretical Framework 13

A. Reliability ...... 13

B. Robustness ...... 13

C. Data Compression ...... 14

D. Binary-to-Text Encoding ...... 15

IV. Design and Implementation 17

A. Protocol ...... 17

iii B. Hardware Setup ...... 22

C. Software and Libraries Used ...... 22

D. Hardware and Software Requirements ...... 24

V. Results 26

VI. Discussions 37

VII. Summary and Conclusions 40

VIII. Recommendations 41

Bibliography 43

Appendix 47

A. Source Code ...... 47

Acknowledgements 157

iv I. Introduction

A. Background of the Study

File Sharing Methods and Technologies

File sharing can be defined as "a method of distributing computer files, … among a large number of users".[24] Such practice enabled users to share various digital data such as documents, multimedia, and computer programs across the world.

Files can be shared in a number of ways. They can be distributed using removable storage media, over wired connections, or wirelessly such as wireless Ethernet and mobile data.

Files were first exchanged using non-volatile physical media. In 1725, Basil Bouchon used punched paper rolls to store instructions for machines. In 1846, punch tapes became viable for use in recording telegrams. Data stored on these storage devices were more of instructions for machines rather than actual recorded data, however. In 1880, Hermal Hollerith invented recording data on punch cards which ultimately became a format that IBM used in the following decades.

Afterwards, various magnetic media such as magnetic tapes (used in 1951 as data storage medium), drum memory (1946), hard disk drives (1956), and floppy disks (1971) were used as storage media. Magnetic tapes and floppy disks saw widespread use as removable media. Around mid-1980s, specifications for storing digital signals on optical discs are completed which led to the development of Compact Discs (CDs), Digital Video Discs (DVDs), and Blu-ray Discs (BD).

1 Electronic flash memory were introduced for storing digital data later on. Examples include CompactFlash cards, Secure Digital (SD) cards, Memory Sticks, and USB Flash Drives.

Concurrently, file sharing has also been a practice through wired connections. During 1970s up to late 1980s, file sharing was mostly done over landline telephones and modems using systems such as , BBS, and XModem. Internet Service Providers (ISPs) emerged on 1990s and the Internet became fully commercialized by 1995. This gave way to modern methods of distributing files such as the File Transfer Protocol (FTP), the World Wide Web, Internet Relay Chat (IRC) programs, Instant Messaging, and later on, the BitTorrent protocol.[13]

Wireless connectivity made the Internet accessible to people who don't have access to wired connections and people living on locations where setting up infrastructure for wired services would be difficult or impossible. In 1996, access to mobile web was made possible with the introduction of the Nokia 9000 Communicator phone. It was done over 2G cellular networks, which initially do not offer data services. During the early 2000s, Wireless Access Protocol (WAP) and i-mode achieved popularity as means of accessing the mobile web and Internet services. In

2000, General Packet Radio Service (GPRS) was introduced as a packet-switched data service, which extends Global System for Mobile (GSM) Packet circuit-switched data capabilities. While in 2003, Enhanced Data rates for GSM Evolution (EDGE) was deployed on GSM networks which allows improved data transmission rates over GPRS.

2 The first Internet-ready satellite was made available for consumers in 2003. Satellite Internet was targeted mainly to rural areas as an alternative to traditional Internet connections such as dial-up and ADSL. The first commercial Third Generation (3G) network was also finally launched in the same year.

With the advent of 3G cellular networks and its extensions, higher data speeds and better security compared to 2G networks are observed on 3G-capable devices. However, adoption in some countries are slowed down due to the large cost of upgrades and spectrum licensing fees. In 2013, Fourth Generation technologies (4G) started to roll out. Technologies can only be considered 4G if "a substantial level of improvement in performance and capabilities with respect to the initial third generation systems now deployed".[8]

Short Messaging Service (SMS) and Its State in the Philippines

Short Messaging Service is a component of web, telephone, and mobile device networks that enables users to send and receive text messages. SMS is part of standardized communication protocols that were defined in 1985 as part of the GSM standards. [25]

SMS messages are sent to Short Message Service Centers (SMSC), which then attempts to forward the message to the recipient. This delivery however is "best-effort". In case the SMSC cannot reach the intended recipient, it keeps the message for a duration set by the mobile operator and retries until the recipient becomes available again, the message expires, or the retry limit set by the operator has been reached. This also means that there are no guarantees that the message will

3 be delivered on time or be delivered to the recipient at all. Despite being best-effort, SMS is an almost reliable service with an observed failure rate as high as 5.1% during normal operation conditions.[12]

The Philippine Statistics Authority reported in 2017 that an estimated 8,957,952 people are subscribed on an Internet Service Provider which includes wired DSL, cable Internet, and wireless Internet. The reported number of active mobile telephone service subscriptions, on the other hand, is 130,319,459, which is more than the country's total population.[15] Various studies and news sources also consistently place the Philippines as the "text messaging capital of the world".[5]

B. Statement of the Problem

With the current technologies available to consumers today, ideally anyone should be able to send and receive digital data conveniently anytime and anywhere. Unfortunately, these methods and technologies are not as accessible or widely available in the real world.

First, transferring files over wired connections is limited only to areas where wired telephone networks and ISPs had laid out infrastructure for their services.

These areas exclude remote rural communities and areas where building infrastructure would be challenging or just impossible.

Wireless Internet, on the other hand, has its own challenges as well. Satellite Internet typically requires a completely clear line of sight and is sensitive to even minor obstructions. 3G and 4G networks have limited coverage and availability

4 compared to traditional 2G networks. An assessment of 2G and 3G mobile phone networks found that UMTS networks perform worse compared to GSM networks in terms of coverage.[20] These 3G networks will have problems covering an area with an otherwise good GSM coverage unless aided by a significantly increased power consumption and additional equipment. Signals attenuate more and higher noise figures can be observed on higher frequency channels that are used on 3G and 4G networks (~5MHz and 40MHz respectively). 3G and 4G technologies also demands for stricter signal-to-noise ratio requirements.[22]

While network carriers would like to advertise that they have "100% 3G and 4G coverage", that would not be exactly true.[7] As mentioned earlier, theoretically 3G and 4G networks would perform worse than 2G networks in terms of coverage. Additionally, in the Philippines, 3G and 4G are not as widespread as real-world data shows. Over 63 percent of Filipinos are still not connected to the Internet. This is mainly attributed to lack of infrastructure and lack of mobile Internet coverage and network access.[11] On a later report, it was found out that in the Philippines mobile Internet users get 3G or 4G connections only 68.63% of the time.[13] Compared with the rest of the world, the Philippines severely lags behind on modern mobile Internet both in terms of speed and availability.[14]

Transporting files over physical non-volatile removable media has its share of its own problems too. There is still a possibility of the data being corrupted in the removable storage being used. There are also chances of the removable media being damaged or lost during transit. Weather, terrain, and other hazards will heavily affect

5 the success and speed of this method. In case of failure, the whole process of copying to and transporting the physical media needs to be repeated again.

C. Objectives of the Study

With the limited availability and accessibility of technologies that enable people to connect and share digital data, there is a need to produce a method that makes sharing digital data more accessible. For this reason, it appeared reasonable to attempt to develop a system with the following criteria:

1. The system should be able to send and receive files over SMS. In terms of availability and coverage, 2G is a suitable candidate despite being an older technology. SMS is also a viable as data carrier due to its simplicity, low cost, and its ubiquity eliminating the need for specialized equipment and technologies.

2. The protocol to be used should be reliable and robust enough to address the following issues inherent on Short Messaging Service:

a. SMS is still just a best-effort service.[12] The system should be able to request and retransmit lost messages during the file transfer.

b. There is no guarantee that SMS messages will arrive instantaneously nor arrive in order.[12] The system should have a mechanism to sort the messages received in order.

c. There is also a possibility that an SMS message intended for a previous state or session has been received. The system should be able to handle such cases.

6 d. In case the received SMS messages are incomplete or erratic, there should be a verification mechanism to validate each message received by both sending and receiving ends.

3. The protocol must allow the users to transfer files securely barring any adversary to be able to read or manipulate the files and keys being transferred. This can be done by a combination of the use of the Advanced Encryption Standard (AES) and Rivest-Shamir-Adleman (RSA) algorithms, which is used by majority of technologies used today.

4. The system must give the users a convenient way to to pair with another user's system, a requirement for transferring files securely.

5. The system should be able to transfer files over SMS regardless of their type or content.

D. Significance of the Study

The results of this study will benefit people who have the need to share digital data but are unable to due to other methods being unavailable, inconvenient, or hazardous. The system that will be developed will serve as an alternative to other methods of transferring files.

This system can be used by private organizations and government agencies doing data collection that requires information gathered be collected as soon as possible, such as beneficiaries of government welfare programs that require immediate assistance, disaster recovery assistance data, and earthquake/flood mapping data.

7 This can also serve rural government offices that do not have stable Internet connection but still require constant information exchange such as police, local government, etc.

People in the scientific field may utilize this to communicate important metrics such as volcanic activity and weather data when wired or wireless Internet is unavailable and conditions for manual transporting data are unfavorable.

In case 3G and 4G connectivity fails but the location has good reception for sending and receiving SMS messages, in automated election systems this is viable as a last resort for wireless communications before defaulting to manual transport of storage media. This can alleviate concerns over safety of data and personnel when terrain, weather, and other factors increase hazard if transporting the data physically.

Finally, the protocol, the software, and the system developed can be a framework for development of future technologies that can take advantage of SMS and its successors in the future beyond third and fourth generation networks.

E. Scope, Limitations, and Assumptions

This study will be limited to the bounds specified below:

1. While the protocol to be formulated is intended to be platform and hardware independent, the software to be developed for this study will be limited only to the phones and modems supported by the Gammu project and most Android devices.

8 2. There will be a huge drawback in terms of transmission speed which is caused by the non-instantaneous delivery and slow nature of SMS.[6][12][18][19]

3. The software will only allow transmission of files of sizes less than or equal to 10 megabytes at a time due to the limit of 160 7-bit characters for each SMS message, which will constrict the number of characters reserved for both the segment numbers and the payload. This should be enough for common file sizes of 2-12 kilobytes for images, 4-20 kilobytes for documents, and 3-4 megabytes for a full mp3 song.[17]

4. There should be enough signal strength and network coverage for the hardware components to be able to send and receive SMS messages properly.

5. This study assumes that the sender and receiver Subscriber Identification Modules (SIMs) and their respective network subscriptions are allowed to send and receive unlimited SMS messages.

6. Man-in-the-middle (MiTM) attacks would not be entirely impossible. The only security provided by the protocol to be formulated would be authentication and encryption for the keys and files exchanged using the combination of RSA and

AES.

7. Denial-of-Service Attacks by an adversary will not be addressed in this study.

9 II. Review of Related Literature

People who have limited or no access to wired and wireless technologies such as ADSL, 3G networks, and 4G networks will benefit from having a more accessible alternative method of sending data. Sending files over SMS is one possible way to achieve this. Despite the topic being given limited attention, sending binary data over SMS has been explored in various ways.

As early as 2008, an SMS-based transport layer has been designed and developed by Rao et. al.[18] Having GPRS and other technologies being unavailable in some areas has been a motivation for the study with the intention of completely bypassing GPRS networks and mobile ISPs. It was implemented by converting TCP packets and UDP datagrams into payloads and headers carried by SMS messages, sending significantly smaller packets or datagrams in batches, if possible. It was found out that such scheme is efficient for infrequent and non-bulky data transfers such as micro-browsing or Yellow Page searches. Its use for frequent and bulky data transfers is considered expensive and unoptimized however. Additionally, using the scheme for time critical applications is highly discouraged.

An attempt to provide a free Internet access has been developed by using an Internet-connected server and a specialized client browser that communicates with each other via SMS.[19] This method however is intended only for basic web browsing, stripping off images and other unnecessary elements leaving only the HTML source. It was concluded that while web browsing is possible, the service was deemed too slow for practical use despite the attempts of stripping out elements to further speed up the browsing experience.

10 A method of sending images over SMS as an alternative to Multimedia Messaging Service (MMS) has been presented on a separate study.[21] Motivated by the unavailability of MMS in some areas, the study attempts to provide a more accessible alternative. Unlike the previously discussed study by Rao et. al., the method used in this study simply converts the images into a text file using Base 64 encoding and sends the encoded data through several SMS messages. This eliminates the overhead introduced by the TCP or UDP protocols. However, SMS's default 7-bit encoding seems to be underutilized by using Base 64 as the encoding scheme for the data.

Another study tested the viability of data transmission using SMS as an alternative over GPRS. It was found out that SMS in general works in a slow but consistent pace while GPRS using UDP is faster but in an inconsistent manner.[6] Additionally, using the data gathered in the study, it was concluded that UDP will always be faster than SMS when sending large amounts of data even if UDP retries to send the correct data and if SMS had perfect transmission rates. Similar to the method of sending images over SMS previously discussed earlier, the whole file is encoded and split into different SMS messages. This time, a header is included together with a CRC32 checksum. The CRC32 checksum is used to verify the integrity of each message while the header value contains the queue number, which is requested by the receiver in case of timeouts or missing parts.

From the existing studies previously presented and discussed, it can be inferred that SMS as a data bearer will be slow. Additionally, using SMS for time critical applications should be strongly discouraged. An SMS message's size would be too

11 small to fit a single file so splitting a file into smaller parts would be unavoidable. Using an efficient encoding scheme would also be needed due to SMS messages' textual nature. SMS messages aren't guaranteed to be delivered at all times so a method to retransmit missing parts would also be necessary.

12 III. Theoretical Framework

A. Reliability

Reliability can be defined as a quality of a protocol that ensures the accuracy and integrity of transmitted data. Reliable protocols guarantee that the data will be delivered to the recipient. This ensures that at least one copy of the message will be always delivered to the recipient. A reliable protocol will always verify that all data transmitted is received in the correct order and is intact.

On the other hand, reliability can be defined in terms of failure. If a protocol can handle a certain set of well-defined and understood failures, then it can be considered reliable with respect to those failures.[27]

For the purpose of this study, we will use both approaches but with more emphasis on guaranteed delivery, integrity, and correct order of delivered data. During the development and implementation of the protocol, certain set of tests will be done to ensure that the protocol will be reliable against a limited set of well-defined failure scenarios.

B. Robustness

Robustness can be defined as "the degree to which a system or component can function correctly in the presence of invalid inputs or stressful environmental conditions".[26] Fuzz testing is a common tool for demonstrating a system's robustness. Alternatively, fault injection can be used for testing robustness.

13 Fault injection techniques will be used in testing the protocol to be developed, given its effectiveness in testing for error handling in code paths as well as its predictability so that bugs can be easier to track and fix. These will be implemented in test cases where the input is mutated in a controlled manner and the use of custom components in place of the ones expected by the system, which produces repeatable and well-defined faulty scenarios.

C. Data Compression

Data compression is defined as encoding information using fewer bits than the original representation. Compression can be classified as either lossless or lossy compression. Lossy compression minimizes the space needed by removing parts of data that are deemed unnecessary or less important. Lossless compression uses various methods in reducing size without loss of data.

14 The common factors to be considered when choosing a compression algorithm are compression/decompression speed, memory usage, and compression ratio. Compression ratio is defined as the ratio between the uncompressed size and the compressed size of the data:

CompressionRatio = UncompressedSize CompressedSize Since this project deals with SMS, a slow data bearer, compression is a necessary tool to decrease the time needed for a transmission to be completed. This project will also not be using lossy compression.

The compression algorithm that will be used for this project will be selected based on compression ratio alone. Compression/decompression speeds and memory usage do not matter since modern computers can handle these without problems and delays produced by a slow compression algorithm is insignificant if compared to the total time needed to transmit the compressed data. With this, the LZMA algorithm is chosen based on benchmark results.[2][10] The XZ utility program will be used for handling LZMA and LZMA2 compression algorithms.[3]

D. Binary-to-Text Encoding

Binary-to-Text encoding is simply the action of encoding binary data into plain text. This is necessary if the transmission method does not support sending of binary data. Common examples include Base16, , and MIME.

For the purposes of this study, a slightly modified encoding is used due to its high efficiency of 80%, which is more efficient than common encoding

15 schemes like uuencode or Base64. The encoding scheme is also optimal for SMS's default 7-bit character encoding reducing the size of the encoded data while at the same time, taking advantage of a slightly larger character set.

16 IV. Design and Implementation

A. Protocol

Overview

The TransmiSMS file transfer protocol* is designed to be a simple, connection-oriented, reliable, and robust protocol. The protocol aims to facilitate sending and receiving of files over SMS using the least amount of SMS messages sent as possible while providing essential features that provides reliability, robustness, security, privacy, and convenience.

The most basic unit of data used by the TransmiSMS file transfer protocol is called a Protocol Data Unit (PDU). Each PDU is contained in a single SMS message and contains identifiers for its type, Session ID, its segment number, checksum (if already connected), human-readable instructions for force disconnecting (when not yet connected), and user data.

Transferring files and pairing with a peer starts with establishing a connection. The connection process is a three-way handshake, initiated by the sender/initiator.

This prevents the sender from flooding the receiver if the receiver is not yet ready to receive data and to prevent unsolicited file transfers. During the handshake, each PDU contains human-readable instructions on how to reject and force disconnect the connection.

* Not to be confused with FTP; this protocol is not related to the File Transfer Protocol specified by RFC 959.[16]

17 Terminating the connection is also done via a three-way handshake normally initiated by the sender/initiator. This ensures that all parties are notified about each other's disconnection state. After sending the last PDU, the initiator waits for a while to retransmit missing PDUs in case the other end has failed to receive them.

File transfer and key exchange operations are managed via sessions. Each session is assigned a Session ID which is a 128-bit Universally Unique Identifier (UUID) negotiated during the establishment of the connection. UUIDs have an almost zero percent chance of collisions so no two Session IDs will be the same. This is done so that PDUs not belonging to the current Session can be safely ignored and prevent them from interrupting the current operation.

The protocol will also ignore duplicate PDUs and PDUs not appropriate for the current connection state. Invalid and malformed PDUs will also be ignored.

Pairing

Before the sender and the receiver can use security features such as encryption and authentication, they must be paired first prior the file transfer. Pairing means exchanging necessary RSA public keys for encryption and authentication. This can be done over the air or manually by importing the appropriate keys.

Pairing the keys over the air is a three-step process. An initiator party must send a pairing request to the other end in order to establish the connection. After the responder has accepted the request, they both exchange Base85-encoded encryption

18 and authentication keys split into multiple parts. After confirming that both parties have received the keys correctly, the initiator then requests disconnection.

Optionally, after receiving the encryption and authentication keys, the parties are allowed to further authenticate they keys that they have received. They are presented with an easy to read set of words which they communicate and compare over a secure channel to confirm that no man-in-the-middle attack has taken place.

File Transfer

File transfer starts with the sender requesting a file transfer request to the receiver. After the connection is established and the receiver has agreed to receive the file, both ends exchange information for the file transfer.

If security features are enabled, the receiver first generates a new AES key for file encryption and encrypts it using the sender's RSA public key. The receiver then sends the encrypted key split into multiple parts via multiple INIT PDUs. The sender then decrypts the file encryption key using its own RSA private key. The sender can compress the file first using LZMA. The sender also needs to sign the unencrypted file using the receiver's authentication key. These can be done concurrently. The file can then be processed for encryption via the AES key afterwards. Optionally, the sender can further compress the encrypted file, if its size can be further reduced. It can then be Base85-encoded and split into segments. The sender then sends the number of segments, if compression is used, and the encoded RSA signature to the receiver via a series of META PDUs.

19 After receiving the META PDU, confirming the information provided, and allocated the necessary resources for receiving the file, the receiver will send a READY PDU to the sender to start the file transfer. The sender then proceeds to send the processed and encoded data using a series of DATA PDUs and sending and END PDU to signify that the sender has completed sending all segments.

After the receiver has confirmed receiving the all the DATA PDU segments, reordered the DATA PDUs received, assembled and processed the encoded data, and authenticated the signature sent by the sender, it sends its own END PDU to confirm that file transfer is successful. Upon receiving the END PDU, the sender then proceeds with the disconnection procedure.

Without the security features, the receiver just sends an empty INIT PDU to the sender to signify acceptance of the request. The sender first calculates the SHA-512 checksum of the file. The sender then skips the encryption and proceeds with the compression. Using the same encoding, the data is then encoded and split into parts. Using a META PDU, the sender sends the number of segments, the checksum, if compression is used, and the file's encoded checksum to the receiver. The whole process of sending and receiving the DATA PDUs proceeds along with the verification of the received data using SHA-512 instead of an RSA signature.

Retransmission and Timeouts

The protocol assumes that all previously sent PDUs are successfully received by the receiver unless a NACK request for retransmission is received. This is done to reduce the number of PDUs the receiver needs to transmit such as ACK for every

20 PDU received and to take advantage of SMS's low failure rate on normal operational conditions. Retransmission requests are sent in batches inside the least amount of NACK PDU necessary. On some states during the transmission, resending a PDU signals a request for retransmission of the previously sent PDU. PDUs can also retransmitted in case of timeouts.

The maximum PDU lifetime is set as 60 seconds. Not receiving any PDU for the duration of the maximum PDU lifetime x 2 will result in a timeout. The user will then be prompted if he/she wants to continue or if the connection must be terminated immediately.

Security

For file encryption, a 128-bit symmetric AES key will be used. Cipher Block Chaining (CBC) mode of operation for AES will be used as well. Additionally, Public Key Cryptography Standards #7 (PKCS#7) will be used for padding for added security. For file authentication, a 2048-bit asymmetric RSA key will be used together with PKCS#1 with SHA-512 hashing. The AES key that will be transmitted will be further encrypted using a 2048-bit asymmetric RSA key with Optimal Asymmetric Encryption Padding (OAEP) with SHA-256 for Mask Generation Function 1 (MGF1) padding.

To reduce predictability, for every file transfer session, a new AES key is generated. Additionally, only public keys are exposed during the pairing process. The user is also warned if the already paired peer requests to redo the pairing process.

21 During the file transfer, when receiving the encrypted AES key, the sender can immediately verify the receiver's authenticity. The receiver, however, can only verify the sender's authenticity if the sender failed to decrypt the encryption key sent or after all the parts of the file is sent successfully but the receiver cannot decrypt the file using its own keys. The receiver must be able to verify the authenticity of the decrypted file using the stored keys the receiver and the sender has exchanged prior the transmission.

Because the protocol allows a kill switch for the connection, an adversary with enough resources to spoof the other end can always terminate the connection prematurely. However, the adversary won't be able to fake a file transfer successfully nor view the contents of the keys and the files being exchanged.

B. Hardware Setup

The Hardware to be used for this project will consist of a sender and a receiver host. Each host will be a computer connected to either a compatible mobile phone or an Internet dongle which will function as a GSM modem. There should also be ample signal coverage for these modems to operate normally. The sender and receiver hosts can also switch roles depending on the tests needed to be performed.

C. Software and Libraries Used

In this study, Java is used as the programming language due to its cross-platform nature and the number of libraries available to be used. It has a native support for classes, events, and threading which is useful for the development of the

22 system and its components. The JavaFX platform is used for the Graphical User Interface (GUI) due to its consistent look and feel across different operating systems and its logical separation between program logic, GUI layout, and design.

As mentioned earlier, LZMA is used as the compression algorithm and a modified implementation of ASCII85 is used as the binary-to-text encoder due to their excellent compression ratio and encoding efficiency.

Gammu SMSD will be used as a service layer to interface with various mobile phones, GSM modems, and Internet dongles from different vendors removing the need to send AT commands directly.[4] A custom Android companion app was also developed to communicate with Android devices.

A combination of RSA and AES is used for encrypting data to be transfered over SMS. AES is used for encrypting the raw data to be sent while RSA is for encrypting the AES key to be shared between the sender and receiver. RSA is also used for authentication.

For verifying each PDU's integrity, CRC8 is used for its simplicity and small size. RSA authentication will also serve as verification for the file received. SHA-512 will be used in place of RSA for verifying files if RSA authentication is disabled.

Pretty Good Privacy (PGP) word list is used when manually authenticating key exchange over the air to detect man-in-the-middle attacks.

23 D. Hardware and Software Requirements

In order for the system to be fully operational, it requires the sender and receiver computers to meet the following requirements:

Hardware

• GSM modem or mobile phone capable of sending and receiving SMS messages with Gammu SMS support or an Android device capable of sending and receiving SMS (running at least Android 4.4)

• Bluetooth interface or USB/Serial Cable that comes with the GSM modem or mobile phone

• An available USB/Serial port when using USB/Serial interfaces

• 200 MB of available RAM or higher

• 20 MB of available hard disk space or higher

Software

• GNU+Linux, BSD, Mac OS, or Windows operating systems

• Java Runtime Environment 8 or newer

• Gammu 1.3 or newer (and a complete setup for the chosen Gammu backend)

24 • Drivers for the devices to be used

25 V. Results

The TransmiSMS File Transfer program is a JavaFX-based GUI frontend that enables users to send and receive files reliably over SMS. This file transfer system uses the TransmiSMS protocol designed for the purposes of this study.

The program features contact management, which allows the user to add, edit, and remove contacts. The user can then send and receive files, as well as pair and exchange security keys from these contacts. The program's toolbar allow the user to do the previously mentioned actions as well as the option to send files either via a secure or an unsecure connection.

Figure 1. Main Window with no Contacts Added yet

26 Figure 2. Add/Edit Contact Dialog

As soon as the program starts, it starts listening for connection requests from both registered contacts and non-registered users. It can be disabled by pressing the "Stop accepting" button on the infobar.

27 Figure 3. Main Window, Listening for New Connections

Figure 4. Main Window, Not Listening for New Connections

28 The program also detects if there are problems encountered from the SMS service being used. If such problems exist, the user must resolve these issues first before proceeding. Supported SMS services are Gammu SMSD via PostgreSQL and the Android Companion app developed mainly to enable the program to interact with Android devices.

Figure 5. Detected Problems with SMS Service

The program automatically saves the files received on a predetermined location. The user can change this on the preferences window, along with SMS backend-specific settings.

29 Figure 6. Preferences Window

The user will be prompted if he/she receives a file transfer request. A detailed prompt containing the senders' name, the sender's number if the sender is not yet in the user's contact list, the file name, and if the transfer will use a secure connection will be displayed.

30 Figure 7. Secure File Transfer Request Prompt

Figure 8. Unsecure File Transfer Request Prompt

The user will also be prompted in case he/she receives a pairing request. The sender's details are displayed on the prompt and if the pairing request has already been done before.

31 Figure 9. Pairing Request Prompt

Figure 10. Renew Pairing Request Prompt

If the receiver rejects the file transfer or pairing request, the sender is prompted immediately that the receiver has rejected the request.

32 Figure 11. Peer Reject/Disconnect Notification

Upon accepting the request, the connection will be established and the sender and receiver will then begin exchanging information. At this point, the progress of the key exchange or the file transfer will not yet be displayed.

Figure 12. Connection Established

33 After successfully exchanging information with each other, the sender and receiver will begin the transmission. The transmission progress will be both displayed on the sender's and receiver's GUI. The users will be then notified if the file transfer has been successful.

Figure 13. Sending File in Progress

34 Figure 14. Receiving File in Progress

Another feature implemented on this program is enabling users to manually verify keys exchanged by comparing the displayed PGP words on their screen. This can be done over a secure channel or at least on a trusted voice channel. The words are designed to be distinct from each other even when dictated over a phone call. This prevents man-in-the-middle attacks by an adversary.

35 Figure 15. Key Exchange Verification using PGP Word List

36 VI. Discussions

The conceived TransmiSMS program and protocol aims to provide users a more accessible alternative for sharing digital data. It uses SMS as a data bearer to make transmission of files possible without the need for an Internet connection provided by a wired or wireless provider. It can even take advantage of 2G's larger coverage that wired, 3G, and 4G connections are not able to achieve.

Using a set of repeated rigid test cases and fault injection during the development process and testing, both simulated and real world tests have all been successful. The following scenarios are tested during the simulations and if possible, on real hardware:

• delayed delivery of messages

• out of order delivery of messages

• tests if timeouts due to a faulty component will trigger in a timely manner

• occasional sending of duplicate PDUs

• occasional sending of PDUs not appropriate for the receiver's current state

• occasional sending of PDUs with invalid CRCs

• occasional corruption of PDUs received

• occasional sending of unrecognizable PDUs

37 • sending of PDUs to the wrong intended recipient

• consistent sending of extra PDUs with wrong Session IDs during an operation

• sending of invalid security information

• all combinations of invalid, empty, truncated, and missing values for all PDUs used by the protocol

The system proposed and developed proved to be reliable and robust enough to survive all these problems presented to it.

While the system can cope up with hardware problems up to a limited extent, total hardware failure and severe network problems can still render it useless. For instance, if the SMS device used suddenly hangs up, the user can still intervene, reset the device, and retry the connection. Network outages, however, won't even let the system to send a single message in the first place.

Secure connections are also offered by the system as an additional feature. SMS, a relatively old technology, is not entirely secure when faced with an adversary equipped with specialized hardware such as a Stingray phone tracker and sniffer to perform a MiTM attacks. The protocol designed introduced security measures to protect sensitive data being transmitted from such adversaries. While attackers can read plaintext SMS data and possibly interrupt ongoing file transfers, integrity of the files and keys sent and received will not be compromised. These adversaries will never be able to successfully spoof the sender's or receiver's identity nor sniff the encrypted data as long as the security features are enabled.

38 Connection setup times are measured to be around 5 seconds up to a full minute. Transfer speeds can be variable as SMS transmission speeds can vary depending on the network condition or if the SMS device decides to slow down sending of messages. Assuming an uncompressed and unencrypted file is sent over transmission speeds of one SMS per 5 seconds, it can reach speeds up to 11.1 kbps sans the connection and teardown phases. That is around 20% of the maximum speed of a 56.6kbps dial-up modem.

Unfortunately, SMS's sluggishness as a data bearer still became apparent despite the fact that measures to reduce the total amount of data to be transmitted are done. However, such measures are still invaluable in making transmission speeds more palatable to the end user.

39 VII. Summary and Conclusions

A reliable and robust file transfer system over SMS has been developed to address the needs of users with limited or no access to technologies normally used to send and receive data. It was proven reliable and robust enough to survive problems related to SMS's unreliable nature on both simulated and real world tests.

Various methods to reduce the total size of data transmitted has been implemented. Additional security measures such as encryption and authentication has also been implemented to provide protection against well-funded adversaries.

Despite successes in file transmission over SMS, its speed remains as its greatest drawbacks. Setup and teardown times are less than ideal in some cases. On a very congested or faulty network, the system would probably be still running but would run on a greatly reduced speed.

It is not recommended to use this for everyday use if other faster technologies are readily available. This system can still prove useful as an effective alternative in remote places where such technologies won't be accessible or when physically transporting data to another location might be hazardous or impossible.

40 VIII. Recommendations

While the resulting protocol and system proved to be reliable and robust for file transfers via SMS messages, there are still other areas that can be further improved:

• Ability to pause and resume file transfer sessions especially for larger files.

• An Android implementation to eliminate the need for a separate computer when transferring files. The implementation must also consider quirks present when developing on Android. This includes reduced performance during sleep, applications and services being dynamically woken, paused, or killed on memory-intensive loads, and an entirely different UI framework that also affects decisions when implementing multithreaded tasks.

• The use of Content Providers, which are companies and organizations that are allowed by mobile network operators to send and receive SMS messages at a different priority and at a faster rate. The source code is made to be easily extensible in case a need to implement a new SMS Service for a new device or backend arises.

• Further improvements to speed up connection setup time and teardown.

• Fully secured communications which might be possibly provided using open-source projects such as Signal and Silence

41 • Applications other than file sharing that requires exchange of binary data.

• Possible reuse of the protocol for future iterations and successors of SMS over future generation networks.

42 Bibliography

[1] Augustyniak, P. & Tadeusiewicz, R. (2009). Ubiquitous Cardiology: Emerging Wireless Telemedical Applications: Emerging Wireless Telemedical Applications. IGI Global.

[2] Collin, L. (2005). A Quick Benchmark: GZIP vs BZIP2 vs LZMA. Retrieved from https://tukaani.org/lzma/benchmarks.html

[3] Collin, L., (2016). XZ Utils. Retrieved from https://tukaani.org/xz/

[4] Čihař, M. (2018). [GW]ammu. Retrieved from https://wammu.eu/gammu/

[5] Dimacali, T. (2010). Philippines still Text Messaging Champ -US Study. GMA News. Retrieved from http://www.gmanetwork.com/news/scitech/content/19883 2/philippines-still-text-messaging-champ-us-study/story/

[6] Dykimching, A.M., Lee, J.A.A., & Yu, W.E. (2011). A Study on the aspect of data transmission on SMS interface. Journal of Information Security Research, vol. 2, no. 1, pp. 30-39.

[7] Globe Telecom (2014). Globe Telecom completes HSPA+ rollout; network now 100% 4G. Globe Telecom Press Release. Retrieved from http://www.globe.com.ph/press-room/network-now-100-4g

[8] International Telecommunication Union (2010). ITU World Radiocommunication Seminar highlights future communication technologies. Retrieved from https://www.itu.int/net/pressoffice/press_releases/2010/48.aspx

43 [9] Internet World Stats (2017). Asia Marketing Research, Internet Usage, Population Statistics and Facebook Subscribers. Retrieved from https://www.internetworldstats.com/asia.htm#ph

[10] Klausmann, T. (2008). GZIP, BZIP2, and LZMA Compared. Retrieved from http s://web.archive.org/web/20130106193958/blog.i-no.de//archives/2008/05/08/ind ex.html

[11] McKinsey & Company (2015). Offline and falling behind: Barriers to Internet adoption. Retrieved from https://www.mckinsey.com/industries/high-tech/our-in sights/offline-and-falling-behind-barriers-to-internet-adoption

[12] Meng, X., Zerfos, P., Samanta, V., Wong, S.H.Y., & Lu, S. (2007). Analysis of the Reliability of a Nationwide Short Message Service. 26th IEEE International Conference on Computer Communications, pp. 1811-1819.

[13] OpenSignal (2016). Global State of Mobile Networks, August 2016. Retrieved from https://opensignal.com/reports/2016/08/global-state-of-the-mobile-network

[14] OpenSignal (2018). The State of LTE, February 2018. Retrieved from https://opensignal.com/reports/2018/02/state-of-lte

[15] Philippine Statistics Authority (2017). Philippines in Figures 2017 p. 11. Retrieved from https://psa.gov.ph/sites/default/files/PIF_2017.pdf

[16] Postel, J., & Reynolds, J. (1985). RFC 959. FILE TRANSFER PROTOCOL (FTP)(Oct. 1985), Retrieved from ftp://ftp.rfc-editor.org/innotes/rfc959.txt

44 [17] QaamGo Media GmbH. Average File Sizes. Retrieved from https://blog.online-convert.com/average-file-sizes

[18] Rao, S., Vora, R., Dhar, S., Salvi, S., & Kumar, V. (2008). Development of a Transport Layer using SMS. 7th International Conference on Cognitive Systems, India.

[19] Sam, A. (2009). Free Wireless Internet through SMS. ADSA Life Development, Sweden.

[20] Scharnhorst, W., Hilty, & L., Jolliet, O. (2006). Life Cycle Assessment of Second Generation (2G) and Third Generation (3G) Mobile Phone Networks. Environment International 32.5 (2006) pp. 656-675.

[21] Shirali-Shahreza, M. & Shirali-Shahreza, S. (2009). Sending Pictures by SMS. 2009 11th International Conference on Advanced Communication Technology.

[22] Tolstrup, M. (2015). Indoor Radio Planning: A Practical Guide for 2G, 3G and 4G pp. 328-329, 349-350. John Wiley & Sons.

[23] Yahoo! & The Nielsen Company (2009). Yahoo Nielsen Internet Habits Study for

the Philippines. Yahoo!-Nielsen Press Release. Retrieved from https://www.scrib d.com/doc/13661672/Yahoo-Nielsen-Internet-Habits-Study-for-the-Philippines

[24] Collins English Dictionary - Complete & Unabridged 2012 Digital Edition. William Collins Sons & Co. Ltd., HarperCollins Publishers.

45 [25] GSM Doc 28/85 "Services and Facilities to be provided in the GSM System" rev2, June 1985

[26] IEEE Standard Glossary of Software Engineering Terminology, IEEE Std 610.12-1990

[27] Reliable Request-Reply Patterns. ØMQ - The Guide. Retrieved from http://zguide.zeromq.org/page:all#reliable-request-reply

46 /* Appendix compileJava { //options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" options.compilerArgs << "-Xlint:all" } */

jar { A. Source Code manifest { attributes 'Implementation-Title': 'Transmisms-core', 'Implementation-Version': version, 'Main-Class': mainClassName } exclude 'META-INF/*.RSA', 'META-INF/*.SF','META-INF/*.DSA' // exclude Dummy SMS Service Backend and digests to avoid problems Simulator: } task fatJar(type:OneJar) { mainClass = mainClassName } from gevent import monkey; monkey.patch_all() # monkey all the things! import zmq.green as zmq artifacts { import gevent archives(jar) { group 'com.transmisms.smsftp' name 'smsftp-protocol' } initiator_address = 'tcp://127.0.0.1:8768' } responder_address = 'tcp://127.0.0.1:8769' mediator_address = 'inproc://dummymediator' repositories { jcenter() // because we love https and supersets mavenCentral() mavenLocal() def filter_ping(address1, address2, identity): } ctx = zmq.Context.instance() s1 = ctx.socket(zmq.PAIR) configurations { s1.bind(address1) integrationCompile.extendsFrom testCompile s1.linger = 0 integrationRuntime.extendsFrom testRuntime stagingCompile.extendsFrom testCompile s2 = ctx.socket(zmq.PAIR) stagingRuntime.extendsFrom testRuntime if(identity == 1): } s2.bind(address2) else: buildscript { s2.connect(address2) repositories { s2.linger = 0 jcenter() // because we love https and supersets mavenCentral() while True: mavenLocal() r = s1.poll(50) } if r != 0: msg = s1.recv() dependencies { if msg == "PING": classpath 'com.github.rholder:gradle-one-jar:1.0.4' s1.send("PONG") } else: } s2.send(msg) r = s2.poll(50) dependencies { if r != 0: compile group: 'com.github.rholder', name: 'gradle-one-jar', version: msg = s2.recv() '1.0.4' s1.send(msg) print "end of " + identity compile group: 'org.zeromq', name: 'jeromq', version: '0.4.3' compile group: 'com.google.code.gson', name: 'gson', version: '2.8.1' compile group: 'org.apache.commons', name: 'commons-io', version: '1.3.2' if __name__ == "__main__": compile group: 'commons-collections', name: 'commons-collections', version: g1 = gevent.spawn(filter_ping, initiator_address, mediator_address, 1)'3.2' g2 = gevent.spawn(filter_ping, responder_address, mediator_address, 2) compile group: 'commons-codec', name: 'commons-codec', version: '1.10' gs = [g1,g2] compile group: 'org.apache.commons', name: 'commons-compress', version: '1.10' gevent.joinall(gs) compile group: 'org.tukaani', name: 'xz', version: '1.5' compile group: 'org.apache.pdfbox', name: 'pdfbox', version: '1.8.11' compile group: 'net.sourceforge.javaflacencoder', name: 'java-flac-encoder', version: '0.3.7' compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.1' JavaFx UI: compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.1' compile group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-', version: '2.5.5' compile group: 'org.yaml', name: 'snakeyaml', version: '1.12' ------compile group: 'technology.zeroalpha.security', name: 'j-pgp-wordlist', Filename: build.gradle version: '0.1.1' ------compile group: 'org.kordamp.ikonli', name: 'ikonli-javafx', version: '1.9.0' compile group: 'org.kordamp.ikonli', name: 'ikonli-material-pack', version: apply plugin: 'java' // add java tasks '1.9.0' apply plugin: 'eclipse' // create eclipse project compile group: 'org.kordamp.ikonli', name: 'ikonli-materialdesign-pack', apply plugin: 'application' // prepare for packaging as application version: '1.9.0' apply plugin: 'gradle-one-jar' // for single self-contained jar outputs compile group: 'org.postgresql', name: 'postgresql', version: '42.1.4' // java jar configuration (manifest.mf in jar) //sourceCompatibility = '1.7' // NOTE: version lock on 1.56 due to constant API changes sourceCompatibility = '1.8' // See src/main/java/com/transmisms/core/util/crypto/RSA.java version = '0.9' compile group: 'org.bouncycastle', name: 'bcprov-jdk15on', version: '1.56' mainClassName = 'com.transmisms.ui.javafx.Main' compile group: 'org.bouncycastle', name: 'bcpkix-jdk15on', version: '1.56' applicationDefaultJvmArgs = [] // set as empty for now

47 testCompile group: 'junit', name: 'junit', version: '4.12' testCompile group: 'org.testng', name: 'testng', version: '6.9.6' testCompile group: 'org.mockito', name: 'mockito-core', version: '1.10.19' ------integrationCompile sourceSets.main.output Filename: src/main/java/com/transmisms/core/protocol/BinaryPDUDecoder.java integrationCompile sourceSets.test.output ------integrationCompile configurations.testCompile integrationCompile configurations.testRuntime package com.transmisms.core.protocol;

stagingCompile sourceSets.main.output import com.transmisms.core.protocol.CoreProtocolFacade; stagingCompile sourceSets.test.output import com.transmisms.core.protocol.CoreProtocolFacade.DataSizes; stagingCompile configurations.testCompile import com.transmisms.core.protocol.CoreProtocolFacade.DataStrSizes; stagingCompile configurations.testRuntime import com.transmisms.core.protocol.PDU; } import com.transmisms.core.protocol.PDUType; import com.transmisms.core.protocol.PDUMalformedException; test { import com.transmisms.core.protocol.InvalidCRCException; include "**/**Suite.class" import com.transmisms.smsftp.protocol.SmsftpPDUType; } import com.transmisms.core.util.codec.Base85; sourceSets { import net.sourceforge.javaflacencoder.CRC8; test { java.srcDir 'src/test/unit/java' import java.nio.ByteBuffer; } import java.util.Arrays; import java.util.ArrayList; integration { import java.util.Hashtable; java.srcDir file('src/test/integration/java') import java.util.List; resources.srcDir file('src/test/resources') import java.util.Map; } import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; staging { import java.util.function.Function; java.srcDir file('src/test/staging/java') resources.srcDir file('src/test/resources') import java.io.IOException; } import java.io.UnsupportedEncodingException; } task integration(type: Test) { group 'Verification' public class BinaryPDUDecoder { description 'Runs the integration tests.' private Function prefixToTypeFunc; useTestNG { // enable TestNG private Function typeToPrefixFunc; //parallel = "classes" parallel = "methods" private static Function basePrefixToTypeFunc; threadCount = 4 private static Function baseTypeToPrefixFunc; } testClassesDir = sourceSets.integration.output.classesDir private static final Map prefixToTypeLookup = // NOTE: use the following instead if you're using gradle 5.0 and above new Hashtable<>(); //testClassesDirs = sourceSets.integration.output.classesDirs private static final Map typeToPrefixLookup = classpath = sourceSets.integration.runtimeClasspath new Hashtable<>(); } // utility function for use for the maps above private static void biDiAddToLookup(Character c, PDUType p) { task staging(type: Test) { PDUType evalValue = BinaryPDUDecoder.prefixToTypeLookup.put(c, p); group 'Verification' assert null == evalValue; description 'Runs the staging tests.' Character evalValue2 = BinaryPDUDecoder.typeToPrefixLookup.put(p, c); useTestNG { // enable TestNG assert null == evalValue2; //parallel = "classes" } parallel = "classes" threadCount = 4 private static BinaryPDUDecoder thisInstance; } static { testClassesDir = sourceSets.staging.output.classesDir // initialize instance // NOTE: use the following instead if you're using gradle 5.0 and above BinaryPDUDecoder.thisInstance = new BinaryPDUDecoder(null, null); //testClassesDirs = sourceSets.staging.output.classesDirs BinaryPDUDecoder.basePrefixToTypeFunc = classpath = sourceSets.staging.runtimeClasspath (c) -> BinaryPDUDecoder.prefixToTypeLookup.get(c); } BinaryPDUDecoder.baseTypeToPrefixFunc = (p) -> BinaryPDUDecoder.typeToPrefixLookup.get(p); tasks.withType(Test) { // populate lookup maps // experiment with different values to see what works best BinaryPDUDecoder.biDiAddToLookup('r', maxParallelForks = 8 CorePDUType.CORE_RETRANSMISSION); // can also compute dynamically BinaryPDUDecoder.biDiAddToLookup('f', CorePDUType.CORE_FIN); // maxParallelForks = Runtime.runtime.availableProcessors() / 2 BinaryPDUDecoder.biDiAddToLookup('F', CorePDUType.CORE_FINACK); } } javadoc { title = 'Transmisms API' protected BinaryPDUDecoder(Function prefixToTypeFunc, } Function typeToPrefixFunc) { this.prefixToTypeFunc = prefixToTypeFunc; this.typeToPrefixFunc = typeToPrefixFunc; uploadArchives { } repositories { flatDir { public static BinaryPDUDecoder getInstance() { name 'local' return BinaryPDUDecoder.thisInstance; dirs '../repos' } } } public final PDUType prefixToPduType(Character prefix) { } PDUType retType = BinaryPDUDecoder.basePrefixToTypeFunc.apply(prefix); // this was added to demonstrate adding -X arguments to `javac` if(retType == null && this.prefixToTypeFunc != null) { /*compileTestJava { return this.prefixToTypeFunc.apply(prefix); options.compilerArgs << "-Xlint:unchecked" // for unsafe thing javac is } complaining return retType; }*/ }

48 bb.put(segmentNumBytes).flip(); // flip() is required for reading public final Character pduTypeToPrefix(PDUType pduType) { return bb.getInt(); // then return the value Character retPrefix = } BinaryPDUDecoder.baseTypeToPrefixFunc.apply(pduType); if(retPrefix == null && this.typeToPrefixFunc != null) { protected List decodeBody( return this.typeToPrefixFunc.apply(pduType); PDUType pduType, String encoded) } throws PDUMalformedException { return retPrefix; return null; } } public static int getHexSegmentId(String body, int maxSegmentCount, private final List defaultDecodeBody( String encodedStr, String pduTypeName) PDUType pduType, String encoded) throws PDUMalformedException { throws PDUMalformedException { return getHexSegmentId(body, maxSegmentCount, encodedStr, pduTypeName, String body = encoded.substring(1, encoded.length()-2); false); List decodedData = new ArrayList<>(); } if(pduType instanceof CorePDUType) { public static int getHexSegmentId(String body, int maxSegmentCount, CorePDUType cPduType = (CorePDUType)pduType; String encodedStr, String pduTypeName, boolean allowZeroId) switch(cPduType) { // fill up data depending on the pduType throws PDUMalformedException { case CORE_FIN: { String segmentIdStr = body.substring(0, // 1st form: f/ DataStrSizes.HEXSEGMENTID.getSize()); // check if size is less than SESSIONID if(body.length() < DataStrSizes.SESSIONID.getSize()) { Integer segmentId = -1; // preeempt for error, just in case throw new PDUMalformedException( boolean invalidNumberFormat = false; "CORE_FIN too short to fit Session ID", try { encoded); segmentId = Integer.valueOf(segmentIdStr, 16); } } String sessionIdStr = body.substring(0, catch(NumberFormatException e) { DataStrSizes.SESSIONID.getSize()); invalidNumberFormat = true; // decode sessionId from here } UUID sessionId = base85ToUUID(sessionIdStr, encoded, if(segmentId < 0 || (!allowZeroId && segmentId == 0) "CORE_FIN"); || segmentId > maxSegmentCount decodedData.add(sessionId); // and add to decoded data || invalidNumberFormat) { throw new PDUMalformedException( if(body.length() > DataStrSizes.SESSIONID.getSize()) { "Invalid " + // 2nd form a: f/// ((pduTypeName == null) ? "" : pduTypeName+" ") + // 2nd form b: "Segment ID: " + segmentIdStr, encodedStr); f/// } return segmentId; // check if size is less than SESSIONID+STATUSCODE } if(body.length() < DataStrSizes.SESSIONID.getSize() + DataStrSizes.STATUSCODE.getSize()) { public static UUID base85ToUUID(String b85String, String encodedStr, throw new PDUMalformedException( String pduTypeName) "CORE_FIN too short to fit Status Code", throws PDUMalformedException { encoded); // extract byte[] format from base85 } byte[] uuidBytes = null; // get status code try { String statusCode = body.substring( uuidBytes = Base85.decode(b85String); DataStrSizes.SESSIONID.getSize(), } DataStrSizes.SESSIONID.getSize()+ catch(IOException e) { DataStrSizes.STATUSCODE.getSize()); throw new PDUMalformedException( if(CoreProtocolFacade.getStatusMessage( pduTypeName + "'s Session ID is corrupt", encodedStr); statusCode) == null) { } throw new PDUMalformedException( // verify if sessionId's length is correct "Invalid status code from " + if(uuidBytes.length != DataSizes.SESSIONID.getSize()) { pduType.toString() + ": " + statusCode, throw new PDUMalformedException( encoded); pduTypeName + "'s Session ID's byte length is incorrect", } null); decodedData.add(statusCode); } // check if there is an optional message String message = null; // split the byte[] into msb and lsb if(body.length() > DataStrSizes.SESSIONID.getSize() + byte[] msb = Arrays.copyOfRange(uuidBytes, 0, 8); DataStrSizes.STATUSCODE.getSize()) { byte[] lsb = Arrays.copyOfRange(uuidBytes, 8, 16); message = body.substring( // convert the msb and lsb byte[] into long and return afterwards DataStrSizes.SESSIONID.getSize()+ ByteBuffer msbBuffer = ByteBuffer.allocate(Long.BYTES).put(msb); DataStrSizes.STATUSCODE.getSize(), ByteBuffer lsbBuffer = ByteBuffer.allocate(Long.BYTES).put(lsb); body.length()); msbBuffer.flip(); // required for reading decodedData.add(message); lsbBuffer.flip(); // required for reading } return new UUID(msbBuffer.getLong(), lsbBuffer.getLong()); } } break; } public static int base85ToInteger(String b85String, String encodedStr, String pduTypeName) throws PDUMalformedException { case CORE_FINACK: { // extract bytes first from base85 // check if size is less than SESSIONID byte[] segmentNumBytes = null; if(body.length() < DataStrSizes.SESSIONID.getSize()) { try { throw new PDUMalformedException( segmentNumBytes = Base85.decode(b85String); "CORE_FINACK too short to fit Session ID", } encoded); catch(IOException e) { } throw new PDUMalformedException( // try to parseSessionId pduTypeName + "'s Segment Id/Count is corrupt", String sessionIdStr = body.substring(0, encodedStr); DataStrSizes.SESSIONID.getSize()); } ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES); UUID sessionId = base85ToUUID(sessionIdStr, encoded, // pad with zeroes if there's still space for Integer "CORE_FINACK"); for(int i = 0; i < Integer.BYTES - segmentNumBytes.length; i++) { decodedData.add(sessionId); // and add to decoded data bb.put((byte)0); break; } } // then add the bytes

49 crc.updateCRC8(crcBody.getBytes("UTF-8"), 0, case CORE_RETRANSMISSION: { crcBody.getBytes("UTF-8").length); AtomicInteger pointer = new AtomicInteger(0); byte computedCRC = crc.checksum(); String computedCrcStr = String.format("%02x", computedCRC); while(pointer.get() < body.length()) { if(!givenCRCStr.equalsIgnoreCase(computedCrcStr)) { char retPrefix = body.charAt( throw new InvalidCRCException("Invalid CRC! Got: " + pointer.getAndIncrement()); givenCRCStr + "; Expected: " + computedCrcStr, PDUType retPduType = this.prefixToPduType(retPrefix); encoded); } if(retPduType != null) { // if valid } Object segmentId = null; catch(UnsupportedEncodingException e) { try { // this should never happen; crash early if(retPduType == SmsftpPDUType.SMSFTP_DATA) { throw new IllegalStateException(e.getMessage(), e); String b85SegmentIdStr = body.substring( } pointer.getAndAdd( DataStrSizes.SEGMENTID.getSize()), String body = encoded.substring(1, encoded.length()-2); pointer.get()); // just to make sure that pduType is "initialized" Integer b85SegmentId = base85ToInteger( PDUType pduType = null; b85SegmentIdStr, encoded, { // get PDU type pduType.toString()); char firstChar = encoded.charAt(0); segmentId = b85SegmentId; pduType = this.prefixToPduType(firstChar); } if(pduType == null) { else { throw new PDUMalformedException( String hexSegmentIdStr = body.substring( "Invalid Binary PDU Prefix: " + firstChar, encoded); pointer.getAndAdd( } } DataStrSizes.HEXSEGMENTID.getSize()), pointer.get()); try { Integer hexSegmentId = getHexSegmentId( decodedData.addAll( hexSegmentIdStr, 256, encoded, this.defaultDecodeBody(pduType, encoded)); pduType.toString(), true); } segmentId = hexSegmentId; catch(PDUMalformedException e) { } List l = this.decodeBody(pduType, encoded); } if(l != null) { // do nothing on exceptions decodedData.addAll(l); catch(PDUMalformedException e) {} } catch(StringIndexOutOfBoundsException e) {} else { // add if no errors encountered throw e; if(segmentId != null) { } decodedData.add(retPduType); } decodedData.add(segmentId); } return new PDU(pduType, decodedData.toArray()); } } else { // just increment and go } pointer.getAndAdd( DataStrSizes.HEXSEGMENTID.getSize()); // assume all other PDUs will be hex } } ------Filename: src/main/java/com/transmisms/core/protocol/Connection.java break; ------} default: { package com.transmisms.core.protocol; // code should NEVER reach here throw new AssertionError("Uncaught PDU Type: " + pduType);import java.util.concurrent.atomic.AtomicInteger; } import java.util.concurrent.atomic.AtomicReference; } } else { throw new PDUMalformedException("Uncaught PDU Type: " + pduType,public class Connection { encoded); public enum Role { INITIATOR, RESPONDER }; } return decodedData; public enum ConnectionState { } CLOSED, ESTABLISHED, public final PDU decodeBinary(String encoded) throws PDUMalformedException, InvalidCRCException { LISTENING, List decodedData = new ArrayList<>(); REQUEST_RECEIVED,

// some sanity checks before parsing REQUEST_SENT, if(encoded == null) { REQUEST_SENT_2, // ignore for now throw new NullPointerException( REQUEST_SENT_3, // ignore for now "Binary encoded string cannot be null"); } CLOSE_WAIT, else if(encoded.equals("")) { FIN_WAIT, throw new PDUMalformedException( CLOSING_1, "Binary encoded string cannot be empty", encoded); CLOSING_2, } TIME_WAIT else if(encoded.length() < DataStrSizes.CRC.getSize()+1) { }; throw new PDUMalformedException("Binary encoded string too short", encoded); public final Role role; } public final String localMin; public String peerMin; try { // determine if checksum matched PDU body public int maxPduLifetime = 60000; // it doesn't matter that much if the last 2 chars of the string is // a valid hex string; they are just compared as strings private final AtomicReference connectionStateRef = // anyways new AtomicReference<>(ConnectionState.CLOSED); String givenCRCStr = encoded.substring(encoded.length()- private final AtomicInteger errorCount = new AtomicInteger(0); DataStrSizes.CRC.getSize()); CRC8 crc = new CRC8(); // buffer variables for use by core protocol String crcBody = encoded.substring(0, encoded.length()-2); protected PDUType lastPduBeforeEst = null;

50 protected String finStatusComboReceived; CoreLookupTable.createResendCoreRepTask(t, u)); protected String encodedRepPdu; assert null == evalValue; protected String encodedHeadPdu; evalValue = lookupTable.put( protected String encodedFinPdu; new PdlEntry( protected String encodedFinackPdu; ConnectionState.REQUEST_RECEIVED, CorePDUType.CORE_HEAD), (t, u) -> private Connection(String localMin, String peerMin, Role role) { CoreLookupTable.createEstConnAsResponderTask(t, u)); this.localMin = localMin; assert null == evalValue; this.peerMin = peerMin; evalValue = lookupTable.put( this.role = role; new PdlEntry( } ConnectionState.REQUEST_SENT, CorePDUType.CORE_CONNECTION_RESPONSE), public static Connection createResponderConnection(String localMin) { (t, u) -> return new Connection(localMin, null, Role.RESPONDER); CoreLookupTable.createEstConnAsInitiatorTask(t, u)); } assert null == evalValue; public static Connection createInitiatorConnection(String localMin, evalValue = lookupTable.put( String peerMin) { new PdlEntry( return new Connection(localMin, peerMin, Role.INITIATOR); ConnectionState.ESTABLISHED, } CorePDUType.CORE_CONNECTION_RESPONSE), (t, u) ->CoreLookupTable.createResendCoreHeadIfValidTask( t, u)); assert null == evalValue; public int getErrorCount() { evalValue = lookupTable.put( return this.errorCount.get(); new PdlEntry( } ConnectionState.ESTABLISHED, CorePDUType.CORE_HEAD), (t, u) -> CoreLookupTable.createDoNothingTask(t, u)); public int incrementErrorCount() { assert null == evalValue; return this.errorCount.incrementAndGet(); evalValue = lookupTable.put( } new PdlEntry( ConnectionState.ESTABLISHED, CorePDUType.CORE_FIN), public ConnectionState getConnectionState() { (t, u) -> CoreLookupTable.createFinReceivedTask(t, u)); return this.connectionStateRef.get(); assert null == evalValue; } evalValue = lookupTable.put( new PdlEntry( public void setConnectionState(ConnectionState connectionState) { ConnectionState.FIN_WAIT, CorePDUType.CORE_FIN), if(connectionState == null) { (t, u) -> CoreLookupTable.createSimulFinReceivedTask(t, u)); throw new NullPointerException("Connection state cannot be null"); assert null == evalValue; } evalValue = lookupTable.put( this.connectionStateRef.set(connectionState); new PdlEntry( } ConnectionState.FIN_WAIT, CorePDUType.CORE_FINACK), } (t, u) -> CoreLookupTable.createFinwaitDisconnectTask(t, u)); assert null == evalValue; evalValue = lookupTable.put( new PdlEntry( ------ConnectionState.FIN_WAIT, CorePDUType.CORE_LAST_ACK), Filename: src/main/java/com/transmisms/core/protocol/CoreLookupTable.java (t, u) -> CoreLookupTable.createSimulLastackReceivedTask( ------t, u)); assert null == evalValue; package com.transmisms.core.protocol; evalValue = lookupTable.put( // from extended Termination FSM draft new PdlEntry( import com.transmisms.core.protocol.Connection.ConnectionState; ConnectionState.FIN_WAIT, import com.transmisms.core.protocol.Presenter.MessageType; CorePDUType.CORE_CONNECTION_RESPONSE), import com.transmisms.core.protocol.Presenter.PromptType; (t, u) -> import com.transmisms.core.protocol.Presenter.StatusType; CoreLookupTable.createResendCoreHeadIfValidTask(t, u)); assert null == evalValue; import java.util.Hashtable; evalValue = lookupTable.put( import java.util.List; new PdlEntry( import java.util.Map; ConnectionState.CLOSE_WAIT, CorePDUType.CORE_FIN), import java.util.UUID; (t, u) -> CoreLookupTable.createResendFinackTask(t, u)); import java.util.concurrent.Future; assert null == evalValue; import java.util.function.BiFunction; evalValue = lookupTable.put( new PdlEntry( ConnectionState.CLOSE_WAIT, CorePDUType.CORE_LAST_ACK), (t, u) -> CoreLookupTable.createDisconnectTask(t, u)); public class CoreLookupTable { assert null == evalValue; private static final Map> lookupTable = new PdlEntry( new Hashtable<>(); ConnectionState.CLOSING_1, CorePDUType.CORE_FIN), (t, u) -> CoreLookupTable.createResendFinTask(t, u)); static { // initialize lookupTable assert null == evalValue; BiFunction evalValue = evalValue = lookupTable.put( null; // only used for asserts new PdlEntry( evalValue = lookupTable.put( ConnectionState.CLOSING_1, CorePDUType.CORE_LAST_ACK), new PdlEntry( (t, u) -> ConnectionState.LISTENING, CoreLookupTable.createClosing1DisconnectTask(t, u)); CorePDUType.CORE_CONNECTION_REQUEST), assert null == evalValue; (t, u) -> CoreLookupTable.createRequestReceivedTask(t, u)); evalValue = lookupTable.put( assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( ConnectionState.CLOSING_2, CorePDUType.CORE_FIN), new PdlEntry( (t, u) -> ConnectionState.REQUEST_RECEIVED, CoreLookupTable.createClosing2DisconnectTask(t, u)); CorePDUType.CORE_CONNECTION_REQUEST), assert null == evalValue; (t, u) -> evalValue = lookupTable.put( CoreLookupTable.createResendCoreRepTask(t, u)); new PdlEntry( assert null == evalValue; ConnectionState.CLOSING_2, CorePDUType.CORE_LAST_ACK), evalValue = lookupTable.put( (t, u) -> CoreLookupTable.createResendFinTask(t, u)); new PdlEntry( assert null == evalValue; ConnectionState.REQUEST_RECEIVED, evalValue = lookupTable.put( CorePDUType.CORE_FIN), // from extended Handshake FSM draft new PdlEntry( (t, u) -> ConnectionState.TIME_WAIT, CorePDUType.CORE_FIN),

51 (t, u) -> CoreLookupTable.createResendLastackTask(t, u)); ConnectionState.CLOSING_2, assert null == evalValue; CorePDUType.CORE_CONNECTION_RESPONSE), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( ConnectionState.CLOSING_2, CorePDUType.CORE_HEAD), ConnectionState.TIME_WAIT, CorePDUType.CORE_FINACK), new PdlEntry( (t, u) -> CoreLookupTable.createResendLastackTask(t, u)); ConnectionState.CLOSING_2, CorePDUType.CORE_FINACK), assert null == evalValue; }; for(PdlEntry e : errorEntries) { //// cases for retransmission evalValue = lookupTable.put(e, ConnectionState[] retransStates = { ConnectionState.ESTABLISHED, (t, u) -> CoreLookupTable.createPDUMismatchTask(t, u)); ConnectionState.CLOSE_WAIT, assert null == evalValue; ConnectionState.FIN_WAIT, } ConnectionState.CLOSING_1, } ConnectionState.CLOSING_2, ConnectionState.TIME_WAIT }; for(ConnectionState cs : retransStates) { evalValue = lookupTable.put(new PdlEntry(cs, //// "action" functions CorePDUType.CORE_RETRANSMISSION), public static Runnable createDoNothingTask( (t, u) -> CoreLookupTable.createRetransmissionTask(t, u)); CoreProtocolFacade tObject, PDU uObject) { assert null == evalValue; return () -> {}; } }

//// error cases for lookupTable public static Runnable createHumanRejectTask( PdlEntry[] errorEntries = { CoreProtocolFacade tObject, PDU uObject) { new PdlEntry( return () -> { ConnectionState.LISTENING, // simply "close" the connection CorePDUType.CORE_CONNECTION_RESPONSE), CoreProtocolFacade.setStateAndNotify(tObject, new PdlEntry( ConnectionState.CLOSED); ConnectionState.LISTENING, CorePDUType.CORE_HEAD), }; new PdlEntry( } ConnectionState.LISTENING, CorePDUType.CORE_FIN), new PdlEntry( public static Runnable createPeerTimeoutTask( ConnectionState.LISTENING, CorePDUType.CORE_FINACK), CoreProtocolFacade tObject, PDU uObject) { new PdlEntry( return () -> { ConnectionState.LISTENING, CorePDUType.CORE_LAST_ACK), // pause all operations (sms consumption only) new PdlEntry( tObject.waitingForPrompt.set(true); ConnectionState.REQUEST_RECEIVED, // notify observers CorePDUType.CORE_CONNECTION_RESPONSE), Object[] timeoutPrompt = {MessageType.PROMPT, PromptType.TIMEOUT}; /* CoreProtocolFacade.setChangedAndNotifyObservers(tObject, //commented out due to changes from extended Handshake FSM timeoutPrompt); new PdlEntry( }; ConnectionState.REQUEST_RECEIVED, CorePDUType.CORE_FIN), } */ new PdlEntry( // NOTE: being used by resend*Tasks ConnectionState.REQUEST_RECEIVED, CorePDUType.CORE_FINACK), public static Runnable createPDUMismatchTask( new PdlEntry( CoreProtocolFacade tObject, PDU uObject) { ConnectionState.REQUEST_RECEIVED, CorePDUType.CORE_LAST_ACK), return () -> { new PdlEntry( Connection connection = ConnectionState.REQUEST_SENT, CorePDUType.CORE_FIN), tObject.coreSession.connection; new PdlEntry( connection.incrementErrorCount(); ConnectionState.REQUEST_SENT, CorePDUType.CORE_FINACK), if(connection.getErrorCount() > 10) { new PdlEntry( CoreProtocolFacade.logAndNotify(tObject, MessageType.WARN, ConnectionState.REQUEST_SENT, CorePDUType.CORE_LAST_ACK), "Connection got too many errors"); new PdlEntry( } ConnectionState.ESTABLISHED, CorePDUType.CORE_FINACK), }; new PdlEntry( } ConnectionState.ESTABLISHED, CorePDUType.CORE_LAST_ACK), new PdlEntry( public static Runnable createRequestReceivedTask( ConnectionState.FIN_WAIT, CorePDUType.CORE_CONNECTION_REQUEST), CoreProtocolFacade tObject, PDU uObject) { /* return () -> { //commented out due to changes from extended Termination FSM CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, new PdlEntry( "Received CONNECTION_REQUEST"); ConnectionState.FIN_WAIT, String[] payloadArr = tObject.processReqAndGenRepPayload( CorePDUType.CORE_CONNECTION_RESPONSE), uObject.getData()); */ new PdlEntry( String repPayload; ConnectionState.FIN_WAIT, CorePDUType.CORE_HEAD), if(payloadArr.length == 1) { new PdlEntry( repPayload = generateStatusPayload(payloadArr[0]); ConnectionState.CLOSE_WAIT, } CorePDUType.CORE_CONNECTION_REQUEST), else if(payloadArr.length == 2) { new PdlEntry( repPayload = payloadArr[0] + "/" + payloadArr[1]; ConnectionState.CLOSE_WAIT, } CorePDUType.CORE_CONNECTION_RESPONSE), else { // invalid new PdlEntry( return; // do nothing ConnectionState.CLOSE_WAIT, CorePDUType.CORE_HEAD), } new PdlEntry( // use the received Session ID from sender ConnectionState.CLOSE_WAIT, CorePDUType.CORE_FINACK), if(uObject == null || uObject.getData() == null || new PdlEntry( uObject.getData().length < 2 || ConnectionState.CLOSING_1, !(uObject.getData()[1] instanceof UUID)) { // invalid CorePDUType.CORE_CONNECTION_REQUEST), return; // do nothing new PdlEntry( } ConnectionState.CLOSING_1, tObject.coreSession.sessionId = (UUID)(uObject.getData()[1]); CorePDUType.CORE_CONNECTION_RESPONSE), // respond with REP new PdlEntry( PDUEncoder encoder = new PDUEncoder(false); ConnectionState.CLOSING_1, CorePDUType.CORE_HEAD), encoder.appendVersion(CoreProtocolFacade.VERSION) new PdlEntry( .appendSessionId(tObject.coreSession ConnectionState.CLOSING_1, CorePDUType.CORE_FINACK), .sessionId).appendPayload( new PdlEntry( "rep/" + tObject.getSubprotocolName() + "/" + repPayload) ConnectionState.CLOSING_2, .appendAdditionalMessage( CorePDUType.CORE_CONNECTION_REQUEST), CoreProtocolFacade.DEFAULT_ADDITIONAL_MESSAGE) new PdlEntry( .finalizePDU();

52 tObject.sendMessageRepeatedly(encoder.getEncoded()); }; tObject.coreSession.connection.encodedRepPdu = } encoder.getEncoded(); CoreProtocolFacade.setStateAndNotify(tObject, public static Runnable createResendCoreHeadIfValidTask( ConnectionState.REQUEST_RECEIVED); CoreProtocolFacade tObject, PDU uObject) { }; return () -> { } // check if ESTABLISHED state is acquired via a // REP PDU from a REQUEST_SENT state public static Runnable createResendCoreRepTask( if(tObject.coreSession.connection.lastPduBeforeEst == CoreProtocolFacade tObject, PDU uObject) { CorePDUType.CORE_CONNECTION_RESPONSE && return () -> { tObject.coreSession.connection.encodedHeadPdu != null) { CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, "Resending CONNECTION_RESPONSE"); "Resending HEAD"); // just resend stored REP tObject.smsService.sendMessage( if(tObject.coreSession.connection.encodedRepPdu != null) { tObject.coreSession.connection.encodedHeadPdu); tObject.smsService.sendMessage( } tObject.coreSession.connection.encodedRepPdu); else { // increment error count and notify listeners for } // subsequent actions, if needed // just maintain state CoreLookupTable.createPDUMismatchTask( }; tObject, uObject).run(); } } // just maintain state public static Runnable createEstConnAsInitiatorTask( }; CoreProtocolFacade tObject, PDU uObject) { } return () -> { Object[] data = uObject.getData(); public static Runnable createRetransmissionTask( // if peer accepted the connection CoreProtocolFacade tObject, PDU uObject) { if(data != null && data.length >= 4 && return () -> { data[3] instanceof String && Object[] data = uObject.getData(); CoreProtocolFacade.USER_ACCEPT_STATUS_CODE.equals( if(data == null || data.length < 2) { (String)data[3])) { return; CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, } "Responder has accepted the connection request"); // loop through the PDUType, Integer pairs } for(int i = 0; i < data.length/2; i++) { else if(data != null && data.length >= 4 && if(!(data[0] instanceof PDUType data[3] instanceof String) { // non-accept status code && data[1] instanceof Integer)) { String bufferPlus = (String)data[3] + " " + return; // exit asap on errors ((data.length >= 5 && data[4] instanceof String) ? } (String)data[4] : ""); // extract data CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, PDUType t = (PDUType)data[0]; "Peer has rejected the connection request with " + int n = (Integer)data[1]; "status: " + bufferPlus); } // determine if n is for a multipart else { // something else List l = tObject.getRetransList(t); return; // exit asap if(l == null) { // PDUType not yet registered } CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, // respond with HEAD "Requested PDU not yet available: " + PDUEncoder encoder = new PDUEncoder(false); t + " part " + n); encoder.appendVersion(CoreProtocolFacade.VERSION) continue; // try next pair .appendSessionId(tObject.coreSession } .sessionId).appendPayload( if(n == 0 && l.size() == 1) { // single-part PDUs "head/" + tObject.getSubprotocolName() + "/" + // log received 'n' first tObject.generateHeadPayload()) CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, .appendAdditionalMessage( "Resending " + t + "'s only part "); CoreProtocolFacade.DEFAULT_ADDITIONAL_MESSAGE) // then leave it as it is .finalizePDU(); } tObject.sendMessageRepeatedly(encoder.getEncoded()); // check for errors first tObject.coreSession.connection.encodedHeadPdu = else if((n != 0 && n > l.size()+1) || // exceeded l.size() encoder.getEncoded(); (n == 0 && l.size() > 1)) { // '0' on multipart PDUs tObject.coreSession.connection.lastPduBeforeEst = CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, CorePDUType.CORE_CONNECTION_RESPONSE; "Invalid requested PDU: " + t + " part " + n); CoreProtocolFacade.setStateAndNotify(tObject, return; // exit asap on errors ConnectionState.ESTABLISHED); } // finally, execute the onEstablished() hook else { // multi-part PDUs tObject.onEstablished(); // log received 'n' first // start the retransmission thread CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, tObject.startRetransLoop(); "Resending " + t + " part " + n); }; n--; // then decrement for real value } } // notify observers about the resent PDU public static Runnable createEstConnAsResponderTask( CoreProtocolFacade.setChangedAndNotifyObservers(tObject, CoreProtocolFacade tObject, PDU uObject) { MessageType.STATUS, StatusType.NOTICE, return () -> { tObject.coreSession.connection.getConnectionState(), Object[] data = uObject.getData(); t, n); // if we accepted the connection // then retransmit the PDU if(data != null && data.length >= 4 && data[3] instanceof String) { tObject.smsService.sendMessage(l.get(n)); tObject.processHeadPayload(data); } CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, }; "Initiator has completed the handshake"); } // stop repeated sending of REP ASAP tObject.sendMessageRepeatedly(null); public static Runnable createFinReceivedTask( } CoreProtocolFacade tObject, PDU uObject) { else { // something else return () -> { return; // exit asap Object[] data = uObject.getData(); } String statusCode = (String)data[1]; CoreProtocolFacade.setStateAndNotify(tObject, String statusMessage = (data.length == 3 && data[2] ConnectionState.ESTABLISHED); instanceof String) ? (String)data[2] : ""; // finally, execute the onEstablished() hook CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, tObject.onEstablished(); "Peer has requested disconnection. Reason: " + // start the retransmission thread statusCode + " " + statusMessage); tObject.startRetransLoop();

53 // respond with FINACK CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, PDUEncoder encoder = new PDUEncoder(true); "Resending FIN"); encoder.appendPrefix(CorePDUType.CORE_FINACK, tObject.smsService.sendMessage( BinaryPDUDecoder.getInstance()) tObject.coreSession.connection.encodedFinPdu); .appendSessionId(tObject.coreSession.sessionId) } .finalizePDU(); // just maintain state tObject.sendMessageRepeatedly(encoder.getEncoded()); }; // store the message somewhere } tObject.coreSession.connection.encodedFinackPdu = encoder.getEncoded(); public static Runnable createResendLastackTask( // notify observers of the received statusCode and message CoreProtocolFacade tObject, PDU uObject) { CoreProtocolFacade.setChangedAndNotifyObservers(tObject, return () -> { MessageType.STATUS, StatusType.NOTICE, CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, ConnectionState.CLOSE_WAIT, statusCode, statusMessage); "Resending LAST_ACK"); // then set to CLOSE_WAIT state afterwards // respond with LAST_ACK CoreProtocolFacade.setStateAndNotify(tObject, String lastackStr = generateEncodedLastack(tObject); ConnectionState.CLOSE_WAIT); tObject.smsService.sendMessage(lastackStr); }; // just maintain state } }; } public static Runnable createSimulFinReceivedTask( CoreProtocolFacade tObject, PDU uObject) { public static Runnable createClosing1DisconnectTask( return () -> { CoreProtocolFacade tObject, PDU uObject) { // respond with LAST_ACK return () -> { String lastackStr = generateEncodedLastack(tObject); // set to TIME_WAIT afterwards tObject.smsService.sendMessage(lastackStr); CoreProtocolFacade.setStateAndNotify(tObject, // set to CLOSING_1 afterwards ConnectionState.TIME_WAIT); CoreProtocolFacade.setStateAndNotify(tObject, }; ConnectionState.CLOSING_1); } }; } public static Runnable createClosing2DisconnectTask( CoreProtocolFacade tObject, PDU uObject) { public static Runnable createFinwaitDisconnectTask( return () -> { CoreProtocolFacade tObject, PDU uObject) { // respond with LAST_ACK return () -> { String lastackStr = generateEncodedLastack(tObject); CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, tObject.smsService.sendMessage(lastackStr); "Peer acknowledged disconnection request"); // set to TIME_WAIT afterwards // respond with LAST_ACK CoreProtocolFacade.setStateAndNotify(tObject, String lastackStr = generateEncodedLastack(tObject); ConnectionState.TIME_WAIT); tObject.smsService.sendMessage(lastackStr); }; // set to TIME_WAIT afterwards } CoreProtocolFacade.setStateAndNotify(tObject, ConnectionState.TIME_WAIT); public static Runnable createDisconnectTask( CoreProtocolFacade tObject, PDU uObject) { // transition from a proper TIME_WAIT to CLOSED transition on a return () -> { // background thread // just disconnect (set state to CLOSED) Future delayedFuture = tObject.pool.submit(() -> { CoreProtocolFacade.setStateAndNotify(tObject, try { // wait until the maxPduLifetime*3/2 ConnectionState.CLOSED); // trip timeoutLoopFuture CoreProtocolFacade.logAndNotify(tObject, MessageType.INFO, tObject.timeoutLoopFuture.cancel(true); "Disconnected"); // then sleep for a while before triggering disconnection // kill all unneeded threads Thread.sleep( tObject.shutdownNow(); tObject.coreSession.connection.maxPduLifetime); }; } } catch(InterruptedException e) { // do nothing; just proceed with shutdown procedure public static Runnable createTimeoutDisconnectTask( } CoreProtocolFacade tObject, PDU uObject) { CoreLookupTable.createDisconnectTask( return () -> { tObject, uObject).run(); // send a LAST_ACK message with timeout as reason }); String statusCombo = }; CoreProtocolFacade.REQUEST_TIMEOUT_STATUS_CODE + "/" + } CoreProtocolFacade.getStatusMessage( CoreProtocolFacade.REQUEST_TIMEOUT_STATUS_CODE); public static Runnable createSimulLastackReceivedTask( tObject.smsService.sendMessage(generateEncodedLastack(tObject, CoreProtocolFacade tObject, PDU uObject) { statusCombo)); return () -> { // finaly, disconnect (set state to CLOSED) // set to CLOSING_2 afterwards CoreProtocolFacade.setStateAndNotify(tObject, CoreProtocolFacade.setStateAndNotify(tObject, ConnectionState.CLOSED); ConnectionState.CLOSING_2); CoreProtocolFacade.logAndNotify(tObject, MessageType.INFO, }; "Disconnected"); } // and kill all unneeded threads tObject.shutdownNow(); public static Runnable createResendFinackTask( }; CoreProtocolFacade tObject, PDU uObject) { } return () -> { // just resend stored FINACK if(tObject.coreSession.connection.encodedFinackPdu != null) { CoreProtocolFacade.logAndNotify(tObject, MessageType.DEBUG, //// utility functions "Resending FINACK"); private static String generateEncodedLastack(CoreProtocolFacade tObject) { tObject.smsService.sendMessage( return generateEncodedLastack(tObject, tObject.coreSession.connection.encodedFinackPdu); tObject.coreSession.connection.finStatusComboReceived); } } // just maintain state }; private static String generateEncodedLastack(CoreProtocolFacade tObject, } String statusCombo) { PDUEncoder encoder = new PDUEncoder(false); public static Runnable createResendFinTask( encoder.appendVersion(CoreProtocolFacade.VERSION) CoreProtocolFacade tObject, PDU uObject) { .appendSessionId(tObject.coreSession.sessionId) return () -> { .appendPayload("last/" + tObject.getSubprotocolName() + "/" + // just resend stored FIN statusCombo) if(tObject.coreSession.connection.encodedFinPdu != null) { .appendAdditionalMessage(

54 CoreProtocolFacade.DEFAULT_ADDITIONAL_MESSAGE) ------.finalizePDU(); return encoder.getEncoded(); package com.transmisms.core.protocol; } import com.transmisms.core.protocol.Connection.ConnectionState; private static String generateStatusPayload(String statusCode) { import com.transmisms.core.protocol.Presenter.MessageType; return statusCode + "/" + import com.transmisms.core.protocol.Presenter.PromptType; CoreProtocolFacade.getStatusMessage(statusCode); import com.transmisms.core.util.commons.FIFOEntry; } import java.util.Arrays; import java.util.Hashtable; // convenience get() function import java.util.List; public static BiFunction import java.util.Map; get(PdlEntry e) { import java.util.Observable; return CoreLookupTable.lookupTable.get(e); import java.util.Set; } import java.util.UUID; } import java.util.concurrent.Callable; import java.util.concurrent.ConcurrentSkipListMap; import java.util.concurrent.CountDownLatch; import java.util.concurrent.Future; import java.util.concurrent.Phaser; ------import java.util.concurrent.PriorityBlockingQueue; Filename: src/main/java/com/transmisms/core/protocol/CorePDUType.java import java.util.concurrent.TimeUnit; ------import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; package com.transmisms.core.protocol; import java.util.function.BiFunction; import com.transmisms.core.protocol.PDUType.PDUSubType; import com.transmisms.core.protocol.PDUType.PduSegmentIdFormat; import java.util.concurrent.Executors; import java.util.concurrent.ExecutorService;

import java.util.concurrent.ExecutionException; public enum CorePDUType implements PDUType { import java.util.concurrent.TimeoutException; HUMAN_REJECT (PDUSubType.HUMAN), CORE_CONNECTION_REQUEST (PDUSubType.TEXT_BASED, 1), CORE_CONNECTION_RESPONSE (PDUSubType.TEXT_BASED, 1), CORE_HEAD (PDUSubType.TEXT_BASED, 1), CORE_FIN (PDUSubType.BINARY, 0), public abstract class CoreProtocolFacade extends Observable { CORE_FINACK (PDUSubType.BINARY, 0), private final String SUBPROTOCOL_NAME = null; CORE_LAST_ACK (PDUSubType.TEXT_BASED, 1), CORE_RETRANSMISSION (PDUSubType.BINARY); public enum DataSizes { CRC (1), SESSIONID (16), HEXSEGMENTID (1), public final PDUSubType pduSubType; SEGMENTID (3), SEGMENTCOUNT (3), public final PduSegmentIdFormat segIdFormat; SHA1_SUM (20), AES128_KEY (16), public final int sessionIdPosition; RSA2048_PUBKEY (294), RSA2048ENC_AES128_KEY (256), RSA2048_SHA512_SIG (256), FILESIZE (3); // default access control since enums are restricted CorePDUType(PDUSubType pduSubType) { private final int size; this(pduSubType, -1); } private DataSizes(int size) { this.size = size; CorePDUType(PDUSubType pduSubType, int sessionIdPosition) { } this.pduSubType = pduSubType; this.segIdFormat = PduSegmentIdFormat.NONE; public int getSize() { return this.size; } this.sessionIdPosition = sessionIdPosition; } } public enum DataStrSizes { PREFIX (1), CRC (2), @Override SESSIONID (20), public CorePDUType valueOf() { STATUSCODE (4), return valueOf(this.name()); CEC (3), } HEXSEGMENTID (2), SEGMENTCOUNT (4), @Override SHA1_SUM (25), public PDUSubType getPduSubType() { RSA2048_PUBKEY (368), return this.pduSubType; RSA2048_PUBKEY_PART1 (141), } RSA2048ENC_AES128_KEY (320), RSA2048ENC_AES128_KEY_PART3 (12), @Override RSA2048_SHA512_SIG (320), public PduSegmentIdFormat getPduSegmentIdFormat() { SEGMENTID (4); return this.segIdFormat; } private final int size;

@Override private DataStrSizes(int size) { public int getSessionIdPosition() { this.size = size; return this.sessionIdPosition; } } } public int getSize() { return this.size; } }

protected static final Map statusCodes = new Hashtable(); ------static { Filename: src/main/java/com/transmisms/core/protocol/CoreProtocolFacade.java // initialize statusCodes

55 statusCodes.put("0001", "OK; Completed without error"); ExecutorService pool) { statusCodes.put("0002", "Accepted"); this(coreSession, binaryDecoder, textBasedDecoder, smsService, pool, -1); statusCodes.put("1001", "User timeout"); } statusCodes.put("1002", "Cancelled by user"); */ statusCodes.put("1003", "Rejected by user"); // NOTE: only subclasses should be able to use the base class's statusCodes.put("1101", "Blacklisted"); // constructor(s) statusCodes.put("1102", "Peer unknown"); /** * statusCodes.put("2001", "PDU not acceptable"); * @param maxPduLifetime time in seconds to indicate the Maximum PDU statusCodes.put("2002", "Request timeout"); * Lifetime */ statusCodes.put("3001", "Protocol not supported"); protected CoreProtocolFacade(Session coreSession, statusCodes.put("3002", "Version not supported"); BinaryPDUDecoder binaryDecoder, statusCodes.put("3003", "Filesystem error"); TextBasedPDUDecoder textBasedDecoder, statusCodes.put("3004", "Device error"); SmsService smsService, ExecutorService pool, int maxPduLifetime) { statusCodes.put("4001", "Remote host identification mismatch"); this.coreSession = coreSession; statusCodes.put("4101", "Key malformed"); this.binaryDecoder = binaryDecoder; statusCodes.put("4201", "File decryption failed"); this.textBasedDecoder = textBasedDecoder; statusCodes.put("4202", "File authentication failed"); this.smsService = smsService; statusCodes.put("4203", "File signature mismatch"); this.pool = pool; statusCodes.put("4901", "Too many PDU errors"); if(maxPduLifetime >= 0) { } this.coreSession.connection.maxPduLifetime = maxPduLifetime * 1000; } public static final String COMPLETED_STATUS_CODE = "0001"; } public static final String USER_ACCEPT_STATUS_CODE = "0002"; public static final String REQUEST_TIMEOUT_STATUS_CODE = "2002"; public static final String DEFAULT_ADDITIONAL_MESSAGE = "To stop receiving messages, reply with NO"; //// member methods public abstract String getSubprotocolName(); public static final Version VERSION = new Version(0, 7, 0); public static final Version MIN_VERSION = new Version(0, 7, 0); protected abstract String generateHeadPayload(); public static final Version MAX_VERSION = new Version(0, 7, 0); /** * * @return the default status message String for the statusCode // non-constant members start here */ public static String getStatusMessage(String statusCode) { public final Session coreSession; return CoreProtocolFacade.statusCodes.get(statusCode); private final BinaryPDUDecoder binaryDecoder; } private final TextBasedPDUDecoder textBasedDecoder; protected abstract void processHeadPayload(Object[] headData); public final SmsService smsService; protected abstract String[] processReqAndGenRepPayload( protected final Map segmanMap = Object[] requestData); new ConcurrentSkipListMap<>(); protected final Map> retransMap = protected abstract void onEstablished(); new ConcurrentSkipListMap<>(); public PDU decode(String encoded) protected final ExecutorService pool; throws PDUMalformedException, InvalidCRCException { protected final ExecutorService pduThreadPool = // some sanity checks before parsing Executors.newSingleThreadExecutor(); if(encoded == null) { throw new NullPointerException("Encoded string cannot be null"); private Future smsConsumerFuture; } private Future pduConsumerFuture; else if(encoded.equals("")) { protected Future timeoutLoopFuture; throw new PDUMalformedException("Encoded string cannot be empty", private Future retransLoopFuture; encoded); private Future classMainFuture; } private final Phaser timeoutPhaser = new Phaser(1); // determine if text-based, human, or binary private final AtomicReference promptLatch = if(encoded.startsWith("?transmismsv")) { // if text based protocol new AtomicReference<>(new CountDownLatch(1)); return this.textBasedDecoder.decodeTextBased(encoded); protected final AtomicBoolean waitingForPrompt = new AtomicBoolean(false); } else if(encoded.trim().toUpperCase().equals("NO")) { // human private final Phaser dumbRetransPhaser = new Phaser(1); return new PDU(CorePDUType.HUMAN_REJECT, null); private final AtomicReference dumbRetransMsg = } new AtomicReference<>(null); else { // if binary protocol return this.binaryDecoder.decodeBinary(encoded); } } protected CoreProtocolFacade(Session coreSession, BinaryPDUDecoder binaryDecoder, protected Runnable processPdu(PDU pduObject) { TextBasedPDUDecoder textBasedDecoder, SmsService smsService) { return null; this(coreSession, binaryDecoder, textBasedDecoder, smsService, } Executors.newCachedThreadPool(), -1); } // this is where the real action is private final Runnable processCorePdu(PDU pduObject) { protected CoreProtocolFacade(Session coreSession, { // process PDU from subprotocols first BinaryPDUDecoder binaryDecoder, Runnable result = this.processPdu(pduObject); TextBasedPDUDecoder textBasedDecoder, SmsService smsService, if(result != null) { int maxPduLifetime) { return result; // return if subprotocol can parse this(coreSession, binaryDecoder, textBasedDecoder, smsService, } Executors.newCachedThreadPool(), maxPduLifetime); } // otherwise, let the core handle the PDU } switch(this.coreSession.connection.getConnectionState()) { /* case CLOSED: // do nothing; we're supposedly closed protected CoreProtocolFacade(Session coreSession, case REQUEST_SENT_2: BinaryPDUDecoder binaryDecoder, case REQUEST_SENT_3: TextBasedPDUDecoder textBasedDecoder, SmsService smsService, return null;

56 default: { "PduConsumer interrupted while waiting"); BiFunction pdlFunction = break; // just break on interrupt CoreLookupTable.get(new PdlEntry( } this.coreSession.connection.getConnectionState(), if(wrapper != null) { // should not be the other way since you pduObject.getPduType())); // can't offer a null object if(pdlFunction == null) { // not in lookup table // notify timeout loop first return null; // do nothing int phaseNumber = this.timeoutPhaser.arrive(); } else { // process the received pdu return pdlFunction.apply(this, pduObject); PDU entryPdu = wrapper.getEntry(); } } } // filter out PDUs that are not for this session } Runnable runThis = null; // get session Id for the PDU /** int sessionIdPos = * entryPdu.getPduType().getSessionIdPosition(); * @throws NullPointerException if there are no observers for this object UUID sessionId = null; */ if(this.coreSession.sessionId != null && private void initializeConnection() throws NullPointerException { sessionIdPos != -1) { if(this.countObservers() <= 0) { Object o = entryPdu.getData()[sessionIdPos]; throw new NullPointerException("No observers set for this Facade"); if(o != null && o instanceof UUID) { } sessionId = (UUID)o; logAndNotify(this, MessageType.TRACE, "Initializing connection"); } } // initialize required objects // set as null if invalid, else process final CoreProtocolFacade self = this; if(sessionId != null && PriorityBlockingQueue> pbq = this.coreSession.sessionId != null && new PriorityBlockingQueue<>(); !this.coreSession.sessionId.equals((sessionId))) { if(entryPdu.getPduType() != // sms consumer thread this.smsConsumerFuture = this.pool.submit(() -> { com.transmisms.smsftp.protocol.SmsftpPDUType.SMSFTP_DATA) { // loop until interrupt or SmsService error logAndNotify(this, MessageType.TRACE, while(!Thread.currentThread().isInterrupted()) { "Received old PDU " + phaseNumber + try { ": " + entryPdu.getPduType()); // await while operation is paused } if(this.waitingForPrompt.get()) { runThis = null; this.promptLatch.get().await(); } } else { if(entryPdu.getPduType() != // get String messages from SmsService SmsEntry entry = self.smsService.pollMessage( com.transmisms.smsftp.protocol.SmsftpPDUType.SMSFTP_DATA) { 100, TimeUnit.MILLISECONDS); logAndNotify(this, MessageType.TRACE, if(entry == null) { // if timed out "Received PDU " + phaseNumber + // continue ": " + entryPdu.getPduType()); continue; } } runThis = this.processCorePdu(entryPdu); /* } // NOTE: You can uncomment this if needed logAndNotify(this, MessageType.TRACE, // run and wait "got message: " + entry.getBody()); if(runThis != null) { */ Future runThisFuture = self.pduThreadPool.submit( runThis); PDU messagePdu = null; try { { // parse then put; on errors, log erors runThisFuture.get(); // entry is guaranteed to be non-null } String encoded = entry.getBody(); catch(ExecutionException e) { try { logAndNotify(this, MessageType.TRACE, messagePdu = this.decode(encoded); "Caught: " + e); pbq.put(new FIFOEntry<>(messagePdu)); e.printStackTrace(); } } catch(InvalidCRCException e) { catch(InterruptedException e) { logAndNotify(this, MessageType.DEBUG, logAndNotify(this, MessageType.TRACE, "Invalid CRC: " + e.toString()); "PduConsumer interrupted"); } break; // just break on interrupt catch(PDUMalformedException e) { } logAndNotify(this, MessageType.DEBUG, } "Malformed PDU: " + e.toString()); } } } } }); } catch(SmsServiceException e) { // timeout loop thread } this.timeoutLoopFuture = this.pool.submit(() -> { catch(InterruptedException e) { // loop until interrupt or SmsService error logAndNotify(this, MessageType.TRACE, while(!Thread.currentThread().isInterrupted()) { "SmsConsumer interrupted"); ConnectionState cs = null; break; // just break on interrupt try { } // wait for maxPduLifetime*2 seconds or until triggered } int prevPhase = this.timeoutPhaser.getPhase(); }); this.timeoutPhaser.awaitAdvanceInterruptibly(prevPhase, this.coreSession.connection.maxPduLifetime*2, // pdu consumer thread TimeUnit.MILLISECONDS); this.pduConsumerFuture = this.pool.submit(() -> { } // loop until interrupt or SmsService error // if no PDUs received after waiting while(!Thread.currentThread().isInterrupted()) { catch(TimeoutException e) { FIFOEntry wrapper = null; cs = this.coreSession.connection.getConnectionState(); try { if(!(cs.equals(ConnectionState.CLOSED) || wrapper = pbq.take(); cs.equals(ConnectionState.TIME_WAIT) || } cs.equals(ConnectionState.CLOSE_WAIT))) { catch(InterruptedException e) { logAndNotify(this, MessageType.INFO, logAndNotify(this, MessageType.TRACE, "Timed out");

57 default: // should never happen // schedule timeout procedure encoderC[0].appendHexSegmentId(0); this.pduThreadPool.submit( } CoreLookupTable.createPeerTimeoutTask( } this, null)); }); } // finally send the last message } this.smsService.sendMessage(encoderC[0].finalizePDU()); catch(InterruptedException e) { } // Thread.interrupt(); // preserve interrupt status } /** * // check if we need to exit the loop */ cs = this.coreSession.connection.getConnectionState(); protected void sendMessageRepeatedly(String msg) { if(cs.equals(ConnectionState.CLOSED) || // set new value first cs.equals(ConnectionState.TIME_WAIT) || this.dumbRetransMsg.set(msg); cs.equals(ConnectionState.CLOSE_WAIT)) { this.dumbRetransPhaser.arrive(); logAndNotify(this, MessageType.TRACE, "Exiting TimeoutLoop..."); Future msgRepeatFuture = this.pool.submit(() -> { break; // then break the loop while(msg != null && this.dumbRetransMsg.get() != null && } msg.equals(this.dumbRetransMsg.get()) && else { !this.coreSession.connection.getConnectionState().equals( continue; // just loop and reset again ConnectionState.TIME_WAIT)) { } try { } // send message before "sleeping" }); this.smsService.sendMessage(msg); } // wait for maxPduLifetime*2 seconds or until interrupt int prevPhase = this.dumbRetransPhaser.getPhase(); /** this.dumbRetransPhaser.awaitAdvanceInterruptibly(prevPhase, * this.coreSession.connection.maxPduLifetime*2, */ TimeUnit.MILLISECONDS); protected void startRetransLoop() { } if(this.retransLoopFuture == null) { catch(TimeoutException e) { this.retransLoopFuture = this.pool.submit(() -> { // do nothing // loop until interrupt or SmsService error // just loop and retry on timeout while(!Thread.currentThread().isInterrupted() && } !this.coreSession.connection.getConnectionState() catch(InterruptedException e) { .equals(ConnectionState.TIME_WAIT)) { // just break after getting the signal try { break; // sleep before sending any retransmission PDU(s) } Thread.sleep( } this.coreSession.connection.maxPduLifetime); }); // check if we need to exit ASAP } if(this.coreSession.connection.getConnectionState() .equals(ConnectionState.TIME_WAIT)) { /** break; * } * @throws NullPointerException if there are no observers for this object this.sendRetransPdu(); */ } protected void listen() throws NullPointerException { catch(InterruptedException e) { // set state to listening // preserve interrupt status CoreProtocolFacade.setStateAndNotify(this, ConnectionState.LISTENING); Thread.currentThread().interrupt(); this.initializeConnection(); break; // then break the loop logAndNotify(this, MessageType.INFO, } "Listening for connection requests"); } // wait for request (via the smsConsumer and pduConsumer threads }); // and do nothing on the "main thread" } } // finally, just wait for other threads this.joinAll(); /** } * */ /** public void sendRetransPdu() { * final PDUEncoder[] encoderC = { new PDUEncoder(true) * @throws NullPointerException if there are no observers for this object .appendPrefix( */ CorePDUType.CORE_RETRANSMISSION, protected void connect(String additonalPayload) BinaryPDUDecoder.getInstance()) }; throws NullPointerException { // get each missed pdu type and segment number this.initializeConnection(); this.segmanMap.forEach((k, v) -> { for(Integer n : v.getSkippedSegNums()) { // class "main thread" // cut into parts with 152-character limit this.classMainFuture = this.pool.submit(() -> { if(encoderC[0].getEncoded().length() >= 152) { logAndNotify(this, MessageType.INFO, // send the message "Connecting to peer"); this.smsService.sendMessage( if(this.coreSession.connection.role == encoderC[0].finalizePDU()); Connection.Role.INITIATOR) { // then set up another instance of encoder // construct string to be appended later encoderC[0] = new PDUEncoder(true) String addStr = ""; .appendPrefix( if(additonalPayload != null) { CorePDUType.CORE_RETRANSMISSION, addStr = "/" + additonalPayload; BinaryPDUDecoder.getInstance()); } } // construct and try to send REQ PDUEncoder encoder = new PDUEncoder(false); encoderC[0].appendPrefix(k, encoder.appendVersion(CoreProtocolFacade.VERSION) this.binaryDecoder); .appendSessionId(this.coreSession switch(k.getPduSegmentIdFormat()) { .sessionId).appendPayload( case BINARY: "req/" + this.getSubprotocolName() + addStr) encoderC[0].appendBinarySegmentId(n+1); .appendAdditionalMessage( break; CoreProtocolFacade.DEFAULT_ADDITIONAL_MESSAGE) case HEX: .finalizePDU(); encoderC[0].appendHexSegmentId(n+1); this.sendMessageRepeatedly(encoder.getEncoded()); break; CoreProtocolFacade.setStateAndNotify(this, case NONE: ConnectionState.REQUEST_SENT);

58 } } else { // if receiver } protected SegmentManager registerSegMan(PDUType k, SegmentManager v) { }); return this.segmanMap.put(k, v); // finally, just wait for other threads } this.joinAll(); } protected Future submitToInternalPool(Callable task) { return this.pool.submit(task); public final void disconnect(String statusCode) { } this.disconnect(statusCode, null); } protected Future submitToInternalPool(Runnable task) { return this.pool.submit(task); // NOTE: this can be safely not subprotocol-specific } public final void disconnect(String statusCode, String statusStr) { // make sure that we are ESTABLISHED in the first place if(this.coreSession.connection.getConnectionState() == ConnectionState.ESTABLISHED) { //// utility functions logAndNotify(this, MessageType.INFO, protected static void setChangedAndNotifyObservers(CoreProtocolFacade self, "Requesting disconnection"); Object ... o) { // if statusStr is null, use the default one self.setChanged(); String bufferStr = statusStr; self.notifyObservers(o); if(bufferStr == null) { } bufferStr = CoreProtocolFacade.getStatusMessage(statusCode); protected static void logAndNotify(CoreProtocolFacade self, MessageType t, } String message) { Object[] logArr = {t, message}; // construct and try to send FIN CoreProtocolFacade.setChangedAndNotifyObservers(self, logArr); String addStr = statusCode + "/" + bufferStr; } PDUEncoder encoder = new PDUEncoder(true); encoder.appendPrefix(CorePDUType.CORE_FIN, protected static void setStateAndNotify(CoreProtocolFacade self, this.binaryDecoder) ConnectionState newState) { .appendSessionId(this.coreSession.sessionId) ConnectionState oldState = .appendStatusCode(statusCode); self.coreSession.connection.getConnectionState(); if(bufferStr != null) { self.coreSession.connection.setConnectionState(newState); encoder.appendString(bufferStr); CoreProtocolFacade.setChangedAndNotifyObservers(self, } MessageType.STATUS, Presenter.StatusType.STATE_CHANGED, encoder.finalizePDU(); oldState, newState); this.sendMessageRepeatedly(encoder.getEncoded()); } this.coreSession.connection.encodedFinPdu = encoder.getEncoded(); this.coreSession.connection.finStatusComboReceived = statusCode + protected void shutdownNow() { (bufferStr != null ? "/" + bufferStr : ""); /* // set to FIN_WAIT afterwards // stop smsService from sending and receiving messages CoreProtocolFacade.setStateAndNotify(this, this.smsService.stop(); ConnectionState.FIN_WAIT); */ } // stop any attempts/operations to establish the connection // stop sendMessageRepeatedly() ASAP else if(this.coreSession.connection.getConnectionState() == this.sendMessageRepeatedly(null); ConnectionState.LISTENING) { // start hard disconnect sequence instead which includes: // shutdown all threadPools registered on this facade // - setting ConnectionState to CLOSED ExecutorService[] deathList = { this.pool, this.pduThreadPool }; // - stopping any threads started, if any for(int i = 0; i < deathList.length; i++) { this.pool.submit(CoreLookupTable.createDisconnectTask(this, null)); deathList[i].shutdown(); // disable new tasks from being submitted // NOTE: null PDU is still okay since it's an unused parameter try { } deathList[i].shutdownNow(); // try to cancel currently running else { // tasks } // wait until tasks respond to being cancelled } if(!deathList[i].awaitTermination( 500, TimeUnit.MILLISECONDS)) { public void continueOperationAfterPrompt() { logAndNotify(this, MessageType.DEBUG, logAndNotify(this, MessageType.INFO, "Pool " + i + " did not terminate!"); "User decided to continue waiting after timeout"); } // continue all operations if needed } this.waitingForPrompt.set(false); catch(InterruptedException e) { logAndNotify(this, MessageType.DEBUG, CountDownLatch tmp = this.promptLatch.get(); "Pool " + i + " interrupted; Retrying shutdown"); // prepare a new CountDownLatch // retry cancelling current tasks again this.promptLatch.set(new CountDownLatch(1)); deathList[i].shutdownNow(); // wake the awaiting thread } tmp.countDown(); } } } public void cancelOperationAfterPrompt() { private void joinAll() { logAndNotify(this, MessageType.INFO, try { "User requested to cancel after timeout"); if(this.classMainFuture != null) { // start shutdown sequence this.classMainFuture.get(); Future timeoutDisconnectFuture = this.pool.submit( } CoreLookupTable.createTimeoutDisconnectTask(this, null)); this.smsConsumerFuture.get(); } this.pduConsumerFuture.get(); } catch(ExecutionException e) { //// exposed functions for use by package and subclasses System.out.println("thrown " + e); protected List registerRetrans(PDUType k, List v) { e.printStackTrace(); return this.retransMap.put(k, v); } } catch(InterruptedException e) { logAndNotify(this, MessageType.DEBUG, protected List registerRetrans(PDUType k, String[] a) { "CoreProtocolFacade join()s interrupted!"); return this.retransMap.put(k, Arrays.asList(a)); // preserve interrupt status } Thread.currentThread().interrupt(); } protected List getRetransList(PDUType k) { } return this.retransMap.get(k); }

59 public class PDU implements Comparable { public static final String transmismsCoreProtocolStr = "core"; ------public static final String transmismsSmsftpProtocolStr = "smsftp"; Filename: src/main/java/com/transmisms/core/protocol/InvalidCRCException.java ------private final PDUType pduType; private final Object[] data; package com.transmisms.core.protocol; private static final Map pduTypePriorities = new Hashtable(); public class InvalidCRCException extends Exception { static { // initialize pduType priorities private String malformedPDU = null; //// human reject PDU pduTypePriorities.put(CorePDUType.HUMAN_REJECT, 50); public InvalidCRCException(String message, String malformedPDU) { //// core connection/termination PDUs super(message); pduTypePriorities.put(CorePDUType.CORE_CONNECTION_REQUEST, 40); this.malformedPDU = malformedPDU; pduTypePriorities.put(CorePDUType.CORE_CONNECTION_RESPONSE, 40); } pduTypePriorities.put(CorePDUType.CORE_HEAD, 40); pduTypePriorities.put(CorePDUType.CORE_FIN, 40); public String getMalformedPDU() { pduTypePriorities.put(CorePDUType.CORE_FINACK, 40); return this.malformedPDU; pduTypePriorities.put(CorePDUType.CORE_LAST_ACK, 40); } //// retransmission PDUs } pduTypePriorities.put(CorePDUType.CORE_RETRANSMISSION, 30); //// smsftp control PDUs pduTypePriorities.put(SmsftpPDUType.SMSFTP_INIT, 20); pduTypePriorities.put(SmsftpPDUType.SMSFTP_META, 20); pduTypePriorities.put(SmsftpPDUType.SMSFTP_READY, 20); ------pduTypePriorities.put(SmsftpPDUType.SMSFTP_END, 20); Filename: src/main/java/com/transmisms/core/protocol/PdlEntry.java pduTypePriorities.put(SmsftpPDUType.SMSFTP_END_WAIT, 20); ------//// blobs pduTypePriorities.put(SmsftpPDUType.SMSFTP_DATA, 10); package com.transmisms.core.protocol; pduTypePriorities.put(SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY, 10); pduTypePriorities.put(SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY, 10); import com.transmisms.core.protocol.Connection.ConnectionState; } import com.transmisms.core.protocol.PDUType; public PDU(PDUType pduType, Object[] data) { this.pduType = pduType; this.data = data; // access is between public and protected, see Java documentation } final class PdlEntry { public final ConnectionState s; public PDUType getPduType() { public final PDUType t; return this.pduType; }

public PDUType.PDUSubType getPduSubType() { public PdlEntry(ConnectionState s, PDUType t) { return this.pduType.getPduSubType(); this.s = s; } this.t = t; } public Object[] getData() { return this.data; }

@Override // for priority comparisons only public boolean equals(Object o) { public int compareTo(PDU o) { if(o instanceof PdlEntry) { PDUType ownPDUType = this.getPduType(); PdlEntry po = (PdlEntry)o; PDUType otherPDUType = o.getPduType(); // check for nulls before proceeding with equals() Integer ownPriority = pduTypePriorities.get(ownPDUType); if(po.s == null || po.t == null || Integer otherPriority = pduTypePriorities.get(otherPDUType); this.s == null || this.t == null) { if(ownPriority == null) { return false; ownPriority = 0; } } return this.s.equals(po.s) && this.t.equals(po.t); if(otherPriority == null) { } otherPriority = 0; else { } return false; } return otherPriority-ownPriority; } } } @Override public int hashCode() { return this.s.hashCode() + this.t.hashCode(); } // CAUTION: this has no protection against nulls and problematic types------} Filename: src/main/java/com/transmisms/core/protocol/PDUEncoder.java ------

package com.transmisms.core.protocol;

------import com.transmisms.core.protocol.CoreProtocolFacade.DataSizes; Filename: src/main/java/com/transmisms/core/protocol/PDU.java import com.transmisms.core.util.codec.Base85; ------import net.sourceforge.javaflacencoder.CRC8; package com.transmisms.core.protocol; import java.nio.ByteBuffer; import com.transmisms.core.protocol.PDUType; import java.util.Arrays; import com.transmisms.smsftp.protocol.SmsftpPDUType; import java.util.UUID; import java.util.Hashtable; import java.io.UnsupportedEncodingException; import java.util.Map;

60 raw.length-DataSizes.SEGMENTID.getSize(), raw.length); // convenience class to construct PDUs this.appendString(Base85.encode(truncated)); public class PDUEncoder { return this; } private boolean isBinary; private boolean isFinalized; public PDUEncoder appendStatusCode(String statusCode) { private String encoded; this.appendString(statusCode); return this; public PDUEncoder(boolean isBinary) { } this.isBinary = isBinary; // the rest can just use the appendString() method this.isFinalized = false; this.encoded = ""; /* UTILITY FUNCTIONS */

if(!isBinary) { public static String UUIDToBase85(UUID uuidObject) { this.encoded+="?transmismsv"; Long msb = uuidObject.getMostSignificantBits(); } Long lsb = uuidObject.getLeastSignificantBits(); } byte[] bytes = ByteBuffer.allocate(2*Long.BYTES).putLong(msb) .putLong(lsb).array(); public String getEncoded() { return Base85.encode(bytes); return this.encoded; } } }

public String finalizePDU() { if(!this.isFinalized) { if(this.isBinary) { // append the CRC for this PDU ------CRC8 crc = new CRC8(); Filename: src/main/java/com/transmisms/core/protocol/PDUMalformedException.java try { ------crc.updateCRC8(this.getEncoded().getBytes("UTF-8"), 0, this.getEncoded().getBytes("UTF-8").length); package com.transmisms.core.protocol; this.appendString(String.format("%02x", crc.checksum())); } catch(UnsupportedEncodingException e) { public class PDUMalformedException extends Exception { // this should never happen; crash early throw new IllegalStateException(e.getMessage(), e); private String malformedPDU = null; } } public PDUMalformedException(String message, String malformedPDU) { this.isFinalized = true; super(message); } this.malformedPDU = malformedPDU; return this.getEncoded(); } } public String getMalformedPDU() { public PDUEncoder appendString(String s) { return this.malformedPDU; if(!this.isFinalized) { } this.encoded+=s; } } return this; }

/* TEXT-BASED PDUS */ ------Filename: src/main/java/com/transmisms/core/protocol/PDUType.java public PDUEncoder appendVersion(Version v) { ------this.appendString(v + "\n"); return this; package com.transmisms.core.protocol; }

public PDUEncoder appendSessionId(UUID sessionId) { this.appendString(PDUEncoder.UUIDToBase85(sessionId)); public interface PDUType { if(!this.isBinary) { this.appendString("\n"); public enum PDUSubType { BINARY, } TEXT_BASED, return this; HUMAN }; } public enum PduSegmentIdFormat { NONE, public PDUEncoder appendPayload(String payload) { HEX, this.appendString(payload + "\n"); BINARY }; return this; } public T valueOf(); public PDUEncoder appendAdditionalMessage(String message) { this.appendString("\n" + message); public PDUSubType getPduSubType(); return this; public PduSegmentIdFormat getPduSegmentIdFormat(); } public int getSessionIdPosition(); } /* BINARY PDUS */

public PDUEncoder appendPrefix(PDUType x, BinaryPDUDecoder b) { this.appendString(b.pduTypeToPrefix(x) + ""); return this; ------} Filename: src/main/java/com/transmisms/core/protocol/Presenter.java ------public PDUEncoder appendHexSegmentId(int segmentId) { this.appendString(String.format("%02x", segmentId)); package com.transmisms.core.protocol; return this; } import java.util.Observable; import java.util.Observer; public PDUEncoder appendBinarySegmentId(int segmentId) { ByteBuffer buffer = ByteBuffer.allocate(Integer.BYTES); buffer.putInt(segmentId); byte[] raw = buffer.array(); public abstract class Presenter implements Observer { byte[] truncated = Arrays.copyOfRange(raw, public enum MessageType {

61 // UI-related throw new IllegalArgumentException( PROMPT, "Segment number should not be negative"); STATUS, } // logging if(segNum >= this.segments.length) { FATAL, throw new IllegalArgumentException( ERROR, "Segment number should not be greater than or equal " + WARN, "to the number of segments"); INFO, } DEBUG, TRACE // check if can be updated } if(this.segments[segNum] != null) { return true; public enum PromptType { } CONNECTION_REQUEST, // add if null/update if existing TIMEOUT, this.segments[segNum] = segment; SMS_SERVICE_ERROR, PEER_ERROR_EXCEEDED_LIMIT // update missingnos } this.missingnos.remove(segNum); if(this.lastSegmentReceived < segNum) { public enum StatusType { // add skipped segments to missingnos, if any STATE_CHANGED, for(int i = lastSegmentReceived+1; i < segNum; i++) { NOTICE if(this.segments[i] == null) { } this.missingnos.add(i); } @Override } public abstract void update(Observable o, Object arg); // do nothing // add the next segNum to missingnos } if(segNum+1 < this.segCount) { this.missingnos.add(segNum+1); } this.lastSegmentReceived = segNum; // update last segment number } ------Filename: src/main/java/com/transmisms/core/protocol/SegmentManager.java // preemptive consolidation ------this.getCompletedString(); package com.transmisms.core.protocol; return true; } import java.util.concurrent.ConcurrentSkipListSet; import java.util.concurrent.atomic.AtomicReference; public Integer[] getSkippedSegNums() { return this.missingnos.toArray(new Integer[0]); } public class SegmentManager { public int getLastSegmentReceived() { private final String[] segments; return this.lastSegmentReceived; public final int segCount; }

private final AtomicReference completedString = /** new AtomicReference<>(); * @return the combined segments if completed; null otherwise */ // properties used for tracking 'lost' segments // Consolidates segments if not yet completed private final ConcurrentSkipListSet missingnos = public synchronized String getCompletedString() { new ConcurrentSkipListSet<>(); // check for segments complete conditions and do appropriate private int lastSegmentReceived = -1; // actions if(this.completedString.get() == null && this.missingnos.isEmpty() && this.lastSegmentReceived+1 == this.segments.length) { public SegmentManager(int segCount) { // consolidate and convert String data if(segCount <= 0) { StringBuilder sb = new StringBuilder(); throw new IllegalArgumentException( for(String s : this.segments) { "Segment count should be greater than 0"); sb.append(s); } } this.missingnos.add(0); // seed first segment this.completedString.set(sb.toString()); this.segCount = segCount; } this.segments = new String[this.segCount]; } return this.completedString.get(); } }

private synchronized String getSegment(int segNum) { if(segNum < 0) { throw new IllegalArgumentException( "Segment number should not be negative"); ------} Filename: src/main/java/com/transmisms/core/protocol/Session.java if(segNum >= this.segments.length) { ------throw new IllegalArgumentException( "Segment number should not be greater than or equal " package+ com.transmisms.core.protocol; "to the number of segments"); } import java.util.UUID; return this.segments[segNum]; import java.nio.ByteBuffer; }

/** * public abstract class Session { * @return true if segment is updated; false if segment already exists */ public final Connection connection; public synchronized boolean updateSegment(String segment, int segNum) { if(segment == null) { public UUID sessionId; throw new IllegalArgumentException( "Segment cannot be null"); } if(segNum < 0) { /**

62 * Creates a new Session with the specified Session Id */ public Session(Connection connection, UUID sessionId) { this.sessionId = sessionId; public class SmsServiceException extends Exception { this.connection = connection; public interface Reason { } public T valueOf(); } }

public final Reason reason;

public SmsServiceException(Reason reason) { ------super(reason.toString()); Filename: src/main/java/com/transmisms/core/protocol/SmsEntry.java this.reason = reason; ------} package com.transmisms.core.protocol; public Reason getReason() { return this.reason; import java.util.Date; } } public class SmsEntry { private String sender; private String body; ------private Date timestamp; Filename: src/main/java/com/transmisms/core/protocol/TextBasedPDUDecoder.java ------public SmsEntry(String sender, String body) { this(sender, body, new Date()); package com.transmisms.core.protocol; } import com.transmisms.core.protocol.PDU; public SmsEntry(String sender, String body, Date timestamp) { import com.transmisms.core.protocol.PDUType; this.sender = sender; import com.transmisms.core.protocol.PDUMalformedException; this.body = body; this.timestamp = timestamp; import java.util.Arrays; } import java.util.ArrayList; import java.util.List; public String getSender() { import java.util.regex.Pattern; return this.sender; } import java.util.UUID; import java.nio.ByteBuffer; public String getBody() { import java.io.IOException; return this.body; import com.transmisms.core.util.codec.Base85; } import com.transmisms.core.protocol.CoreProtocolFacade.DataSizes;

public Date getTimestamp() { return this.timestamp; } public class TextBasedPDUDecoder { } private static final Pattern LF_PATTERN = Pattern.compile("\n"); private static final Pattern LFLF_PATTERN = Pattern.compile("\n\n"); private static final Pattern DOT_PATTERN = Pattern.compile("\\."); private static final Pattern SLASH_PATTERN = Pattern.compile("/");

------private String[] knownProtocols; Filename: src/main/java/com/transmisms/core/protocol/SmsService.java ------private static TextBasedPDUDecoder thisInstance; static { package com.transmisms.core.protocol; String[] emptyArray = {}; TextBasedPDUDecoder.thisInstance = new TextBasedPDUDecoder(emptyArray); import java.util.concurrent.TimeUnit; }

protected TextBasedPDUDecoder(String[] knownProtocols) { this.knownProtocols = knownProtocols; // NOTE: handling queuing, setting destination number, etc. should be done } // by the implementer public interface SmsService { public static TextBasedPDUDecoder getInstance() { return TextBasedPDUDecoder.thisInstance; public void sendMessage(String message); }

public SmsEntry pollMessage(long timeout, TimeUnit unit) throws SmsServiceException, InterruptedException; private static UUID base85ToUUID(String b85String, String encodedStr, // for running methods that the service might require (to be run from String pduTypeName) // background threads) throws PDUMalformedException { public boolean start() throws SmsServiceException; // extract byte[] format from base85 public boolean stop(); byte[] uuidBytes = null; try { public boolean isRunning(); uuidBytes = Base85.decode(b85String); } // NOTE: we don't need to check for a ready state here as the service might catch(IOException e) { // become unavailable immediately after we have verified that the throw new PDUMalformedException( // service is available pduTypeName + "'s Session ID is corrupt", encodedStr); } } // verify if sessionId's length is correct if(uuidBytes.length != DataSizes.SESSIONID.getSize()) { throw new PDUMalformedException( pduTypeName + "'s Session ID's byte length is incorrect", ------null); Filename: src/main/java/com/transmisms/core/protocol/SmsServiceException.java } ------// split the byte[] into msb and lsb package com.transmisms.core.protocol; byte[] msb = Arrays.copyOfRange(uuidBytes, 0, 8);

63 byte[] lsb = Arrays.copyOfRange(uuidBytes, 8, 16); } // convert the msb and lsb byte[] into long and return afterwards else { ByteBuffer msbBuffer = ByteBuffer.allocate(Long.BYTES).put(msb); throw new PDUMalformedException("Invalid Payload Type: " + ByteBuffer lsbBuffer = ByteBuffer.allocate(Long.BYTES).put(lsb); payloadParts[0], encoded); msbBuffer.flip(); // required for reading } lsbBuffer.flip(); // required for reading return new UUID(msbBuffer.getLong(), lsbBuffer.getLong()); { // check if protocol name is valid } String protocolName = payloadParts[1]; boolean validProtocolName = false; protected Object[] decodePayloadSubparts(PDUType pduType, for(int i = 0; i < this.knownProtocols.length; i++) { String[] payloadSubparts, String encoded) if(this.knownProtocols[i].equals(protocolName)) { throws PDUMalformedException { validProtocolName = true; // does nothing alone; just return back the payloadSubparts } return payloadSubparts; } } if(validProtocolName) { decodedData.add(protocolName); public final PDU decodeTextBased(String encoded) } throws PDUMalformedException { else { CorePDUType pduType = null; throw new PDUMalformedException("Invalid Protocol name: " + List decodedData = new ArrayList(); protocolName, encoded); } String machineMsg = ""; } String humanMsg = null; { // split machine and human-readable messages { String[] lflfParts = LFLF_PATTERN.split(encoded); String[] subParts = {}; if(lflfParts.length > 1) { if(payloadParts.length > 2) { // last LFLF separated string is the human-readable message subParts = Arrays.copyOfRange(payloadParts, 2, humanMsg = lflfParts[lflfParts.length-1]; payloadParts.length); // join back remaining parts as machine-readable message } machineMsg = ""; for(int i = 0; i < lflfParts.length-1; i++) { Object[] addLater = machineMsg += lflfParts[i]; this.decodePayloadSubparts(pduType, subParts, encoded); } for(Object o : addLater) { } decodedData.add(o); else { // no additional message } machineMsg = lflfParts[0]; } } } } // finally, add human message decodedData.add(humanMsg); // split machine message into segments String[] segments = LF_PATTERN.split(machineMsg); return new PDU(pduType, decodedData.toArray()); // check for "?transmismsvx.x.x" header } if(!segments[0].startsWith("?transmismsv")) { } throw new PDUMalformedException("Invalid Header: " + segments[0], encoded); } // get Version try { ------String[] versionParts = TextBasedPDUDecoder.DOT_PATTERN.split(Filename: src/main/java/com/transmisms/core/protocol/Version.java segments[0].substring(12)); ------if(versionParts.length != 3) { throw new PDUMalformedException("Invalid Version format: "package + com.transmisms.core.protocol; segments[0].substring(12), encoded); } public class Version implements Comparable { int a = new Integer(versionParts[0]); protected final int[] data = new int[3]; int b = new Integer(versionParts[1]); int c = new Integer(versionParts[2]); public Version(int a, int b, int c) throws IllegalArgumentException { if(a < 0 || b < 0 || c < 0) { if(a < 0 || b < 0 || c < 0) { throw new NumberFormatException(); // pass responsibility throw new IllegalArgumentException( // to the catcher below "Version numbers cannot be negative"); } } Version ver = new Version(a, b, c); this.data[0] = a; decodedData.add(ver); this.data[1] = b; } this.data[2] = c; catch(NumberFormatException e) { } throw new PDUMalformedException("Invalid Version: " + segments[0].substring(12), encoded); @Override } public int compareTo(Version o) { return compare(o, 0); { // get Session Id } String sessionIdStr = segments[1]; UUID sessionId = base85ToUUID(sessionIdStr, encoded, @Override "Text-based PDU"); public boolean equals(Object o) { decodedData.add(sessionId); if(!(o instanceof Version)) { } return false; } { // parse payload else { String[] payloadParts = TextBasedPDUDecoder.SLASH_PATTERN.split( return (0 == compareTo((Version)o)); segments[2]); } // get PDU Type } if(payloadParts[0].equals("req")) { pduType = CorePDUType.CORE_CONNECTION_REQUEST; @Override } public int hashCode() { else if(payloadParts[0].equals("rep")) { // prime numbers arbitrarily chosen at random in descending order pduType = CorePDUType.CORE_CONNECTION_RESPONSE; return (7919*this.data[0]) + (809*this.data[1]) + (37*this.data[2]); } } else if(payloadParts[0].equals("head")) { pduType = CorePDUType.CORE_HEAD; private int compare(Version o, int start) { } if(this.data[start] < o.data[start]) { else if(payloadParts[0].equals("last")) { return -1; pduType = CorePDUType.CORE_LAST_ACK; }

64 else if(this.data[start] > o.data[start]) { return 1; /** } * Decodes ASCII85 encoded string else { * if(start == 2) { * @param data String to be decoded from ASCII85 return 0; * @return decoded data } */ else { public static byte[] decode(String data) throws IOException { return this.compare(o, start+1); // remove all unused whitespaces } String encoded = data.trim() } // and replace back all replaced characters on encoding to ASCII } .replace("v", "[") .replace("w", "\\") @Override .replace("x", "]") public String toString() { .replace("y", "^") return this.data[0] + "." + this.data[1] + "." + this.data[2]; .replace("£", "`"); } } // re-add the ~ suffix if missing if(!encoded.endsWith("~") || !encoded.endsWith("~>")) { encoded = encoded + "~"; }

------ByteArrayInputStream bais = new Filename: src/main/java/com/transmisms/core/util/codec/Base85.java ByteArrayInputStream(encoded.getBytes("UTF-8")); ------ASCII85InputStream a85is = new ASCII85InputStream(bais); package com.transmisms.core.util.codec; ArrayList buff = new ArrayList(); import org.apache.pdfbox.io.ASCII85InputStream; int d; import org.apache.pdfbox.io.ASCII85OutputStream; while((d = a85is.read()) != -1) { buff.add((byte)d); import java.io.ByteArrayInputStream; } import java.io.ByteArrayOutputStream; byte[] decoded = new byte[buff.size()]; import java.util.ArrayList; for(int i = 0; i < buff.size(); i++) { decoded[i] = buff.get(i); import java.io.IOException; }

/** a85is.close(); * */ return decoded; public class Base85 { }

/** } * Encodes data to ASCII85 * * @param data data to be encoded into ASCII85 * @return ASCII85 encoded string */ ------public static String encode(byte[] data) { Filename: src/main/java/com/transmisms/core/util/commons/FIFOEntry.java ByteArrayOutputStream baos = new ByteArrayOutputStream(); ------ASCII85OutputStream a85os = new ASCII85OutputStream(baos); package com.transmisms.core.util.commons; String encoded = null; try { a85os.write(data); import java.util.concurrent.atomic.AtomicLong; a85os.flush();

encoded = baos.toString("UTF-8").trim(); // because pdfbox's ascii85 appendspublic class FIFOEntry> // an annoying \n (int 10) every implements Comparable> { // ducking single time static final AtomicLong seq = new AtomicLong(0); final long seqNum; baos.close(); final private E entry; a85os.close(); } public FIFOEntry(E entry) { catch(IOException e) { // should never happen since we are just using this.seqNum = FIFOEntry.seq.getAndIncrement(); // mostly safe ByteArrayOutputStream this.entry = entry; assert false; } return null; } public int compareTo(FIFOEntry other) { int res = this.entry.compareTo(other.entry); // remove ~ and ~> suffixes, if present if(res == 0 && other.entry != this.entry) { if(encoded.endsWith("~")) { res = (this.seqNum < other.seqNum ? -1 : 1); encoded = encoded.substring(0, encoded.length()-1); } } return res; else if(encoded.endsWith("~>")) { } encoded = encoded.substring(0, encoded.length()-2); } public E getEntry() { return this.entry; // remove all unused whitespaces } return encoded.replace("\n", "") } .replace("\r", "") .replace(" ", "") // and replace all non-GSM 7-bit alphabet characters with compliant // ones .replace("[", "v") ------.replace("\\", "w") Filename: src/main/java/com/transmisms/core/util/compression/XZ.java .replace("]", "x") ------.replace("^", "y") .replace("`", "£"); package com.transmisms.core.util.compression; }

65 import org.apache.commons.io.IOUtils; * Encrypts InputStream is with the object's key and outputs it to import org.apache.commons.compress.compressors.xz.XZCompressorInputStream; * OutputStream os. import org.apache.commons.compress.compressors.xz.XZCompressorOutputStream; * * @param is data to be encrypted will be read from import java.io.InputStream; * @param os encrypted data will be written to import java.io.OutputStream; */ public static void encrypt(byte[] key, byte[] iv, InputStream is, import java.io.IOException; OutputStream os) throws IOException { // read key from input stream /** SecretKeySpec sks = byteArrayToKeySpec(key); * Cipher c = AES.getCipher(); */ // encrypt the stream public class XZ { try { c.init(Cipher.ENCRYPT_MODE, sks, new IvParameterSpec(iv)); /** } * catch(InvalidKeyException e) { */ System.err.println("InvalidKeyException at AES.encrypt() !!!"); public static void compress(InputStream is, OutputStream os) } throws IOException { catch(InvalidAlgorithmParameterException e) { // we're going to use default compression level: 6 System.err.println( XZCompressorOutputStream xzos = new XZCompressorOutputStream(os); "InvalidAlgorithmParameterException at AES.encrypt() !!!"); } xzos.write(IOUtils.toByteArray(is)); CipherOutputStream cos = new CipherOutputStream(os, c); xzos.flush(); IOUtils.copy(is, cos); cos.close(); xzos.close(); } } /** /** * Decrypts InputStream is with the object's key and outputs it to * * OutputStream os. */ * public static void decompress(InputStream is, OutputStream os) * @param is data to be decrypted will be read from throws IOException { * @param os decrypted data will be written to XZCompressorInputStream xzis = new XZCompressorInputStream(is); */ public static void decrypt(byte[] key, byte[] iv, InputStream is, os.write(IOUtils.toByteArray(xzis)); OutputStream os) throws IOException { // read key from input stream xzis.close(); SecretKeySpec sks = byteArrayToKeySpec(key); } Cipher c = AES.getCipher(); } // decrypt the stream try { c.init(Cipher.DECRYPT_MODE, sks, new IvParameterSpec(iv)); } ------catch(InvalidKeyException e) { Filename: src/main/java/com/transmisms/core/util/crypto/AES.java System.err.println("InvalidKeyException at AES.decrypt() !!!"); ------} catch(InvalidAlgorithmParameterException e) { package com.transmisms.core.util.crypto; System.err.println( "InvalidAlgorithmParameterException at AES.decrypt() !!!"); import java.security.SecureRandom; } import javax.crypto.Cipher; CipherInputStream cis = new CipherInputStream(is, c); import javax.crypto.CipherInputStream; IOUtils.copy(cis, os); import javax.crypto.CipherOutputStream; cis.close(); import javax.crypto.KeyGenerator; } import javax.crypto.SecretKey; import javax.crypto.spec.IvParameterSpec; /** import javax.crypto.spec.SecretKeySpec; * Generates a 128-bit AES key * import org.apache.commons.io.IOUtils; * @return a 128-bit AES key */ import java.io.InputStream; public static byte[] generateKey() { import java.io.OutputStream; SecretKey key = null; try { import java.security.GeneralSecurityException; KeyGenerator kg = KeyGenerator.getInstance(_AES_KEYGEN_ALGO); import java.security.InvalidAlgorithmParameterException; kg.init(_AES_DEFAULT_KEY_SIZE, new SecureRandom()); import java.security.InvalidKeyException; key = kg.generateKey(); import java.io.IOException; }

// this arises from KeyGenerator.getInstance() // this should be impossible to reach! /** catch(NullPointerException e) { * System.err.println( */ "NullPointerException at AES.generateKey() !!!"); public final class AES { } // see AES() cipher.getInstance() /* private final static String _AES_KEYGEN_ALGO = "AES"; * Covers the following: private final static String _AES_CIPHER_ALGO = "AES"; * - NoSuchAlgorithmException from KeyGenerator.getInstance() private final static String _AES_CIPHER_ALGO_MODE_CBC = "CBC"; * - InvalidAlgorithmParameterException from kg.init() // NOTE: this is actually a PKCS7Padding internally, see Sun's padding */ // security documentation (more of a historical artifact to prevent // this should be impossible to reach! // breaking things; > 8 bytes would yield as PKCS7 instead of catch(GeneralSecurityException e) { // PKCS5) System.err.println( private final static String _AES_CIPHER_ALGO_PADDING_PKCS7 = "GeneralSecurityException at AES.generateKey() !!!"); "PKCS5Padding"; } private final static int _AES_DEFAULT_KEY_SIZE = 128; return key.getEncoded(); }

/** /** * Generates a SecretKeySpec using this class's parameters from the

66 * provided key Filename: src/main/java/com/transmisms/core/util/crypto/RSA.java * ------* @return a SecretKeySpec using this class's parameters from the * provided key package com.transmisms.core.util.crypto; */ public static SecretKeySpec byteArrayToKeySpec(byte[] key) { import org.bouncycastle.util.io.pem.PemReader; return new SecretKeySpec(key, _AES_KEYGEN_ALGO); import org.bouncycastle.util.io.pem.PemWriter; } import org.bouncycastle.util.io.pem.PemObject; import org.bouncycastle.util.encoders.DecoderException;

import java.security.KeyFactory; private static Cipher getCipher() { import java.security.KeyPair; try { import java.security.KeyPairGenerator; Cipher c = Cipher.getInstance(_AES_CIPHER_ALGO + "/" + import java.security.PrivateKey; _AES_CIPHER_ALGO_MODE_CBC + "/" import+ java.security.PublicKey; _AES_CIPHER_ALGO_PADDING_PKCS7);import java.security.Signature; return c; import java.security.spec.PKCS8EncodedKeySpec; } import java.security.spec.X509EncodedKeySpec; /* import javax.crypto.Cipher; * Covers the following: * - NoSuchAlgorithmException from Cipher.getInstance() import org.apache.commons.io.IOUtils; * - NoSuchPaddingException from Cipher.getInstance() */ import java.io.InputStream; // this should be impossible to reach! import java.io.OutputStream; catch(GeneralSecurityException e) { import java.io.InputStreamReader; assert false; // this should never happen import java.io.OutputStreamWriter; return null; } import java.security.GeneralSecurityException; } import java.security.NoSuchAlgorithmException; } import java.security.spec.InvalidKeySpecException; import java.io.IOException;

/** ------* Filename: */ src/main/java/com/transmisms/core/util/crypto/BiometricFingerprint.java public final class RSA { ------private final static int _RSA_DEFAULT_KEY_SIZE = 2048; private final static String _RSA_ALGO = "RSA"; package com.transmisms.core.util.crypto; private final static String _RSA_CIPHER_ALGO = "RSA"; // NOTE: ECB on RSA is interpreted by java security as a null cipher algo import technology.zeroalpha.security.pgpwordlist.PGPWordListConverter; private final static String _RSA_CIPHER_ALGO_MODE_ECB = "ECB"; import technology.zeroalpha.security.pgpwordlist.InvalidHexValueException; private final static String _RSA_ALGO_PADDING_OAEP = import org.apache.commons.codec.digest.DigestUtils; "OAEPWithSHA-256AndMGF1Padding"; private final static String _SHA512_WITH_RSA = "SHA512withRSA";

/** private static KeyFactory keyFactory; * */ static { public final class BiometricFingerprint { try { RSA.keyFactory = KeyFactory.getInstance(_RSA_ALGO); private static final PGPWordListConverter converter = } new PGPWordListConverter(); catch(NoSuchAlgorithmException e) { // this should never happen assert false; /** RSA.keyFactory = null; * } */ } public static String getBiometricFingerprint(byte[] data) throws NullPointerException { private static Cipher generateCipher() { // get SHA1 checksum of data try { String sha1Hex = DigestUtils.sha1Hex(data); return Cipher.getInstance(_RSA_CIPHER_ALGO + "/" + _RSA_CIPHER_ALGO_MODE_ECB + "/" + _RSA_ALGO_PADDING_OAEP); // translate hex string fingerprint to PGP words } boolean oddWord = true; /* String bioFingerprint = ""; * Covers the following: try { * - NoSuchAlgorithmException for(int i = 0; i < sha1Hex.length()/2 ; i++) { * - NoSuchPaddingException String currentWord = sha1Hex.substring(i*2,(i*2)+2); */ bioFingerprint += i==0 ? "" : " "; catch(GeneralSecurityException e) { // this should never happen bioFingerprint += oddWord ? assert false; BiometricFingerprint.converter.getOddWordForHexValue( return null; currentWord) : } BiometricFingerprint.converter.getEvenWordForHexValue( } currentWord); oddWord = !oddWord; // next odd/even word private static Signature generateSignature() { } try { } return Signature.getInstance(_SHA512_WITH_RSA); // this should never happen as the hex returned by DigestUtils should } // always be a valid hex catch(NoSuchAlgorithmException e) { // this should never happen catch(InvalidHexValueException e) { assert false; throw new NullPointerException("Got InvalidHexValueException!"); return null; } } } return bioFingerprint; } public static PrivateKey generatePrivate(byte[] key) } throws InvalidKeySpecException { PKCS8EncodedKeySpec pkcs8Spec = new PKCS8EncodedKeySpec(key); return RSA.keyFactory.generatePrivate(pkcs8Spec); }

------public static PublicKey generatePublic(byte[] key)

67 throws InvalidKeySpecException { } X509EncodedKeySpec x509Spec = new X509EncodedKeySpec(key); /* return RSA.keyFactory.generatePublic(x509Spec); * Covers the following: } * - InvalidKeySpecException from (RSA.)KeyFactory.generatePrivate() * - InvalidKeyException from Cipher.init() /** * - IllegalBlockSizeException from Cipher.doFinal() * */ */ catch(GeneralSecurityException e) { public static byte[] sign(byte[] key, InputStream is) throws IOException { System.err.println("GeneralSecurityException at decrypt() !!!"); Signature sig = RSA.generateSignature(); } try { } PrivateKey privKey = RSA.generatePrivate(key); sig.initSign(privKey); /** sig.update(IOUtils.toByteArray(is)); * return sig.sign(); */ } // this does not guarantee however that the read data will be correct /* public static byte[] readPublicKeyFromPEM(InputStream source) * Covers the following: throws RSADecoderException { * - InvalidKeySpecException from (RSA.)KeyFactory.generatePrivate() PemReader pemReader = new PemReader(new InputStreamReader(source)); * - InvalidKeyException from Signature.initSign() PemObject obj = null; * - SignatureException from Signature.update() and Signature.sign() try { */ obj = pemReader.readPemObject(); catch(GeneralSecurityException e) { } System.err.println("GeneralSecurityException at sign() !!!"); catch(DecoderException e) { } throw new RSADecoderException(e.getMessage(), e.getCause()); return null; // catch all scenario if something has gone wrong } } catch(IOException e) { // NOTE: this method of "filtering" exceptions is only for(StackTraceElement el : e.getStackTrace()) { /** if(el.getClassName() == * "org.bouncycastle.util.io.pem.PemReader" */ && el.getMethodName() == "loadObject") { public static boolean verify(byte[] key, InputStream is, byte[] sigToVer) throw new RSADecoderException(e.getMessage(), throws IOException { e.getCause()); Signature sig = RSA.generateSignature(); } try { } PublicKey pubKey = RSA.generatePublic(key); } sig.initVerify(pubKey); if(obj == null) { sig.update(IOUtils.toByteArray(is)); throw new RSADecoderException("Empty key read from PEM file"); return sig.verify(sigToVer); } } return obj.getContent(); /* } * Covers the following: * - InvalidKeySpecException from (RSA.)KeyFactory.generatePublic() /** * - InvalidKeyException from Signature.initVerify() * * - SignatureException from Signature.update() and Signature.verify() */ */ // this does not guarantee that the bytes written are valid public keys catch(GeneralSecurityException e) { public static void writePublicKeyToPEM(byte[] pubKey, System.err.println("GeneralSecurityException at verify() !!!"); OutputStream destination) throws IOException { } PemWriter pemWriter = new PemWriter( return false; // catch all scenario if something has gone wrong new OutputStreamWriter(destination)); } pemWriter.writeObject(new PemObject("PUBLIC KEY", pubKey)); pemWriter.close(); /** } * */ /** public static void encrypt(byte[] key, InputStream is, OutputStream os) * throws IOException { */ byte[] src = IOUtils.toByteArray(is); public static void generateKeys(OutputStream privKeyOs, OutputStream pubKeyOs) throws IOException { Cipher cipher = RSA.generateCipher(); KeyPairGenerator kpg = null; try { try { PublicKey pubKey = RSA.generatePublic(key); kpg = KeyPairGenerator.getInstance("RSA"); cipher.init(Cipher.ENCRYPT_MODE, pubKey); } os.write(cipher.doFinal(src)); catch(NoSuchAlgorithmException e) { os.flush(); System.err.println( } "NoSuchAlgorithmException at RSA.generateKeys() !!!"); /* } * Covers the following: * - InvalidKeySpecException from (RSA.)KeyFactory.generatePublic() kpg.initialize(_RSA_DEFAULT_KEY_SIZE); * - InvalidKeyException from Cipher.init() KeyPair kp = kpg.genKeyPair(); * - IllegalBlockSizeException from Cipher.doFinal() */ byte[] pubKey = kp.getPublic().getEncoded(); catch(GeneralSecurityException e) { byte[] privKey = kp.getPrivate().getEncoded(); System.err.println("GeneralSecurityException at encrypt() !!!"); } pubKeyOs.write(pubKey); } pubKeyOs.flush(); privKeyOs.write(privKey); /** privKeyOs.flush(); * } */ } public static void decrypt(byte[] key, InputStream is, OutputStream os) throws IOException { byte[] src = IOUtils.toByteArray(is);

Cipher cipher = RSA.generateCipher(); ------try { Filename: src/main/java/com/transmisms/core/util/crypto/RSADecoderException.java PrivateKey privKey = RSA.generatePrivate(key); ------cipher.init(Cipher.DECRYPT_MODE, privKey); os.write(cipher.doFinal(src)); package com.transmisms.core.util.crypto; os.flush();

68 public final class RSADecoderException extends Exception { import java.io.IOException;

/** * @see Exception#Exception(String) Exception */ public class SmsftpBinaryPduDecoder extends BinaryPDUDecoder { public RSADecoderException(String message) { private final static List validNoencCec = new ArrayList(); super(message); private final static List validEncCec = new ArrayList(); } private static final Map prefixToTypeLookup = /** new Hashtable<>(); * @see Exception#Exception(String, Throwable) Exception private static final Map typeToPrefixLookup = */ new Hashtable<>(); public RSADecoderException(String message, Throwable cause) { // utility function for use for the maps above super(message, cause); private static void biDiAddToLookup(Character c, PDUType p) { } PDUType evalValue = } SmsftpBinaryPduDecoder.prefixToTypeLookup.put(c, p); assert null == evalValue; Character evalValue2 = SmsftpBinaryPduDecoder.typeToPrefixLookup.put(p, c); assert null == evalValue2; ------} Filename: src/main/java/com/transmisms/smsftp/protocol/PdlEntry.java ------private static SmsftpBinaryPduDecoder thisInstance; static { package com.transmisms.smsftp.protocol; // initialize instance SmsftpBinaryPduDecoder.thisInstance = new SmsftpBinaryPduDecoder( import com.transmisms.core.protocol.PDUType; (c) -> SmsftpBinaryPduDecoder.prefixToTypeLookup.get(c), import com.transmisms.smsftp.protocol.SmsftpSession.SessionState; (p) -> SmsftpBinaryPduDecoder.typeToPrefixLookup.get(p));

// populate custom lookup maps SmsftpBinaryPduDecoder.biDiAddToLookup('a', // access is between public and protected, see Java documentation SmsftpPDUType.SMSFTP_INIT); final class PdlEntry { SmsftpBinaryPduDecoder.biDiAddToLookup('b', public final SessionState s; SmsftpPDUType.SMSFTP_META); public final PDUType t; SmsftpBinaryPduDecoder.biDiAddToLookup('c', SmsftpPDUType.SMSFTP_READY); SmsftpBinaryPduDecoder.biDiAddToLookup('d', SmsftpPDUType.SMSFTP_DATA); public PdlEntry(SessionState s, PDUType t) { SmsftpBinaryPduDecoder.biDiAddToLookup('e', this.s = s; SmsftpPDUType.SMSFTP_END); this.t = t; SmsftpBinaryPduDecoder.biDiAddToLookup('E', } SmsftpPDUType.SMSFTP_END_WAIT); SmsftpBinaryPduDecoder.biDiAddToLookup('i', SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY); SmsftpBinaryPduDecoder.biDiAddToLookup('j', @Override SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY); public boolean equals(Object o) { if(o instanceof PdlEntry) { // populate valid CEC values return this.s == ((PdlEntry)o).s && this.t == ((PdlEntry)o).t; validNoencCec.add("XXC"); } validNoencCec.add("XXX"); else { validEncCec.add("XEX"); return false; validEncCec.add("CEX"); } validEncCec.add("XEC"); } validEncCec.add("CEC"); } @Override public int hashCode() { protected SmsftpBinaryPduDecoder( return this.s.hashCode() + this.t.hashCode(); Function prefixToTypeFunc, } Function typeToPrefixFunc) { // CAUTION: this has no protection against nulls and problematic types super(prefixToTypeFunc, typeToPrefixFunc); } }

public static SmsftpBinaryPduDecoder getInstance() { return SmsftpBinaryPduDecoder.thisInstance; } ------Filename: private static String getSubstrAndIncrement(String body, src/main/java/com/transmisms/smsftp/protocol/SmsftpBinaryPduDecoder.java AtomicInteger pointer, int substrSize) { ------return body.substring(pointer.get(), pointer.addAndGet(substrSize)); } package com.transmisms.smsftp.protocol; @Override import com.transmisms.core.protocol.BinaryPDUDecoder; protected List decodeBody( import com.transmisms.core.protocol.PDU; PDUType pduType, String encoded) import com.transmisms.core.protocol.PDUType; throws PDUMalformedException { import com.transmisms.core.protocol.PDUMalformedException; String body = encoded.substring(1, encoded.length()-2); import com.transmisms.core.protocol.InvalidCRCException; List decodedData = new ArrayList<>(); import static com.transmisms.smsftp.protocol.SmsftpFacade.DataSizes; import static com.transmisms.smsftp.protocol.SmsftpFacade.DataStrSizes; if(pduType instanceof SmsftpPDUType) { SmsftpPDUType sPduType = (SmsftpPDUType)pduType; import com.transmisms.core.util.codec.Base85; switch(sPduType) { case SMSFTP_INIT: { import java.nio.ByteBuffer; AtomicInteger pointer = new AtomicInteger( import java.util.ArrayList; DataStrSizes.HEXSEGMENTID.getSize()); import java.util.Hashtable; import java.util.List; if(body.length() < 1) { import java.util.Map; throw new PDUMalformedException( import java.util.UUID; "SMSFTP_INIT too short", encoded); import java.util.concurrent.atomic.AtomicInteger; } import java.util.function.Function; // get Segment Id

69 int segmentId = getHexSegmentId(body, 3, encoded, "SMSFTP_INIT", true); decodedData.add(segmentId); case SMSFTP_END: case SMSFTP_END_WAIT: // get Session Id case SMSFTP_READY: { String sessionIdStr = getSubstrAndIncrement(body, if(body.length() < DataStrSizes.SESSIONID.getSize()) { pointer, DataStrSizes.SESSIONID.getSize()); throw new PDUMalformedException( UUID sessionId = base85ToUUID(sessionIdStr, encoded, pduType.toString() + "SMSFTP_INIT"); " too short to fit Session ID", encoded); // then add the extracted Session Id } decodedData.add(sessionId); String sessionIdStr = body.substring(0, DataStrSizes.SESSIONID.getSize()); if(segmentId != 0) { // encrypted connections // decode sessionId from here String excessData = body.substring(pointer.get()); UUID sessionId = base85ToUUID(sessionIdStr, encoded, decodedData.add(excessData); pduType.toString()); } // do nothing for unencrypted connections decodedData.add(sessionId); // and add to decoded data break; break; } }

case SMSFTP_META: { case SMSFTP_DATA: { AtomicInteger pointer = new AtomicInteger( // check if size is less than SEGMENTID DataStrSizes.HEXSEGMENTID.getSize()); if(body.length() < DataStrSizes.SEGMENTID.getSize()) { throw new PDUMalformedException( if(body.length() < 1) { "SMSFTP_DATA too short to fit Segment ID", throw new PDUMalformedException( encoded); "SMSFTP_META too short", encoded); } } String segmentIdStr = body.substring(0, DataStrSizes.SEGMENTID.getSize()); // get Segment Id // decode segmentId from here int segmentId = getHexSegmentId(body, 3, encoded, Integer segmentId = null; "SMSFTP_META", true); try { decodedData.add(segmentId); byte[] segmentIdBytes = Base85.decode(segmentIdStr); ByteBuffer bb = ByteBuffer.allocate(Integer.BYTES); // get Session Id for(int i = 0; String sessionIdStr = getSubstrAndIncrement(body, i < Integer.BYTES - segmentIdBytes.length; pointer, DataStrSizes.SESSIONID.getSize()); i++) { UUID sessionId = base85ToUUID(sessionIdStr, encoded, bb.put((byte)0); "SMSFTP_META"); } decodedData.add(sessionId); // flip() is required for reading if(segmentId == 1 || segmentId == 0) { bb.put(segmentIdBytes).flip(); // get CEC String cecStr = getSubstrAndIncrement(body, pointer, segmentId = bb.getInt(); DataStrSizes.CEC.getSize()); } // verify CEC catch(IOException e) { if(!(segmentId == 1 ? validEncCec : throw new PDUMalformedException( validNoencCec).contains(cecStr)) { "SMSFTP_DATA's Segment ID is corrupt", throw new PDUMalformedException( encoded); pduType.toString() + } " invalid CEC flag: " + cecStr, encoded); decodedData.add(segmentId); // and add to decoded data } // get data body from here // get Segment Count String dataBody = body.substring( String segmentCountStr = getSubstrAndIncrement(body, DataStrSizes.SEGMENTID.getSize(), body.length()); pointer, DataStrSizes.SEGMENTCOUNT.getSize()); decodedData.add(dataBody); Integer segmentCount = base85ToInteger(segmentCountStr, break; encoded, "SMSFTP_META"); }

// add values afterwards decodedData.add(cecStr); case SMSFTP_INITIATOR_IDENTITY: decodedData.add(segmentCount); case SMSFTP_RESPONDER_IDENTITY: { } AtomicInteger pointer = new AtomicInteger( DataStrSizes.HEXSEGMENTID.getSize()); // extract checksum/signature String excessDataStr = body.substring(pointer.get()); int segmentId = getHexSegmentId(body, 5, encoded, Object excessData = null; pduType.toString(), false); if(segmentId == 0) { // unencrypted connection decodedData.add(segmentId); try { byte[] excessDataBytes String excessData = body.substring(pointer.get()); = Base85.decode(excessDataStr); decodedData.add(excessData); if(excessDataBytes.length != break; DataSizes.SHA1_SUM.getSize()) { } throw new PDUMalformedException( default: { "SMSFTP_META's SHA1 checksum's size // code should NEVER reach here invalid", throw new AssertionError("Uncaught PDU Type: " + pduType); encoded); } } } excessData = excessDataBytes; } } else { catch(IOException e) { return null; throw new PDUMalformedException( } "SMSFTP_META's SHA1 checksum is corrupt", return decodedData; encoded); } } } } else { // encrypted connection excessData = excessDataStr; } // finally, add excess data ------decodedData.add(excessData); Filename: src/main/java/com/transmisms/smsftp/protocol/SmsftpFacade.java break; ------}

70 package com.transmisms.smsftp.protocol; protected void processHeadPayload(Object[] headData) { if(this.getSmsftpSession().getSessionIntent() == import com.transmisms.core.protocol.CoreProtocolFacade; SessionIntent.DATA_TRANSFER) { import com.transmisms.core.protocol.PDU; if(headData != null && headData.length >= 5 && import com.transmisms.core.protocol.PDUEncoder; headData[3] instanceof String) { import com.transmisms.core.protocol.PDUType; String encryptionMode = (String)headData[3]; import com.transmisms.core.protocol.Presenter.MessageType; if(encryptionMode.equals("enc")) { import com.transmisms.core.protocol.Presenter.PromptType; this.getSmsftpSession().setCecEncryption(true); import com.transmisms.core.protocol.Presenter.StatusType; } import com.transmisms.core.protocol.SegmentManager; else if(encryptionMode.equals("noenc")) { import com.transmisms.core.protocol.SmsService; this.getSmsftpSession().setCecEncryption(false); import com.transmisms.core.util.codec.Base85; } import com.transmisms.core.util.crypto.AES; } import com.transmisms.core.util.crypto.RSA; } import com.transmisms.smsftp.protocol.SmsftpSession.Role; else if(this.getSmsftpSession().getSessionIntent() import com.transmisms.smsftp.protocol.SmsftpSession.SessionIntent; == SessionIntent.PAIR) { import com.transmisms.smsftp.protocol.SmsftpSession.SessionState; // do nothing } import java.io.ByteArrayInputStream; else { // should never happen import java.io.ByteArrayOutputStream; } import java.nio.ByteBuffer; } import java.security.KeyPair; import java.security.PrivateKey; @Override import java.security.PublicKey; protected String[] processReqAndGenRepPayload(Object[] requestData) { import java.util.List; if(requestData != null && requestData.length >= 6) { import java.util.Map; if(requestData.length >= 6 && requestData[4] instanceof String && import java.util.concurrent.Callable; requestData[4].equals("send") && import java.util.concurrent.CountDownLatch; requestData[5] instanceof String) { // send intent import java.util.concurrent.Future; this.getSmsftpSession().setSessionIntent( import java.util.function.BiFunction; SessionIntent.DATA_TRANSFER); // store filename in the SmsftpSession object import java.io.IOException; this.getSmsftpSession().setFilename((String)requestData[5]); import java.security.spec.InvalidKeySpecException; } else if(requestData.length >= 5 && requestData[4] instanceof String && requestData[4].equals("pair")) { // pair intent public class SmsftpFacade extends CoreProtocolFacade { this.getSmsftpSession().setSessionIntent(SessionIntent.PAIR); private static final String USER_ACCEPT_STATUS_CODE = "0002"; } public final String SUBPROTOCOL_NAME = "smsftp"; else { // error } public final SmsftpSession smsftpSession; // just accept all valid connections protected final CountDownLatch processingLatch = new CountDownLatch(1); String[] retStr = { USER_ACCEPT_STATUS_CODE }; private final CountDownLatch acceptPromptLatch = new CountDownLatch(1); return retStr; } return null; } public SmsftpFacade(SmsftpSession smsftpSession, SmsService smsService, int maxPduLifetime) { @Override super(smsftpSession, protected final Runnable processPdu(PDU pduObject) { SmsftpBinaryPduDecoder.getInstance(), switch(this.smsftpSession.getSessionState()) { SmsftpTextBasedPduDecoder.getInstance(), case TERMINATING: smsService, return null; maxPduLifetime); default: { this.smsftpSession = smsftpSession; BiFunction pdlFunction = } SmsftpLookupTable.get(new PdlEntry( this.smsftpSession.getSessionState(), public SmsftpFacade(SmsftpSession smsftpSession, SmsService smsService) { pduObject.getPduType())); this(smsftpSession, smsService, -1); if(pdlFunction == null) { // not in lookup table } return null; // do nothing } else { return pdlFunction.apply(this, pduObject); public SmsftpSession getSmsftpSession() { } return this.smsftpSession; } } } } @Override public String getSubprotocolName() { @Override return this.SUBPROTOCOL_NAME; final protected void onEstablished() { } SmsftpSession sObject = this.getSmsftpSession(); // extract IV from SessionIntent.DATA_TRANSFER sessions @Override if(sObject.getSessionIntent() != null && protected String generateHeadPayload() { sObject.getSessionIntent().equals( if(this.getSmsftpSession().getSessionIntent() == SessionIntent.DATA_TRANSFER)) { SessionIntent.DATA_TRANSFER) { long msb = sObject.sessionId.getMostSignificantBits(); if(this.getSmsftpSession().getEncryptionFlag()) { // encrypted long lsb = sObject.sessionId.getLeastSignificantBits(); return "enc"; ByteBuffer buffer = ByteBuffer.allocate(Long.BYTES*2); } buffer.putLong(msb); return "noenc"; // unencrypted buffer.putLong(lsb); } sObject.iv = buffer.array(); else if(this.getSmsftpSession().getSessionIntent() } == SessionIntent.PAIR) { if(sObject.getRole() == Role.RESPONDER) { return "noenc"; // technically noenc, but not practically necessary Object[] promptArr = {MessageType.PROMPT, } PromptType.CONNECTION_REQUEST}; else { // should never happen this.setChanged(); return null; this.notifyObservers(promptArr); } } Future userConnReqWaitFuture = this.pool.submit(() -> { this.acceptPromptLatch.countDown(); @Override });

71 } else { // INITIATOR this.smsftpSession.encodedInitPdu = new String[3]; } // construct the INIT PDUs to be sent } this.smsftpSession.encodedInitPdu[0] = (new PDUEncoder(true)) .appendPrefix(SmsftpPDUType.SMSFTP_INIT, SmsftpBinaryPduDecoder.getInstance()) .appendHexSegmentId(1) // multipart ids start at 1 public void receive() { .appendSessionId(this.smsftpSession.sessionId) this.listen(); .appendString(encEncAesKeyParts[0]) } .finalizePDU(); this.smsftpSession.encodedInitPdu[1] = (new PDUEncoder(true)) public void insecureSendFile() { .appendPrefix(SmsftpPDUType.SMSFTP_INIT, this.connect(this.getSmsftpSession().connection.peerMin + SmsftpBinaryPduDecoder.getInstance()) "/send/" + this.getSmsftpSession().getFilename()); .appendHexSegmentId(2) // multipart ids start at 1 } .appendSessionId(this.smsftpSession.sessionId) .appendString(encEncAesKeyParts[1]) public void secureSendFile() { .finalizePDU(); this.connect(this.getSmsftpSession().connection.peerMin + this.smsftpSession.encodedInitPdu[2] = (new PDUEncoder(true)) "/send/" + this.getSmsftpSession().getFilename()); .appendPrefix(SmsftpPDUType.SMSFTP_INIT, } SmsftpBinaryPduDecoder.getInstance()) .appendHexSegmentId(3) // multipart ids start at 1 public void pair() { .appendSessionId(this.smsftpSession.sessionId) // register our own segment manager .appendString(encEncAesKeyParts[2]) this.registerSegMan(SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY, .finalizePDU(); this.smsftpSession.identitySegments); // finally, send the messages // set session waiting state first (similar to CONNECTED) this.smsService.sendMessage( SmsftpFacade.setStateAndNotify(this, SessionState.INITIATOR_WAIT); this.smsftpSession.encodedInitPdu[0]); // then connect as usual this.smsService.sendMessage( this.connect(this.getSmsftpSession().connection.peerMin + this.smsftpSession.encodedInitPdu[1]); "/pair"); this.smsService.sendMessage( } this.smsftpSession.encodedInitPdu[2]);

public void acceptRequestAfterPrompt(byte[] enckey, byte[] authkey) { } SmsftpFacade.logAndNotify(this, MessageType.INFO, else { // unencrypted "User accepted the smsftp request"); this.smsftpSession.encodedInitPdu = new String[1]; // construct and send the INIT PDU // continue all operations if needed this.smsftpSession.encodedInitPdu[0] = (new PDUEncoder(true)) // wake the awaiting thread .appendPrefix(SmsftpPDUType.SMSFTP_INIT, this.acceptPromptLatch.countDown(); SmsftpBinaryPduDecoder.getInstance()) .appendHexSegmentId(0) // non-multipart ids start at 0 if(this.getSmsftpSession().getSessionIntent() == .appendSessionId(this.smsftpSession.sessionId) SessionIntent.DATA_TRANSFER) { .finalizePDU(); // send INIT PDU(s) depending on encryption this.smsService.sendMessage( if(this.smsftpSession.getEncryptionFlag()) { // encrypted this.smsftpSession.encodedInitPdu[0]); // check first if the keys given are legitimate } if(enckey == null || authkey == null) { // register encodedInitPdu Array for retransmission assert false; this.registerRetrans(SmsftpPDUType.SMSFTP_INIT, } this.smsftpSession.encodedInitPdu); try { // finally, set state RSA.generatePublic(enckey); SmsftpFacade.setStateAndNotify(this, SessionState.INIT_SENT); RSA.generatePublic(authkey); } } else if(this.getSmsftpSession().getSessionIntent() catch(InvalidKeySpecException e) { == SessionIntent.PAIR) { assert false; // generate keys and encode them properly } this.smsftpSession.generatePairingIdentity(); byte[] pubEncKey = // set session's authkey this.smsftpSession.localEncKeypair.getPublic() this.getSmsftpSession().authkey = authkey; .getEncoded(); // construct AES key to be used byte[] pubAuthKey = this.getSmsftpSession().fileEncKey = AES.generateKey(); this.smsftpSession.localAuthKeypair.getPublic() // encrypt aesKey .getEncoded(); ByteArrayInputStream is = new ByteArrayInputStream( String concatenatedKeys = this.getSmsftpSession().fileEncKey); Base85.encode(pubEncKey) + "¥" + Base85.encode(pubAuthKey); ByteArrayOutputStream os = new ByteArrayOutputStream(); try { // construct and send the SMSFTP_RESPONDER_IDENTITY PDUs RSA.encrypt(enckey, is, os); int p = 0; os.flush(); for(int i = 0; i < 5; i++) { } // construct each PDU catch(IOException e) { String payload = ""; assert false; // NOTE: should never happen since we are if(p < concatenatedKeys.length()) { // just using ByteArray streams payload = concatenatedKeys.substring(p, } (p+155 < concatenatedKeys.length()) ? p+155 : String encEncAesKey = Base85.encode(os.toByteArray()); concatenatedKeys.length()); String[] encEncAesKeyParts = new String[3]; } encEncAesKeyParts[1] = ""; this.smsftpSession.encodedKeyPdu[i] = (new PDUEncoder(true)) encEncAesKeyParts[2] = ""; .appendPrefix(SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY, if(encEncAesKey.length() < 135) { SmsftpBinaryPduDecoder.getInstance()) encEncAesKeyParts[0] = encEncAesKey; .appendHexSegmentId(i+1) // multipart ids start at 1 } .appendString(payload) else if(encEncAesKey.length() >= 135 && .finalizePDU(); encEncAesKey.length() < 270) { // then send each messasge encEncAesKeyParts[0] = encEncAesKey.substring(0, 135); this.smsService.sendMessage( encEncAesKeyParts[1] = encEncAesKey.substring(135); this.smsftpSession.encodedKeyPdu[i]); } p=p+155; else { } encEncAesKeyParts[0] = encEncAesKey.substring(0, 135); SmsftpFacade.logAndNotify(this, MessageType.INFO, encEncAesKeyParts[1] = encEncAesKey.substring(135, 270); // "Sent last responder identity PDU to receiver"); 135 // register encodedKeyPdu Array for retransmission encEncAesKeyParts[2] = encEncAesKey.substring(270, this.registerRetrans(SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY, encEncAesKey.length()); this.smsftpSession.encodedKeyPdu); } // register our own segment manager

72 this.registerSegMan(SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY, @Override this.smsftpSession.identitySegments); protected Future submitToInternalPool(Runnable task) { // start a new thread for repeated sending of END PDUs return super.submitToInternalPool(task); this.sendEndPduRepeatedly(); } // finally, set state SmsftpFacade.setStateAndNotify(this, SessionState.RESPONDER_KEY_EXCHANGE); } //// utility functions else { // should never happen protected static void logAndNotify(SmsftpFacade self, MessageType t, } String message) { } Object[] logArr = {t, message}; SmsftpFacade.setChangedAndNotifyObservers(self, logArr); public void rejectRequestAfterPrompt(String reason) { } } protected static void setStateAndNotify(SmsftpFacade self, public void rejectRequestAfterPrompt() { SessionState newState) { SmsftpFacade.logAndNotify(this, MessageType.INFO, SmsftpFacade.getAndSetStateAndNotify(self, newState); "User rejected the smsftp request"); } this.rejectRequestAfterPrompt(null); } protected static SessionState getAndSetStateAndNotify(SmsftpFacade self, SessionState newState) { /** SessionState oldState = * self.smsftpSession.getAndSetSessionState(newState); * @return a reference to the internal byte[] object used for storing SmsftpFacade.setChangedAndNotifyObservers(self, MessageType.STATUS, * data to be sent/received StatusType.STATE_CHANGED, oldState, newState); */ return oldState; public byte[] getDataBytes() { } return this.smsftpSession.dataBytes; } protected static void setChangedAndNotifyObservers(CoreProtocolFacade self, Object ... o) { protected void sendEndPduRepeatedly() { CoreProtocolFacade.setChangedAndNotifyObservers(self, o); this.submitToInternalPool(() -> { } SmsftpFacade.logAndNotify(this, MessageType.INFO, } "Waiting until peer has completed"); // loop until interrupt or SmsService error while(!Thread.currentThread().isInterrupted() && !this.smsftpSession.getSessionState().equals( SessionState.TERMINATING)) { ------try { Filename: src/main/java/com/transmisms/smsftp/protocol/SmsftpLookupTable.java this.smsService.sendMessage(new PDUEncoder(true) ------.appendPrefix(SmsftpPDUType.SMSFTP_END, SmsftpBinaryPduDecoder.getInstance()) package com.transmisms.smsftp.protocol; .appendSessionId(this.smsftpSession.sessionId) .finalizePDU()); import com.transmisms.core.protocol.CoreProtocolFacade; // try again after maxPduLifetime import com.transmisms.core.protocol.PDU; Thread.sleep( import com.transmisms.core.protocol.PDUEncoder; this.coreSession.connection.maxPduLifetime); import com.transmisms.core.protocol.Presenter.MessageType; } import com.transmisms.core.protocol.Presenter.StatusType; catch(InterruptedException e) { import com.transmisms.core.protocol.SegmentManager; // preserve interrupt status import com.transmisms.smsftp.protocol.SmsftpSession.SessionState; Thread.currentThread().interrupt(); break; // then break the loop import com.transmisms.core.util.codec.Base85; } import com.transmisms.core.util.compression.XZ; } import com.transmisms.core.util.crypto.AES; }); import com.transmisms.core.util.crypto.RSA; } import com.transmisms.core.util.crypto.RSA;

/* import java.io.ByteArrayInputStream; // operations for prompt feedbacks (for UI, CLI, tester, etc.) import java.io.ByteArrayOutputStream; public void continueOperation(); import java.io.InputStream; public void cancelOperation(); // import java.io.PipedInputStream; // errcode depends on the situation import java.io.PipedOutputStream; // - no prompt: user cancel import java.security.MessageDigest; // - prompt: the real cause import java.util.Arrays; // i.e. timeout import java.util.Hashtable; // import java.util.Map; */ import java.util.concurrent.Future; import java.util.concurrent.atomic.AtomicInteger; import java.util.function.BiFunction; import org.apache.commons.io.output.CountingOutputStream; //// exposed functions for use by package and subclasses @Override import java.io.IOException; protected List registerRetrans(PDUType k, List v) { import java.security.NoSuchAlgorithmException; return super.registerRetrans(k, v); import java.util.concurrent.ExecutionException; }

@Override protected List registerRetrans(PDUType k, String[] a) { public class SmsftpLookupTable { return super.registerRetrans(k, a); /** } * Container class for use by encryptAndCompact() */ @Override private static class CompEncData { protected SegmentManager registerSegMan(PDUType k, SegmentManager v) { public boolean compressAfter = false; return super.registerSegMan(k, v); public byte[] data = null; } }

@Override protected Future submitToInternalPool(Callable task) { return super.submitToInternalPool(task); private static final Map> lookupTable = new Hashtable<>();

73 SmsftpLookupTable.createCompletedEndReceivedTask(t, u)); assert null == evalValue; static { // initialize lookupTable PdlEntry[] doNothingEntries = { BiFunction evalValue = new PdlEntry( null; // only used for asserts SessionState.SENDING_DATA, SmsftpPDUType.SMSFTP_INIT), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.SENDING_DATA, SmsftpPDUType.SMSFTP_READY), SessionState.CONNECTED, SmsftpPDUType.SMSFTP_INIT), new PdlEntry( (t, u) -> SmsftpLookupTable.createInitReceivedTask(t, u)); SessionState.COMPLETED, SmsftpPDUType.SMSFTP_INIT), assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( SessionState.COMPLETED, SmsftpPDUType.SMSFTP_META), new PdlEntry( new PdlEntry( SessionState.INIT_RECEIVED, SmsftpPDUType.SMSFTP_INIT), SessionState.COMPLETED, SmsftpPDUType.SMSFTP_READY), (t, u) -> SmsftpLookupTable.createResendMetaTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.COMPLETED, SmsftpPDUType.SMSFTP_DATA), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.COMPLETED, SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), SessionState.INIT_RECEIVED, SmsftpPDUType.SMSFTP_READY), new PdlEntry( (t, u) -> SmsftpLookupTable.createReadyReceivedTask(t, u)); SessionState.COMPLETED, SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( SessionState.COMPLETE_WAIT, SmsftpPDUType.SMSFTP_INIT), new PdlEntry( new PdlEntry( SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_META), SessionState.COMPLETE_WAIT, SmsftpPDUType.SMSFTP_META), (t, u) -> SmsftpLookupTable.createMetaReceivedTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.COMPLETE_WAIT, SmsftpPDUType.SMSFTP_READY), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.COMPLETE_WAIT, SmsftpPDUType.SMSFTP_DATA), SessionState.RECEIVING_DATA, SmsftpPDUType.SMSFTP_META), new PdlEntry( (t, u) -> SmsftpLookupTable.createResendReadyTask(t, u)); SessionState.COMPLETE_WAIT, assert null == evalValue; SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.COMPLETE_WAIT, SessionState.RECEIVING_DATA, SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), SmsftpPDUType.SMSFTP_DATA), new PdlEntry( (t, u) -> SmsftpLookupTable.createProcessDataTask(t, u)); SessionState.COMPLETE_WAIT, SmsftpPDUType.SMSFTP_END_WAIT), assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( SessionState.TERMINATING, SmsftpPDUType.SMSFTP_INIT), new PdlEntry( new PdlEntry( SessionState.RECEIVING_DATA, SmsftpPDUType.SMSFTP_END), SessionState.TERMINATING, SmsftpPDUType.SMSFTP_META), (t, u) -> SmsftpLookupTable.createSendWaitTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.TERMINATING, SmsftpPDUType.SMSFTP_READY), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.TERMINATING, SmsftpPDUType.SMSFTP_DATA), SessionState.INITIATOR_WAIT, new PdlEntry( SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), SessionState.TERMINATING, SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), (t, u) -> SmsftpLookupTable.createProcFirstRespDataTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.TERMINATING, SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.TERMINATING, SmsftpPDUType.SMSFTP_END_WAIT), SessionState.INITIATOR_WAIT, SmsftpPDUType.SMSFTP_END), }; (t, u) -> SmsftpLookupTable.createSendWaitTask(t, u)); for(PdlEntry e : doNothingEntries) { assert null == evalValue; evalValue = lookupTable.put(e, evalValue = lookupTable.put( (t, u) -> SmsftpLookupTable.createDoNothingTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.INITIATOR_KEY_EXCHANGE, } SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), (t, u) -> SmsftpLookupTable.createProcessRespDataTask(t, u)); PdlEntry[] errorEntries = { assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( SessionState.CONNECTED, SmsftpPDUType.SMSFTP_META), new PdlEntry( new PdlEntry( SessionState.INITIATOR_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_END), SessionState.CONNECTED, SmsftpPDUType.SMSFTP_READY), (t, u) -> SmsftpLookupTable.createSendWaitTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.CONNECTED, SmsftpPDUType.SMSFTP_DATA), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.CONNECTED, SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), SessionState.RESPONDER_KEY_EXCHANGE, new PdlEntry( SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), SessionState.CONNECTED, SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), (t, u) -> SmsftpLookupTable.createProcessInitDataTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.CONNECTED, SmsftpPDUType.SMSFTP_END), evalValue = lookupTable.put( new PdlEntry( new PdlEntry( SessionState.CONNECTED, SmsftpPDUType.SMSFTP_END_WAIT), SessionState.RESPONDER_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_END), new PdlEntry( (t, u) -> SmsftpLookupTable.createSendWaitTask(t, u)); SessionState.INIT_RECEIVED, SmsftpPDUType.SMSFTP_META), assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( SessionState.INIT_RECEIVED, SmsftpPDUType.SMSFTP_DATA), new PdlEntry( new PdlEntry( SessionState.COMPLETED, SmsftpPDUType.SMSFTP_END), SessionState.INIT_RECEIVED, (t, u) -> SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), SmsftpLookupTable.createCompletedEndReceivedTask(t, u)); new PdlEntry( assert null == evalValue; SessionState.INIT_RECEIVED, evalValue = lookupTable.put( SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), new PdlEntry( new PdlEntry( SessionState.COMPLETE_WAIT, SmsftpPDUType.SMSFTP_END), SessionState.INIT_RECEIVED, SmsftpPDUType.SMSFTP_END), (t, u) -> new PdlEntry( SmsftpLookupTable.createCompleteWaitEndReceivedTask(t, u)); SessionState.INIT_RECEIVED, SmsftpPDUType.SMSFTP_END_WAIT), assert null == evalValue; new PdlEntry( evalValue = lookupTable.put( SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_INIT), new PdlEntry( new PdlEntry( SessionState.TERMINATING, SmsftpPDUType.SMSFTP_END), SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_READY), (t, u) -> new PdlEntry(

74 SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_DATA), return () -> {}; new PdlEntry( } SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), new PdlEntry( public static Runnable createPDUMismatchTask( SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), SmsftpFacade tObject, PDU uObject) { new PdlEntry( return () -> {}; SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_END), } new PdlEntry( SessionState.INIT_SENT, SmsftpPDUType.SMSFTP_END_WAIT), public static Runnable createInitReceivedTask( new PdlEntry( SmsftpFacade tObject, PDU uObject) { SessionState.SENDING_DATA, SmsftpPDUType.SMSFTP_META), return () -> { tObject.submitToInternalPool(() -> { new PdlEntry( // initialize segment manager if not yet initialized SessionState.SENDING_DATA, SmsftpPDUType.SMSFTP_DATA), SmsftpSession sObject = tObject.smsftpSession; new PdlEntry( SessionState.SENDING_DATA, boolean isChanged = false; SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), if(sObject.getEncryptionFlag()) { new PdlEntry( isChanged = sObject.initSegments.compareAndSet( SessionState.SENDING_DATA, null, new SegmentManager(3)); SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), } new PdlEntry( else { SessionState.SENDING_DATA, SmsftpPDUType.SMSFTP_END), isChanged = sObject.initSegments.compareAndSet( new PdlEntry( null, new SegmentManager(1)); SessionState.SENDING_DATA, SmsftpPDUType.SMSFTP_END_WAIT), } new PdlEntry( if(isChanged) { // register new segment manager SessionState.RECEIVING_DATA, SmsftpPDUType.SMSFTP_INIT), tObject.registerSegMan( new PdlEntry( SmsftpPDUType.SMSFTP_INIT, sObject.initSegments.get()); SessionState.RECEIVING_DATA, SmsftpPDUType.SMSFTP_READY), } new PdlEntry( SessionState.RECEIVING_DATA, synchronized(sObject.initSegments) { SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), // validate uObject and extract available data new PdlEntry( Object[] uData = uObject.getData(); SessionState.RECEIVING_DATA, if(uData == null || SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), uData.length < 2 || !(uData[0] instanceof Integer)) { new PdlEntry( SmsftpFacade.logAndNotify(tObject, SessionState.RECEIVING_DATA, SmsftpPDUType.SMSFTP_END_WAIT), MessageType.TRACE, new PdlEntry( "Got problematic INIT PDU data"); SessionState.INITIATOR_WAIT, SmsftpPDUType.SMSFTP_INIT), return; // just return on invalid cases new PdlEntry( } SessionState.INITIATOR_WAIT, SmsftpPDUType.SMSFTP_META), int segnum = (Integer)(uData[0]); new PdlEntry( SmsftpFacade.logAndNotify(tObject, SessionState.INITIATOR_WAIT, SmsftpPDUType.SMSFTP_READY), MessageType.TRACE, "Received INIT part " + segnum); new PdlEntry( SessionState.INITIATOR_WAIT, SmsftpPDUType.SMSFTP_DATA), // update segment and determine if all PDUs are completed new PdlEntry( final String[][] veriArrC = { null }; SessionState.INITIATOR_WAIT, if(sObject.getEncryptionFlag()) { // if encrypted/multipart SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), // get encrypted AES segment new PdlEntry( String encAesSeg = null; SessionState.INITIATOR_WAIT, SmsftpPDUType.SMSFTP_END_WAIT), if(uData.length == 3 && uData[2] instanceof String) { new PdlEntry( encAesSeg = (String)(uData[2]); SessionState.INITIATOR_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_INIT), } new PdlEntry( else { // invalid case, just return asap SessionState.INITIATOR_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_META), SmsftpFacade.logAndNotify(tObject, new PdlEntry( MessageType.TRACE, "Erratic INIT part " + SessionState.INITIATOR_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_READY), segnum); new PdlEntry( return; SessionState.INITIATOR_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_DATA), } new PdlEntry( SessionState.INITIATOR_KEY_EXCHANGE, // update segment and check if completed SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY), if(sObject.initSegments.get().updateSegment( new PdlEntry( encAesSeg, segnum-1) && SessionState.INITIATOR_KEY_EXCHANGE, sObject.initSegments.get().getCompletedString() SmsftpPDUType.SMSFTP_END_WAIT), != null) { new PdlEntry( // set lock and exit if already locked SessionState.RESPONDER_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_INIT), if(sObject.processingStarted.getAndSet(true)) { new PdlEntry( SmsftpFacade.logAndNotify(tObject, SessionState.RESPONDER_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_META), MessageType.TRACE, new PdlEntry( "Got another INIT while processing"); SessionState.RESPONDER_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_READY), return; new PdlEntry( } SessionState.RESPONDER_KEY_EXCHANGE, SmsftpPDUType.SMSFTP_DATA), new PdlEntry( SmsftpFacade.logAndNotify(tObject, MessageType.DEBUG, SessionState.RESPONDER_KEY_EXCHANGE, "INIT PDUs complete; reassembling segments"); SmsftpPDUType.SMSFTP_RESPONDER_IDENTITY), new PdlEntry( SessionState.RESPONDER_KEY_EXCHANGE, // process INIT PDU(s) if already completed SmsftpPDUType.SMSFTP_END_WAIT), final CompEncData[] xeRetValC = { null }; new PdlEntry( final CompEncData[] ceRetValC = { null }; SessionState.COMPLETED, SmsftpPDUType.SMSFTP_END_WAIT), final byte[][] aesKeyC = { null }; }; for(PdlEntry e : errorEntries) { //// #p1 evalValue = lookupTable.put(e, Future cecFuture = tObject.submitToInternalPool( (t, u) -> SmsftpLookupTable.createPDUMismatchTask(t, u)); () -> { assert null == evalValue; // extract AES key #p1 } try { } ByteArrayOutputStream baos = new ByteArrayOutputStream(); byte[] encAesKey = Base85.decode( sObject.initSegments.get() //// "action" functions .getCompletedString()); public static Runnable createDoNothingTask( // decrypt and validate consolidated key SmsftpFacade tObject, PDU uObject) { RSA.decrypt(sObject.enckey,

75 new ByteArrayInputStream(encAesKey), }); baos); aesKeyC[0] = baos.toByteArray(); // join here and compare sizes #p1 } try { catch(IOException e) { xeFuture.get(); assert false; // should never happen ceFuture.get(); } } catch(InterruptedException e) { // log event // check cases for CEC, CEX, and XEC #p1x3 SmsftpFacade.logAndNotify(tObject, Future xeFuture = tObject.submitToInternalPool( MessageType.TRACE, () -> { "CEC processing interrupted"); SmsftpFacade.logAndNotify(tObject, // preserve interrupt status MessageType.INFO, Thread.currentThread().interrupt(); "Encrypting file..."); return; // return ASAP // encrypt file #p1.1 XEX + XEC } xeRetValC[0] = encryptAndCompact( catch(ExecutionException e) { // should never happen new ByteArrayInputStream( // log event sObject.dataBytes), aesKeyC[0], SmsftpFacade.logAndNotify(tObject, sObject.iv); MessageType.TRACE, SmsftpFacade.logAndNotify(tObject, "CEC processing exception: " + e); MessageType.INFO, "Encryption done"); return; // return ASAP }); } { // compare sizes and select the smallest one Future ceFuture = tObject.submitToInternalPool( boolean compressPrior = () -> { ceRetValC[0].data.length > SmsftpFacade.logAndNotify(tObject, xeRetValC[0].data.length; MessageType.INFO, "Compressing file (first pass)..."); sObject.cecPreCompression = compressPrior; // compress file #p1.2 CE? sObject.cecPostCompression = compressPrior ? // prepare Piped streams for use later ceRetValC[0].compressAfter : final PipedInputStream pis = xeRetValC[0].compressAfter; new PipedInputStream(); PipedOutputStream tpos = null; SmsftpFacade.logAndNotify(tObject, try { MessageType.DEBUG, tpos = new PipedOutputStream(pis); "Converted data length: " + } (compressPrior ? catch(IOException e) { ceRetValC[0].data.length : assert false; // should never happen xeRetValC[0].data.length)); } final PipedOutputStream pos = tpos; // convert to Base85 #p1 // spawn to a new thread to avoid SmsftpFacade.logAndNotify(tObject, // Piped* deadlocks MessageType.INFO, Future pipedFuture = "Encoding data..."); tObject.submitToInternalPool(() -> { String encoded = Base85.encode(compressPrior ? try { ceRetValC[0].data : xeRetValC[0].data); XZ.compress(new ByteArrayInputStream( SmsftpFacade.logAndNotify(tObject, sObject.dataBytes), pos); MessageType.INFO, "Encoding done"); } SmsftpFacade.logAndNotify(tObject, catch(IOException e) { MessageType.DEBUG, assert false; // should never happen "Encoded length: " + encoded.length()); // since we're just using // byte arrays or similar // split encoded data } int index = 0; SmsftpFacade.logAndNotify(tObject, final AtomicInteger i = new AtomicInteger(1); MessageType.INFO, while(index < encoded.length()) { "Compression (first pass) done"); String str = encoded.substring(index, SmsftpFacade.logAndNotify(tObject, Math.min(index+153, MessageType.INFO, encoded.length())); "Compressing file (second sObject.dataPdu.add(new PDUEncoder(true) pass)..."); .appendPrefix( }); SmsftpPDUType.SMSFTP_DATA, try { SmsftpBinaryPduDecoder // encrypt and try to further compress .getInstance()) // compressed file #p1.2 CEC + CEX .appendBinarySegmentId( ceRetValC[0] = encryptAndCompact(pis, i.getAndIncrement()) aesKeyC[0], sObject.iv); .appendString(str) // while processing pipedFuture .finalizePDU()); pipedFuture.get(); index+=153; } } catch(InterruptedException e) { SmsftpFacade.logAndNotify(tObject, // log event MessageType.DEBUG, SmsftpFacade.logAndNotify(tObject, "Segment count: " + MessageType.TRACE, sObject.dataPdu.size()); "CEC processing 2 interrupted"); // register dataPdu List for retransmission // preserve interrupt status tObject.registerRetrans( Thread.currentThread().interrupt(); SmsftpPDUType.SMSFTP_DATA, return; // return ASAP sObject.dataPdu); } // should never happen encoded = null; catch(ExecutionException e) { } // log event }); SmsftpFacade.logAndNotify(tObject, MessageType.TRACE, "CEC processing 2 exception: " + //// #p2 e); Future sigFuture = tObject.submitToInternalPool( return; // return ASAP () -> { // generate RSA signature #p2 } SmsftpFacade.logAndNotify(tObject, SmsftpFacade.logAndNotify(tObject, MessageType.INFO, "Signing..."); MessageType.INFO, try { "Compression (second pass) done"); byte[] signature = RSA.sign(sObject.authkey,

76 new ByteArrayInputStream( index+=153; sObject.dataBytes)); } String veriString = Base85.encode(signature); SmsftpFacade.logAndNotify(tObject, veriArrC[0] = new String[3]; MessageType.INFO, "Encoding done"); veriArrC[0][0] = veriString.substring(0, 128); SmsftpFacade.logAndNotify(tObject, veriArrC[0][1] = MessageType.DEBUG, "Segment count: " + veriString.substring(128, 263); sObject.dataPdu.size()); veriArrC[0][2] = veriString.substring(263, // register dataPdu List for retransmission veriString.length()); tObject.registerRetrans(SmsftpPDUType.SMSFTP_DATA, } sObject.dataPdu); catch(IOException e) { }); assert false; // should never happen } // compute SHA512 checksum #q2 SmsftpFacade.logAndNotify(tObject, Future hashFuture = tObject.submitToInternalPool( MessageType.INFO, "Signing done"); () -> { }); SmsftpFacade.logAndNotify(tObject, MessageType.INFO, //// join p1 and p2 "Computing checksum..."); try { veriArrC[0] = new String[1]; cecFuture.get(); MessageDigest md = null; sigFuture.get(); try { } md = MessageDigest.getInstance("SHA-1"); catch(InterruptedException e) { } // log event catch(NoSuchAlgorithmException e) { SmsftpFacade.logAndNotify(tObject, assert false; // should never happen (min JDK7) MessageType.TRACE, } "CEC processing and signing interrupted"); md.update(sObject.dataBytes); // preserve interrupt status veriArrC[0][0] = Base85.encode(md.digest()); Thread.currentThread().interrupt(); SmsftpFacade.logAndNotify(tObject, return; // return ASAP MessageType.INFO, } "Checksum computation done"); catch(ExecutionException e) { // should never happen }); // log event SmsftpFacade.logAndNotify(tObject, //// join q1 and q2 MessageType.TRACE, try { "CEC processing/signing exception: " + e); xxcFuture.get(); return; // return ASAP hashFuture.get(); } } catch(InterruptedException e) { } // log event } SmsftpFacade.logAndNotify(tObject, else { // unencrypted, single part MessageType.TRACE, // update with virtually empty contents (and can be only "XXC processing and hashing interrupted"); // executed once) // preserve interrupt status if(sObject.initSegments.get().updateSegment("", segnum)) { Thread.currentThread().interrupt(); // check cases for XXC and XXX #q1 return; // return ASAP Future xxcFuture = tObject.submitToInternalPool( } () -> { catch(ExecutionException e) { // should never happen SmsftpFacade.logAndNotify(tObject, // log event MessageType.INFO, "Compressing file..."); SmsftpFacade.logAndNotify(tObject, ByteArrayOutputStream baos = MessageType.TRACE, new ByteArrayOutputStream(); "XXC processing/hashing exception: " + e); try { // compress file return; // return ASAP XZ.compress(new ByteArrayInputStream( } sObject.dataBytes), baos); } } catch(IOException e) { } assert false; // should never happen } // if not yet complete, just do nothing and return asap // compare, set flags, and encode if(sObject.initSegments.get().getCompletedString() == null) { String encoded = null; return; if(baos.size() < sObject.dataBytes.length) { } sObject.cecPostCompression = true; encoded = Base85.encode(baos.toByteArray()); // further processing for both encrypted and unencrypted } else { // generate CEC string from session properties sObject.cecPostCompression = false; String cecStr = sObject.cecPreCompression ? "C" : "X"; encoded = Base85.encode(sObject.dataBytes); cecStr += sObject.getEncryptionFlag() ? "E" : "X"; } cecStr += sObject.cecPostCompression ? "C" : "X"; SmsftpFacade.logAndNotify(tObject, MessageType.INFO, "Compression done"); // construct and send META PDU(s) PDUEncoder encoder = new PDUEncoder(true); // split encoded data encoder.appendPrefix(SmsftpPDUType.SMSFTP_META, SmsftpFacade.logAndNotify(tObject, SmsftpBinaryPduDecoder.getInstance()); MessageType.INFO, "Encoding data..."); if(sObject.getEncryptionFlag()) { // encrypted // split encoded data encoder.appendHexSegmentId(1) int index = 0; .appendSessionId(sObject.sessionId) final AtomicInteger i = new AtomicInteger(1); .appendString(cecStr) while(index < encoded.length()) { .appendBinarySegmentId(sObject.dataPdu.size()) String str = encoded.substring(index, .appendString(veriArrC[0][0]) Math.min(index+153, .finalizePDU(); encoded.length())); sObject.encodedMetaPdu[0] = encoder.getEncoded(); sObject.dataPdu.add(new PDUEncoder(true) tObject.smsService.sendMessage(sObject.encodedMetaPdu[0]); .appendPrefix( // do the same for parts 2 and 3 SmsftpPDUType.SMSFTP_DATA, encoder = new PDUEncoder(true); // part 2 SmsftpBinaryPduDecoder encoder.appendPrefix(SmsftpPDUType.SMSFTP_META, .getInstance()) SmsftpBinaryPduDecoder.getInstance()) .appendBinarySegmentId( .appendHexSegmentId(2) i.getAndIncrement()) .appendSessionId(sObject.sessionId) .appendString(str) .appendString(veriArrC[0][1]) .finalizePDU()); .finalizePDU();

77 sObject.encodedMetaPdu[1] = encoder.getEncoded(); return () -> { tObject.smsService.sendMessage(sObject.encodedMetaPdu[1]); SmsftpSession sObject = tObject.smsftpSession; encoder = new PDUEncoder(true); // part 3 encoder.appendPrefix(SmsftpPDUType.SMSFTP_META, // initialize segment manager if not yet initialized SmsftpBinaryPduDecoder.getInstance()) boolean isChanged = false; .appendHexSegmentId(3) if(sObject.getEncryptionFlag()) { .appendSessionId(sObject.sessionId) isChanged = sObject.metaSegments.compareAndSet( .appendString(veriArrC[0][2]) null, new SegmentManager(3)); .finalizePDU(); } sObject.encodedMetaPdu[2] = encoder.getEncoded(); else { tObject.smsService.sendMessage(sObject.encodedMetaPdu[2]); isChanged = sObject.metaSegments.compareAndSet( } null, new SegmentManager(1)); else { // unencrypted } encoder.appendHexSegmentId(0) // use '0' for non-multipart if(isChanged) { // register new segment manager .appendSessionId(sObject.sessionId) tObject.registerSegMan( .appendString(cecStr) SmsftpPDUType.SMSFTP_META, sObject.metaSegments.get()); .appendBinarySegmentId(sObject.dataPdu.size()) } .appendString(veriArrC[0][0]) .finalizePDU(); synchronized(sObject.metaSegments) { // finally, send the META PDU // validate uObject and extract available data sObject.encodedMetaPdu[0] = encoder.getEncoded(); Object[] uData = uObject.getData(); tObject.smsService.sendMessage(sObject.encodedMetaPdu[0]); if(uData == null || } uData.length < 2 || !(uData[0] instanceof Integer)) { return; // just return on invalid cases // register encodedMetaPdu Array for retransmission } tObject.registerRetrans(SmsftpPDUType.SMSFTP_META, int segnum = (Integer)(uData[0]); sObject.encodedMetaPdu); SmsftpFacade.logAndNotify(tObject, MessageType.TRACE, "Received META part " + segnum); // release the processing lock here SmsftpFacade.logAndNotify(tObject, MessageType.DEBUG, // update segment and determine if all PDUs are completed "Releasing data processing latch"); if(sObject.getEncryptionFlag()) { // if encrypted/multipart tObject.processingLatch.countDown(); // get encrypted AES segment and further validation String sigSeg = null; // set state as INIT_RECEIVED afterwards if(segnum == 1 && uData.length == 5 && SmsftpFacade.setStateAndNotify( uData[4] instanceof String) { // first part tObject, SessionState.INIT_RECEIVED); if(!(uData[2] instanceof String && } ((String)uData[2]).length() == 3) || });}; !(uData[3] instanceof Integer)) { } return; // invalid, return asap } public static Runnable createResendMetaTask( sigSeg = (String)(uData[4]); SmsftpFacade tObject, PDU uObject) { } return () -> { else if((segnum == 2 || segnum == 3) && }; uData.length == 3 && } uData[2] instanceof String) { // second+ part sigSeg = (String)(uData[2]); public static Runnable createReadyReceivedTask( } SmsftpFacade tObject, PDU uObject) { else { // invalid case, just return asap return () -> { return; SmsftpSession sObject = tObject.smsftpSession; }

// set state // update segment and check if completed if(SmsftpFacade.getAndSetStateAndNotify( if(sObject.metaSegments.get().updateSegment( tObject, SessionState.SENDING_DATA) sigSeg, segnum-1) && .equals(SessionState.SENDING_DATA)) { sObject.metaSegments.get().getCompletedString() return; // and return if already set != null) { } // process META PDU(s) if already completed

// wait for the processing lock here try { // extract signature and store on session object try { sObject.signature = Base85.decode( SmsftpFacade.logAndNotify(tObject, MessageType.DEBUG, sObject.metaSegments.get() "Waiting for data to be sent to be ready..."); .getCompletedString()); tObject.processingLatch.await(); } SmsftpFacade.logAndNotify(tObject, MessageType.DEBUG, catch(IOException e) { "Data ready"); assert false; // should never happen } } catch(InterruptedException e) { } // preserve interrupt status Thread.currentThread().interrupt(); if(segnum == 1) { return; // exit ASAP // extract the session parameters } String cecStr = (String)(uData[2]); if(cecStr.charAt(0) == 'C') { SmsftpFacade.logAndNotify(tObject, MessageType.INFO, sObject.cecPreCompression = true; "Sending data to receiver..."); } sObject.dataPdu.forEach((item) -> { if(cecStr.charAt(2) == 'C') { tObject.smsService.sendMessage(item); sObject.cecPostCompression = true; }); } SmsftpFacade.logAndNotify(tObject, MessageType.INFO, // prepare the new segment manager, if not "Sent last data PDU to receiver"); // yet initialized int segcount = (Integer)(uData[3]); // set state as COMPLETE_WAIT afterwards boolean dIsChanged = SmsftpFacade.setStateAndNotify( sObject.dataSegments.compareAndSet(null, tObject, SessionState.COMPLETE_WAIT); new SegmentManager(segcount)); if(dIsChanged) { // start a new thread for repeated sending of END PDUs tObject.registerSegMan( tObject.sendEndPduRepeatedly(); SmsftpPDUType.SMSFTP_DATA, }; sObject.dataSegments.get()); } } } public static Runnable createMetaReceivedTask( SmsftpFacade tObject, PDU uObject) { }

78 else { // unencrypted, single part try { // get segment and further validation if(sObject.cecPostCompression) { // --C if(!(uData.length >= 5 && uData[4] instanceof byte[] & & SmsftpFacade.logAndNotify(tObject, MessageType.INFO, uData[2] instanceof String && "Decompressing file (second pass)..."); uData[3] instanceof Integer)) { baos = new ByteArrayOutputStream(); return; // invalid, return asap XZ.decompress(new ByteArrayInputStream(rawBytes), } baos); byte[] sigSeg = (byte[])(uData[4]); rawBytes = baos.toByteArray(); // preemptive cleanup // "update" segment and force "completion" bais = null; sObject.metaSegments.get().updateSegment("", 0); SmsftpFacade.logAndNotify(tObject, MessageType.INFO, "Decompression (second pass) done"); // extract signature and store on session object } sObject.signature = sigSeg; if(sObject.getEncryptionFlag()) { // -E- SmsftpFacade.logAndNotify(tObject, MessageType.INFO, // extract session parameters "Decrypting file..."); String cecStr = (String)(uData[2]); baos = new ByteArrayOutputStream(); if(cecStr.charAt(0) == 'C') { AES.decrypt(sObject.fileEncKey, sObject.iv, return; // invalid, return asap new ByteArrayInputStream(rawBytes), baos); } rawBytes = baos.toByteArray(); if(cecStr.charAt(1) == 'E') { // preemptive cleanup return; // invalid, return asap bais = null; } SmsftpFacade.logAndNotify(tObject, MessageType.INFO, if(cecStr.charAt(2) == 'C') { "Decryption done"); sObject.cecPostCompression = true; } } if(sObject.cecPreCompression) { // C-- SmsftpFacade.logAndNotify(tObject, MessageType.INFO, // prepare the new segment manager, if not yet initialized "Decompressing file (first pass)..."); int segCount = (Integer)(uData[3]); baos = new ByteArrayOutputStream(); boolean dIsChanged = XZ.decompress(new ByteArrayInputStream(rawBytes), sObject.dataSegments.compareAndSet(null, baos); new SegmentManager(segCount)); rawBytes = baos.toByteArray(); if(dIsChanged) { // preemptive cleanup tObject.registerSegMan( bais = null; SmsftpPDUType.SMSFTP_DATA, SmsftpFacade.logAndNotify(tObject, MessageType.INFO, sObject.dataSegments.get()); "Decompression (first pass) done"); } } } } catch(IOException e) { if(sObject.metaSegments.get().getCompletedString() != null) { assert false; // should never happen // send READY PDU } tObject.smsService.sendMessage(new PDUEncoder(true) .appendPrefix(SmsftpPDUType.SMSFTP_READY, // verify rawBytes SmsftpBinaryPduDecoder.getInstance()) SmsftpFacade.logAndNotify(tObject, MessageType.INFO, .appendSessionId(sObject.sessionId) "Verifying..."); .finalizePDU()); boolean authentic = false; // set state as RECEIVING_DATA afterwards try { SmsftpFacade.setStateAndNotify( if(sObject.getEncryptionFlag()) { // encrypted tObject, SessionState.RECEIVING_DATA); authentic = RSA.verify(sObject.authkey, } new ByteArrayInputStream(rawBytes), } sObject.signature); }; } } else { // unencrypted MessageDigest md = null; public static Runnable createResendReadyTask( try { SmsftpFacade tObject, PDU uObject) { md = MessageDigest.getInstance("SHA-1"); return () -> { } }; catch(NoSuchAlgorithmException e) { } assert false; // should never happen (min JDK7) } public static Runnable createProcessDataTask( md.update(rawBytes); SmsftpFacade tObject, PDU uObject) { byte[] checksum = md.digest(); return () -> { authentic = Arrays.equals(checksum, sObject.signature); SmsftpSession sObject = tObject.smsftpSession; } // NOTE: no need to initialize since we have done that on } // receipt of SMSFTP_META PDU catch(IOException e) { assert false; // should never happen // validate contents of PDU } Object[] uData = uObject.getData(); SmsftpFacade.logAndNotify(tObject, MessageType.INFO, if(uData == null || uData.length < 2 || "Verification done"); !(uData[0] instanceof Integer && if(authentic) { uData[1] instanceof String)) { SmsftpFacade.logAndNotify(tObject, MessageType.INFO, return; // just return on invalid cases "File complete and verified"); } // move rawBytes to SmsftpSession.dataBytes int segnum = (Integer)(uData[0]); sObject.dataBytes = rawBytes; String dataSeg = (String)(uData[1]); // notify presenters about the success Object[] noticeArr = { MessageType.STATUS, // update extracted data and process the whole file on completion StatusType.NOTICE, SessionState.COMPLETED, if(sObject.dataSegments.get().updateSegment(dataSeg, segnum-1) & & sObject.dataBytes }; sObject.dataSegments.get().getCompletedString() != null) { SmsftpFacade.setChangedAndNotifyObservers( byte[] rawBytes = null; tObject, noticeArr); try { // reply with END PDU ASAP rawBytes = Base85.decode( if(sObject.endReceived) { sObject.dataSegments.get().getCompletedString()); // for formalities sake, we'll transition to COMPLETED } SmsftpFacade.setStateAndNotify( catch(IOException e) { tObject, SessionState.COMPLETED); assert false; // should never happen // send END PDU } tObject.smsService.sendMessage(new PDUEncoder(true) ByteArrayInputStream bais = new ByteArrayInputStream(rawBytes); .appendPrefix(SmsftpPDUType.SMSFTP_END, ByteArrayOutputStream baos = null; SmsftpBinaryPduDecoder.getInstance()) .appendSessionId(

79 tObject.smsftpSession.sessionId) } .finalizePDU()); tObject.smsftpSession.encodedKeyPdu[i] = (new PDUEncoder(true)) // set state as TERMINATING afterwards .appendPrefix(SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY, SmsftpFacade.setStateAndNotify( SmsftpBinaryPduDecoder.getInstance()) tObject, SessionState.TERMINATING); .appendHexSegmentId(i+1) // multipart ids start at 1 return; // no need for other later calls .appendString(payload) } .finalizePDU(); } // then send each messasge else { // sender's file authentication failed tObject.smsService.sendMessage( SmsftpFacade.logAndNotify(tObject, MessageType.INFO, tObject.smsftpSession.encodedKeyPdu[i]); "File failed verification"); p=p+155; // notify presenters about the failure } Object[] noticeArr = { MessageType.STATUS, SmsftpFacade.logAndNotify(tObject, MessageType.INFO, StatusType.NOTICE, SessionState.COMPLETED, "Sent last initiator identity PDU to receiver"); null }; // register encodedKeyPdu Array for retransmission SmsftpFacade.setChangedAndNotifyObservers( tObject.registerRetrans(SmsftpPDUType.SMSFTP_INITIATOR_IDENTITY, tObject, noticeArr); tObject.smsftpSession.encodedKeyPdu); } }; } // finally, set session state to COMPLETED SmsftpFacade.setStateAndNotify( public static Runnable createProcessRespDataTask( tObject, SessionState.COMPLETED); SmsftpFacade tObject, PDU uObject) { } return () -> { else { // not yet complete; misc actions SmsftpLookupTable.processIdentityData(tObject, uObject, // force sending of CORE_RETRANSMISSION PDUs on SessionState.COMPLETE_WAIT); // certain percentages (optional for spec; added for ui) }; int progress = (sObject.getLastDataSegNumReceived() - } sObject.getSkippedSegNumsLength())*100 / sObject.getDataSegCount(); public static Runnable createProcessInitDataTask( if(progress - sObject.lastDataMilestone >= 20) { SmsftpFacade tObject, PDU uObject) { sObject.lastDataMilestone = progress; return () -> { tObject.sendRetransPdu(); SmsftpLookupTable.processIdentityData(tObject, uObject, } SessionState.COMPLETED); } }; } // notify presenters about arrival of new DATA PDU SmsftpFacade.setChangedAndNotifyObservers(tObject, public static Runnable createCompletedEndReceivedTask( MessageType.STATUS, StatusType.NOTICE, SmsftpFacade tObject, PDU uObject) { SessionState.RECEIVING_DATA); return () -> { }; SmsftpFacade.logAndNotify(tObject, MessageType.INFO, } "Received END request. Finalizing session..."); // send END PDU public static Runnable createSendWaitTask( tObject.smsService.sendMessage(new PDUEncoder(true) SmsftpFacade tObject, PDU uObject) { .appendPrefix(SmsftpPDUType.SMSFTP_END, return () -> { SmsftpBinaryPduDecoder.getInstance()) SmsftpFacade.logAndNotify(tObject, MessageType.INFO, .appendSessionId(tObject.smsftpSession.sessionId) "Not yet finished; requesting peer to wait longer..."); .finalizePDU()); SmsftpSession sObject = tObject.smsftpSession; // set state as TERMINATING afterwards // enable endReceived flag SmsftpFacade.setStateAndNotify( sObject.endReceived = true; tObject, SessionState.TERMINATING); { // send an END_WAIT PDU }; tObject.smsService.sendMessage(new PDUEncoder(true) } .appendPrefix(SmsftpPDUType.SMSFTP_END_WAIT, SmsftpBinaryPduDecoder.getInstance()) public static Runnable createCompleteWaitEndReceivedTask( .appendSessionId(sObject.sessionId) SmsftpFacade tObject, PDU uObject) { .finalizePDU()); return () -> { } SmsftpFacade.logAndNotify(tObject, MessageType.INFO, // and request retransmission of missing PDUs "All parties finished. Terminating session..."); tObject.sendRetransPdu(); SmsftpFacade.setStateAndNotify( }; tObject, SessionState.TERMINATING); } // then disconnect asap tObject.disconnect(CoreProtocolFacade.COMPLETED_STATUS_CODE); public static Runnable createProcFirstRespDataTask( }; SmsftpFacade tObject, PDU uObject) { } return () -> { // set to not waiting SmsftpFacade.setStateAndNotify( tObject, SessionState.INITIATOR_KEY_EXCHANGE); //// utility functions // process data received private static void processIdentityData(SmsftpFacade tObject, PDU uObject, SmsftpLookupTable.createProcessRespDataTask( SessionState completeState) { tObject, uObject).run(); SmsftpSession sObject = tObject.smsftpSession; // generate keys and encode them properly // validate contents of PDU tObject.smsftpSession.generatePairingIdentity(); Object[] uData = uObject.getData(); byte[] pubEncKey = if(uData == null || uData.length < 2 || tObject.smsftpSession.localEncKeypair.getPublic() !(uData[0] instanceof Integer && .getEncoded(); uData[1] instanceof String)) { byte[] pubAuthKey = return; // just return on invalid cases tObject.smsftpSession.localAuthKeypair.getPublic() } .getEncoded(); int segnum = (Integer)(uData[0]); String concatenatedKeys = if(segnum < 1 || segnum > 5) { Base85.encode(pubEncKey) + "¥" + Base85.encode(pubAuthKey); return; // just return on invalid cases } // then start sending SMSFTP_INITIATOR_IDENTITY PDUs String dataSeg = (String)(uData[1]); int p = 0; for(int i = 0; i < 5; i++) { // update extracted data and process whole string on completion // construct each PDU if(sObject.identitySegments.updateSegment(dataSeg, segnum-1) && String payload = ""; sObject.identitySegments.getCompletedString() != null) { if(p < concatenatedKeys.length()) { String[] splitStr = sObject.identitySegments payload = concatenatedKeys.substring(p, .getCompletedString().split("¥"); (p+155 < concatenatedKeys.length()) ? p+155 : String b85PubEncKey = splitStr[0]; concatenatedKeys.length()); String b85PubAuthKey = splitStr[1];

80 try { SMSFTP_INIT (PDUSubType.BINARY, PduSegmentIdFormat.HEX, 1), sObject.peerEnckey = Base85.decode(b85PubEncKey); SMSFTP_META (PDUSubType.BINARY, PduSegmentIdFormat.HEX, 1), sObject.peerAuthkey = Base85.decode(b85PubAuthKey); SMSFTP_READY (PDUSubType.BINARY, 0), } SMSFTP_DATA (PDUSubType.BINARY, PduSegmentIdFormat.BINARY), catch(IOException e) { SMSFTP_INITIATOR_IDENTITY (PDUSubType.BINARY, PduSegmentIdFormat.HEX), assert false; // should never happen SMSFTP_RESPONDER_IDENTITY (PDUSubType.BINARY, PduSegmentIdFormat.HEX), } SMSFTP_END (PDUSubType.BINARY, 0), // finally, set necessary session state SMSFTP_END_WAIT (PDUSubType.BINARY, 0); SmsftpFacade.setStateAndNotify( tObject, completeState); // notify presenters about completion SmsftpFacade.setChangedAndNotifyObservers( public final PDUSubType pduSubType; tObject, MessageType.STATUS, StatusType.NOTICE, completeState); public final PduSegmentIdFormat segIdFormat; // start a new thread for repeated sending of END PDUs tObject.sendEndPduRepeatedly(); public final int sessionIdPosition; } }

/** // default access control since enums are restricted * Encrypt stream using given AES key then tries compress data further, if SmsftpPDUType(PDUSubType pduSubType) { * possible this(pduSubType, PduSegmentIdFormat.NONE); * } * @return CompEncData containing details if resulting encrypted data is * compressed and the actual data SmsftpPDUType(PDUSubType pduSubType, PduSegmentIdFormat segIdFormat) { */ this(pduSubType, segIdFormat, -1); private static CompEncData encryptAndCompact(InputStream is, } byte[] aesKey, byte[] iv) { CompEncData retVal = new CompEncData(); SmsftpPDUType(PDUSubType pduSubType, int sessionIdPosition) { ByteArrayOutputStream encBaos = new ByteArrayOutputStream(); this(pduSubType, PduSegmentIdFormat.NONE, sessionIdPosition); } // encrypt bytes -E? try { SmsftpPDUType(PDUSubType pduSubType, PduSegmentIdFormat segIdFormat, AES.encrypt(aesKey, iv, is, encBaos); int sessionIdPosition) { } this.pduSubType = pduSubType; catch(IOException e) { this.segIdFormat = segIdFormat; assert false; // should never happen since we're just using this.sessionIdPosition = sessionIdPosition; // byte arrays } } byte[] encBytes = encBaos.toByteArray(); encBaos = null; // cleanup asap @Override // compress encrypted bytes -EC public SmsftpPDUType valueOf() { ByteArrayOutputStream compEncBaos = new ByteArrayOutputStream(); return valueOf(this.name()); CountingOutputStream compEncCos = } new CountingOutputStream(compEncBaos); try { @Override XZ.compress(new ByteArrayInputStream(encBytes), compEncCos); public PDUSubType getPduSubType() { } return this.pduSubType; catch(IOException e) { } assert false; // should never happen since we're just using // byte arrays @Override } public PduSegmentIdFormat getPduSegmentIdFormat() { return this.segIdFormat; // set proper flags and return values } if(compEncCos.getByteCount() < encBytes.length) { // use encryption retVal.compressAfter = true; @Override retVal.data = compEncBaos.toByteArray(); public int getSessionIdPosition() { } return this.sessionIdPosition; else { // don't use encryption } retVal.compressAfter = false; } retVal.data = encBytes; }

return retVal; } ------Filename: src/main/java/com/transmisms/smsftp/protocol/SmsftpPresenter.java ------

// convenience get() function package com.transmisms.smsftp.protocol; public static BiFunction get(PdlEntry e) { import com.transmisms.core.protocol.Connection.ConnectionState; return SmsftpLookupTable.lookupTable.get(e); import com.transmisms.core.protocol.Presenter; } import com.transmisms.core.protocol.Presenter.MessageType; } import com.transmisms.core.protocol.Presenter.PromptType; import com.transmisms.core.protocol.Presenter.StatusType; import com.transmisms.smsftp.protocol.SmsftpSession.SessionState;

import java.util.Arrays; ------import java.util.Observable; Filename: src/main/java/com/transmisms/smsftp/protocol/SmsftpPDUType.java ------package com.transmisms.smsftp.protocol; public /*abstract*/ class SmsftpPresenter extends Presenter { private final SmsftpFacade smsftpFacade; import com.transmisms.core.protocol.PDUType.PDUSubType; import com.transmisms.core.protocol.PDUType;

public SmsftpPresenter(SmsftpFacade smsftpFacade) { this.smsftpFacade = smsftpFacade; public enum SmsftpPDUType implements PDUType { }

81 this.onStatusUpdate(statusType, sState, carrier); } else if(args[2] instanceof ConnectionState) { public void receive() { ConnectionState cState = this.smsftpFacade.receive(); (ConnectionState)(args[2]); } Object[] carrier = { }; if(args.length >= 4) { public void insecureSendFile() { carrier = Arrays.copyOfRange(args, 3, this.smsftpFacade.insecureSendFile(); args.length); } } this.onStatusUpdate(statusType, cState, carrier); public void secureSendFile() { } this.smsftpFacade.secureSendFile(); else { } return; // just do nothing and return ASAP } public void pair() { break; this.smsftpFacade.pair(); } } default: { assert false; // should never happen @Override } public final void update(Observable o, Object arg) { } // check for unwanted cases break; if(arg == null || !(arg instanceof Object[])) { } return; // do nothing case FATAL: } case ERROR: Object[] args = (Object[])arg; case WARN: if(args.length <= 1 || args.length == 0 || case INFO: !(args[0] instanceof MessageType)) { case DEBUG: return; // do nothing case TRACE: { } // check if the rest of the payload is valid this.processMessage(o, args); if(!(args.length == 2 && args[1] instanceof String)) { } return; // do not log anything that is not loggable } final private void processMessage(Observable o, Object[] args) { // and get logMessage from args // args[0] is guaranteed instanceof MessageType String logMessage = (String)(args[1]); MessageType messageType = (MessageType)args[0]; // finally log message this.onLogMessage(messageType, logMessage); switch(messageType) { break; case PROMPT: { } if(!(args[1] instanceof PromptType)) { // if invalid input default: { return; // just do nothing and return ASAP return; // just do nothing and return ASAP } } PromptType promptType = (PromptType)args[1]; } String promptMessage = ""; } if(args.length >= 3 && args[2] != null && args[2] instanceof String) { protected void onLogMessage(MessageType mType, String message) {} promptMessage = (String)(args[2]); } protected void onStatusUpdate(StatusType sType, SessionState sState, this.onUserPrompt(promptType, promptMessage); Object ... args) {} break; } protected void onStatusUpdate(StatusType sType, ConnectionState oldState, case STATUS: { Object ... args) {} if(args.length < 3 || !(args[1] instanceof StatusType)) { // if invalid input protected void onUserPrompt(PromptType pType, String message) {} return; // just do nothing and return ASAP } } StatusType statusType = (StatusType)args[1]; switch(statusType) { case STATE_CHANGED: { if(args.length < 4) { ------return; // just do nothing and return ASAP Filename: src/main/java/com/transmisms/smsftp/protocol/SmsftpSession.java } ------if((args[2] instanceof SessionState) && (args[3] instanceof SessionState)) { package com.transmisms.smsftp.protocol; SessionState oldState = (SessionState)(args[2]); SessionState newState = (SessionState)(args[3]);import com.transmisms.core.protocol.Connection; this.onStatusUpdate(statusType, import com.transmisms.core.protocol.SegmentManager; oldState, newState); import com.transmisms.core.protocol.Session; } import com.transmisms.core.util.crypto.RSA; else if((args[2] instanceof ConnectionState) && (args[3] instanceof ConnectionState)) { import java.security.KeyPair; ConnectionState oldState = import java.security.PrivateKey; (ConnectionState)(args[2]); import java.security.PublicKey; ConnectionState newState = (ConnectionState)(args[3]); import java.io.ByteArrayOutputStream; this.onStatusUpdate(statusType, import java.util.ArrayList; oldState, newState); import java.util.List; } import java.util.UUID; else { import java.util.concurrent.atomic.AtomicBoolean; return; // just do nothing and return ASAP import java.util.concurrent.atomic.AtomicReference; } break; import java.io.IOException; } import java.security.spec.InvalidKeySpecException; case NOTICE: { if(args[2] instanceof SessionState) { SessionState sState = (SessionState)(args[2]); Object[] carrier = { }; public class SmsftpSession extends Session { if(args.length >= 4) { public enum Role { carrier = Arrays.copyOfRange(args, 3, INITIATOR, // SENDER args.length); RESPONDER // RECEIVER } }

82 this.cecEncryption = cecEncryption; public enum SessionState { CONNECTED, // all if(role == Role.INITIATOR) { INIT_RECEIVED, // snd // check if dataBytes is non-null INIT_SENT, // rcv if(dataBytes == null) { SENDING_DATA, // snd throw new IllegalArgumentException("dataBytes cannot be null"); RECEIVING_DATA, // rcv } INITIATOR_KEY_EXCHANGE, // kx-ini INITIATOR_WAIT, // kx-ini this.dataBytes = dataBytes; RESPONDER_KEY_EXCHANGE, // kx-rsp this.fileSize = dataBytes.length; COMPLETED, // rcv, kx-* } COMPLETE_WAIT, // snd, kx-* else { TERMINATING, // all //this.dataBytes = null; } } this.role = role; public enum SessionIntent { DATA_TRANSFER, PAIR } this.sessionIntent = SessionIntent.DATA_TRANSFER; this.filename = filename;

// if sender on encrypted session, construct necessary keys private final AtomicReference sessionStateRef = if(role == Role.INITIATOR && this.cecEncryption) { new AtomicReference<>(SessionState.CONNECTED); // construct proper keypair from enckey and authkey private SessionIntent sessionIntent = null; try { private final Role role; PrivateKey aPrivKey = RSA.generatePrivate(authkey); PrivateKey ePrivKey = RSA.generatePrivate(enckey); private String filename = null; // finally, set the keys if valid private int fileSize = -1; this.enckey = enckey; this.authkey = authkey; protected boolean cecPreCompression = false; } private boolean cecEncryption = false; catch(InvalidKeySpecException e) { protected boolean cecPostCompression = false; throw new IllegalArgumentException( "Invalid public/private key"); protected boolean endReceived = false; // for RESPONDER only } } protected byte[] iv = null; protected byte[] enckey = null; // construct buffers protected byte[] authkey = null; if(this.role == Role.INITIATOR && this.cecEncryption) { protected byte[] fileEncKey = null; this.encodedMetaPdu = new String[3]; protected byte[] signature = null; } else if(this.role == Role.INITIATOR && !this.cecEncryption) { protected byte[] dataBytes = null; this.encodedMetaPdu = new String[1]; } // only applicable to DATA_TRANSFER intent } protected String[] encodedInitPdu; // only applicable to RECEIVER protected String[] encodedMetaPdu; // only applicable to INITIATOR /** protected String encodedReadyPdu; // only applicable to RECEIVER * Constructs an SmsftpSession for pairing protected final AtomicReference initSegments = */ new AtomicReference<>(); // only applicable to INITIATOR private SmsftpSession(Connection connection, Role role) { protected final AtomicReference metaSegments = super(connection, UUID.randomUUID()); new AtomicReference<>(); // only applicable to RECEIVER this.role = role; protected final AtomicReference dataSegments = this.sessionIntent = null; new AtomicReference<>(); // only applicable to RECEIVER protected final List dataPdu = // only applicable to INITIATOR if(role == Role.INITIATOR) { new ArrayList<>(); this.sessionIntent = SessionIntent.PAIR; protected volatile int lastDataMilestone = -1; } } protected final AtomicBoolean processingStarted = new AtomicBoolean(false); /** // only applicable to PAIRING intent * Constructs an SmsftpSession for receiving public KeyPair localEncKeypair = null; */ public KeyPair localAuthKeypair = null; private SmsftpSession(Connection connection) { public byte[] peerEnckey = null; super(connection, null); public byte[] peerAuthkey = null; protected final String[] encodedKeyPdu = new String[5]; this.role = Role.RESPONDER; protected final SegmentManager identitySegments = new SegmentManager(5); this.sessionIntent = null; }

/** * Constructs an SmsftpSession for sending data /** * * * @param enckey private RSA key to be used in AES key decryption * @param enckey public RSA key to be used in AES key decryption * @param authkey private RSA key to be used in creating the auth signature * @param authkey public RSA key to be used in creating the auth signature * @throws NullPointerException * @throws NullPointerException * @throws IllegalArgumentException * @throws IllegalArgumentException */ */ private SmsftpSession(Connection connection, Role role, String filename, public static SmsftpSession generateSenderDataSession( byte[] dataBytes, byte[] enckey, byte[] authkey) Connection connection, String filename, byte[] dataBytes, throws NullPointerException, IllegalArgumentException { byte[] enckey, byte[] authkey) { super(connection, UUID.randomUUID()); return new SmsftpSession(connection, Role.INITIATOR, filename, dataBytes, enckey, authkey); boolean cecEncryption; } if(enckey == null && authkey == null) { // unencrypted cecEncryption = false; /** } * else if(enckey != null && authkey != null) { // encrypted */ cecEncryption = true; public static SmsftpSession generateInitiatorPairSession( } Connection connection) { else { // one of the keys is missing! return new SmsftpSession(connection, Role.INITIATOR); throw new IllegalArgumentException("enckey and authkey must " + } "be either both non-null or both null"); } /**

83 * } */ } public static SmsftpSession generateReceiverSession(Connection connection) { return new SmsftpSession(connection); public SessionState getSessionState() { } return this.sessionStateRef.get(); } public Role getRole() { return this.role; public void setSessionState(SessionState sessionState) { } this.getAndSetSessionState(sessionState); } public boolean getEncryptionFlag() { return this.cecEncryption; public SessionState getAndSetSessionState(SessionState sessionState) { } if(sessionState == null) { throw new NullPointerException("Session state cannot be null"); public SessionIntent getSessionIntent() { } return this.sessionIntent; return this.sessionStateRef.getAndSet(sessionState); } } } public int getDataSegCount() { return this.dataSegments.get().segCount; } public int getLastDataSegNumReceived() { ------return this.dataSegments.get().getLastSegmentReceived(); Filename: } src/main/java/com/transmisms/smsftp/protocol/SmsftpTextBasedPduDecoder.java ------public int getSkippedSegNumsLength() { return this.dataSegments.get().getSkippedSegNums().length; package com.transmisms.smsftp.protocol; } import com.transmisms.core.protocol.CorePDUType; public int getDataSize() { import com.transmisms.core.protocol.PDU; return this.dataPdu.size(); import com.transmisms.core.protocol.PDUType; } import com.transmisms.core.protocol.TextBasedPDUDecoder; import com.transmisms.core.protocol.PDUMalformedException; /* old methods below */ import com.transmisms.smsftp.protocol.SmsftpFacade; public String getFilename() { import java.util.ArrayList; return this.filename; import java.util.List; } import java.util.regex.Pattern; public void generatePairingIdentity() { ByteArrayOutputStream privBaos = new ByteArrayOutputStream(); ByteArrayOutputStream pubBaos = new ByteArrayOutputStream(); try { public class SmsftpTextBasedPduDecoder extends TextBasedPDUDecoder { // generate encryption key pair private static final Pattern FILENAME_PATTERN = RSA.generateKeys(privBaos, pubBaos); Pattern.compile("[a-zA-Z0-9. \\-#$%&'()+,i=@_]+"); privBaos.flush(); pubBaos.flush(); private static SmsftpTextBasedPduDecoder thisInstance; PrivateKey privKey = RSA.generatePrivate(privBaos.toByteArray()); static { PublicKey pubKey = RSA.generatePublic(pubBaos.toByteArray()); String[] strArray = { "smsftp" }; this.localEncKeypair = new KeyPair(pubKey, privKey); SmsftpTextBasedPduDecoder.thisInstance = new SmsftpTextBasedPduDecoder(strArray); // generate authentication key pair } privBaos = new ByteArrayOutputStream(); pubBaos = new ByteArrayOutputStream(); protected SmsftpTextBasedPduDecoder(String[] knownProtocols) { RSA.generateKeys(privBaos, pubBaos); super(knownProtocols); privBaos.flush(); } pubBaos.flush(); privKey = RSA.generatePrivate(privBaos.toByteArray()); public static SmsftpTextBasedPduDecoder getInstance() { pubKey = RSA.generatePublic(pubBaos.toByteArray()); return SmsftpTextBasedPduDecoder.thisInstance; this.localAuthKeypair = new KeyPair(pubKey, privKey); } } catch(InvalidKeySpecException e) { @Override assert false; // code should never reach here protected Object[] decodePayloadSubparts(PDUType pduType, return; String[] payloadSubparts, String encoded) } throws PDUMalformedException { catch(IOException e) { List returnData = new ArrayList(); assert false; // code should never reach here CorePDUType corePduType = (CorePDUType)pduType; return; switch(corePduType) { } case CORE_CONNECTION_REQUEST: { } if(payloadSubparts.length < 2 || payloadSubparts.length > 3) { throw new PDUMalformedException("Payload parts for " + public void setFilename(String filename) { corePduType.toString() + " too long or too short", this.filename = filename; encoded); } }

/** // get Receiver MSISDN/MIN * Does nothing if this is an INITIATOR session. String receiverMsisdnMin = payloadSubparts[0]; */ // validate msisdn/min public void setCecEncryption(boolean cecEncryption) { boolean validMsisdnMin = true; if(this.getRole() == Role.RESPONDER) { if((receiverMsisdnMin.charAt(0) != '+' && this.cecEncryption = cecEncryption; !Character.isDigit(receiverMsisdnMin.charAt(0))) || } !receiverMsisdnMin.substring(1).chars() } .allMatch(x -> Character.isDigit(x))) { throw new PDUMalformedException( /** "Invalid MSISDN/MIN for " + * Does nothing if sessionIntent is already set corePduType.toString() + ": " + receiverMsisdnMin, */ encoded); public void setSessionIntent(SessionIntent sessionIntent) { } if(this.sessionIntent == null) { returnData.add(receiverMsisdnMin); this.sessionIntent = sessionIntent; // get Intent

84 String intent = payloadSubparts[1]; } if(!intent.equals("send") && !intent.equals("pair")) { // finally, return the data throw new PDUMalformedException("Invalid Intent for " + return returnData.toArray(); corePduType.toString() + ": " + intent, encoded); } } } returnData.add(intent); // get Filename if send if(intent.equals("send")) { if(payloadSubparts.length != 3) { throw new PDUMalformedException( ------"Payload parts for " + Filename: corePduType.toString() + src/main/java/com/transmisms/smsftp/temputil/SmsftpLoggerPresenter.java " too long or too short", encoded); ------

} package com.transmisms.smsftp.temputil; String filename = payloadSubparts[2]; // validate filename import com.transmisms.core.protocol.Presenter.MessageType; if(!FILENAME_PATTERN.matcher(filename).matches()) { import com.transmisms.smsftp.protocol.SmsftpPresenter; throw new PDUMalformedException( "Invalid Filename for " + import java.util.Observable; corePduType.toString() + ": " + filename, encoded); import org.apache.logging.log4j.Logger;

} returnData.add(filename); } public class SmsftpLoggerPresenter extends SmsftpPresenter { else { private final Logger smsftpLogger; if(payloadSubparts.length != 2) { throw new PDUMalformedException( @Override "Payload parts for " + public void receive() {} // disabled corePduType.toString() + @Override " too long or too short", encoded); public void insecureSendFile() {} // disabled @Override } public void secureSendFile() {} // disabled } @Override break; public void pair() {} // disabled } case CORE_HEAD: { public SmsftpLoggerPresenter(Logger smsftpLogger) { if(payloadSubparts.length != 1) { super(null); // we don't need the actual SmsftpFacade object here throw new PDUMalformedException("Payload parts for " + this.smsftpLogger = smsftpLogger; corePduType.toString() + " too long or too short", } encoded); } @Override // get Encryption Mode protected void onLogMessage(MessageType messageType, String logMessage) { String encMode = payloadSubparts[0]; // finally, log into the respective logger if(encMode.equals("enc") || encMode.equals("noenc")) { switch(messageType) { returnData.add(encMode); case FATAL: { } this.smsftpLogger.fatal(logMessage); else { break; throw new PDUMalformedException( } "Invalid Encryption Mode: " + encMode, case ERROR: { encoded); this.smsftpLogger.error(logMessage); } break; break; } } case WARN: { case CORE_CONNECTION_RESPONSE: this.smsftpLogger.warn(logMessage); case CORE_LAST_ACK: { break; // get Status Code } String statusCode = ""; case INFO: { if(payloadSubparts.length == 1 || payloadSubparts.length == 2) { this.smsftpLogger.info(logMessage); statusCode = payloadSubparts[0]; break; returnData.add(statusCode); } } case DEBUG: { else { this.smsftpLogger.debug(logMessage); throw new PDUMalformedException("Payload parts for " + break; corePduType.toString() + " too long or too short", } encoded); case TRACE: { } this.smsftpLogger.trace(logMessage); // validate status code break; if(SmsftpFacade.getStatusMessage(statusCode) } == null) { default: { throw new PDUMalformedException( // do nothing "Invalid status code from " + break; corePduType.toString() + ": " + statusCode, } encoded); } } } }

if(payloadSubparts.length == 1) { // without status message returnData.add(null); } else if(payloadSubparts.length == 2) { // with status message------// add Status Message Filename: src/main/java/com/transmisms/ui/javafx/ConfigManager.java returnData.add(payloadSubparts[1]); ------} break; package com.transmisms.ui.javafx; } default: { import org.yaml.snakeyaml.DumperOptions; // code should NEVER reach here import org.yaml.snakeyaml.Yaml; throw new AssertionError("Uncaught PDU Type: " + corePduType);import org.apache.logging.log4j.Logger; }

85 import java.io.File; new FileInputStream(f))); import java.io.FileInputStream; } import java.io.FileWriter; catch(FileNotFoundException e) { import java.io.InputStream; } import java.util.HashMap; // refresh configRoot since we used a new config import java.util.Map; Object cfRObj = config.get(ConfigManager.ROOT_KEY); import java.util.Set; if(cfRObj == null) { configExists = false; import java.io.IOException; } import java.io.FileNotFoundException; else { configRoot = (Map)(cfRObj); } public final class ConfigManager { } public final static String ROOT_KEY = "appconfig"; else { // load from default yaml if not yet existing public final static String DOWNLOADLOC_KEY = "download-location"; // we have loaded defaultConfig as config previously public final static String RETRY_ON_ERRORS_KEY = "retry-on-errors"; // so we won't do anything here public final static String COUNTRYCODE_KEY = "country-code"; } public final static String BACKENDTYPE_KEY = "backend-type"; public final static String BACKENDSETTINGS_KEY = "backend-settings"; // fixes for some default values // fix blank download location to system-specific Downloads folder public final static String BACKEND_DUMMY_VALUE = "dummy"; if(configRoot.get(ConfigManager.DOWNLOADLOC_KEY) == null) { public final static String BACKEND_GAMMUPSQL_VALUE = "gammu-psql"; configRoot.put(ConfigManager.DOWNLOADLOC_KEY, public final static String BACKEND_ANDROID_VALUE = "android"; System.getProperty("user.home") + File.separator + "Downloads" + File.separator); public final static String BACKEND_DUMMY_KEY = "backend-dummy"; } public final static String BACKEND_GAMMUPSQL_KEY = "backend-gammu-psql"; public final static String BACKEND_ANDROID_KEY = "backend-android"; // write new config/fixes for config FileWriter fw = null; public final static String BACKEND_DUMMY_HOST_KEY = "host"; try { public final static String BACKEND_DUMMY_PORT_KEY = "port"; fw = new FileWriter(f); public final static String BACKEND_DUMMY_LOCALMIN_KEY = "local-min"; ConfigManager.yaml.dump(config, fw); public final static String BACKEND_DUMMY_ROLE_KEY = "role"; fw.flush(); public final static String BACKEND_GAMMUPSQL_HOST_KEY = "database-host"; } public final static String BACKEND_GAMMUPSQL_PORT_KEY = "database-port"; catch(IOException e) { public final static String BACKEND_GAMMUPSQL_NAME_KEY = "database-name"; e.printStackTrace(); public final static String BACKEND_GAMMUPSQL_USER_KEY = "database-user"; } public final static String BACKEND_GAMMUPSQL_PASS_KEY = finally { "database-password"; try { public final static String BACKEND_GAMMUPSQL_LOCALMIN_KEY = fw.close(); "local-min"; } public final static String BACKEND_ANDROID_HOST_KEY = "host"; catch(IOException e) { public final static String BACKEND_ANDROID_PORT_KEY = "port"; e.printStackTrace(); } private final static String CONFIG_FILE_NAME = "transmisms.yaml"; }

private static Yaml yaml; return configRoot; private static Map defaultConfig; } public static Map defaultConfigRoot; public static boolean writeConfig(Map configRoot) { static { Map config = new HashMap<>(); DumperOptions options = new DumperOptions(); config.put(ConfigManager.ROOT_KEY, new HashMap<>(configRoot)); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); ConfigManager.yaml = new Yaml(options); boolean isSuccessful = true; FileWriter fw = null; ConfigManager.defaultConfig = try { (Map)(ConfigManager.yaml.load( File f = new File(new File("conf"), ConfigManager.yaml.getClass().getClassLoader() ConfigManager.CONFIG_FILE_NAME); .getResourceAsStream("transmisms-javafx.yaml"))); fw = new FileWriter(f); ConfigManager.yaml.dump(config, fw); ConfigManager.defaultConfigRoot = fw.flush(); (Map)(ConfigManager.defaultConfig.get( } ConfigManager.ROOT_KEY)); catch(IOException e) { } isSuccessful = false; } finally { public static Map initAndReadConfig(Logger logger) { if(fw != null) { // verify config directory's and config's existence try { File dir = new File("conf"); fw.close(); boolean dirExists = !dir.mkdir(); } boolean configExists = false; catch(IOException e) { File f = new File(dir, ConfigManager.CONFIG_FILE_NAME); isSuccessful = false; try { } configExists = !f.createNewFile(); } logger.debug(configExists ? "Reading config file..." : } "Creating new config file..."); } return isSuccessful; catch(IOException e) { } logger.trace("got IOException!!!"); e.printStackTrace(); public static Map mapDiff(Map l, } Map r) { Map diff = new HashMap<>(); Map config = diff.putAll(l); new HashMap<>(ConfigManager.defaultConfig); diff.putAll(r); Map configRoot = (Map)(config.get(ConfigManager.ROOT_KEY)); diff.entrySet().removeAll(l.size() <= r.size() ? l.entrySet() : r.entrySet()); if(configExists) { // read config from file return diff; try { } config = (Map)(ConfigManager.yaml.load(

86 public static Map mapDeepCopy(Map src) { this.pairButton.setDisable(currentEntry == null); return ConfigManager.mapDeepCopy(src, new HashMap()); this.importButton.setDisable(currentEntry == null); } this.saveButton.setText((currentEntry != null) ? "Save" : "Add Contact"); public static Map mapDeepCopy(Map src, Map dest) { // bind related buttons Set entrySet = src.entrySet(); this.saveButton.disableProperty().bind( for(Map.Entry entry : entrySet) { this.contactNumber.textProperty().isEmpty()); dest.put(entry.getKey(), this.contactEditErrorText.visibleProperty().bind( (entry.getValue() instanceof Map) ? this.contactNumber.textProperty().isEmpty()); mapDeepCopy((Map)(entry.getValue())) : BooleanBinding entryIsPaired = new BooleanBinding() { entry.getValue()); { // this is an instance initializer } super.bind(ContactEditController.this.currentEntry); return dest; } } } @Override protected boolean computeValue() { return ContactEditController.this.currentEntry.containsKey( ContactManager.KEYS_KEY) && ContactEditController.this.currentEntry.get( ------ContactManager.KEYS_KEY) != null; Filename: src/main/java/com/transmisms/ui/javafx/ContactEditController.java } ------}; this.unpairButton.disableProperty().bind(entryIsPaired.not()); package com.transmisms.ui.javafx; this.exportButton.disableProperty().bind(entryIsPaired.not()); import javafx.beans.binding.Bindings; // reset fields/load current entry import javafx.beans.binding.BooleanBinding; if(currentEntry != null) { import javafx.beans.property.SimpleMapProperty; this.contactNumber.setText((String)(currentEntry.get( import javafx.collections.FXCollections; ContactManager.MSISDN_KEY))); import javafx.event.ActionEvent; this.contactName.setText((String)(currentEntry.get( import javafx.fxml.FXML; ContactManager.NAME_KEY))); import javafx.scene.control.Button; } import javafx.scene.control.Label; else { import javafx.scene.control.TextField; this.contactName.setText(""); import javafx.stage.WindowEvent; this.contactNumber.setText(""); import javafx.stage.Stage; } this.contactEditStage.showAndWait(); import java.util.HashMap; } import java.util.List; import java.util.Map; @FXML public void handleSaveButtonAction(ActionEvent event) { this.contactNumber.commitValue(); this.contactName.commitValue(); public class ContactEditController { @FXML // try to normalize contactNumber first private TextField contactNumber; this.contactNumber.setText(Utils.normalizeMsisdn( @FXML this.contactNumber.getText(), "63")); private TextField contactName; @FXML if(this.entryMap == null) { private Label contactEditErrorText; // add to contact list @FXML this.contactOList.add(this.currentEntry); private Label contactExistsErrorText; this.entryMap = this.currentEntry; @FXML } private Button saveButton; @FXML // set properties private Button pairButton; this.entryMap.put( @FXML ContactManager.MSISDN_KEY, this.contactNumber.getText()); private Button unpairButton; this.entryMap.put( @FXML ContactManager.NAME_KEY, this.contactName.getText()); private Button importButton; // and sort contact list again by name @FXML ContactManager.sortContactListByName(this.contactOList); private Button exportButton; // enable/disable controls as needed private Stage contactEditStage; this.contactNumber.setDisable(true); this.pairButton.setDisable(false); private MainWindowController primaryController = null; this.importButton.setDisable(false); this.saveButton.setText("Save"); private List> contactOList = null; private SimpleMapProperty currentEntry = null; // save on file ASAP private Map entryMap = null; ContactManager.writeConfig(ContactManager.convertToContactMap( this.contactOList)); }

public void initialize(Stage contactEditStage, @FXML MainWindowController primaryController) { public void handlePairButtonAction(ActionEvent event) { // set properties // mark proper activeContact this.contactEditStage = contactEditStage; String peer = (String)this.currentEntry.get(ContactManager.MSISDN_KEY); this.primaryController = primaryController; this.primaryController.activeContact.set(peer); this.contactOList = primaryController.contactOList; this.contactEditStage.close(); // cleanup } // then pass this to the MainWindowController this.primaryController.handleExternalPairButtonAction(event); protected void prepareAndShowStage(Map currentEntry) { } this.entryMap = currentEntry; this.currentEntry = new SimpleMapProperty<>( @FXML FXCollections.observableHashMap()); public void handleUnpairButtonAction(ActionEvent event) { if(currentEntry != null) { this.entryMap.remove(ContactManager.KEYS_KEY); this.currentEntry.putAll(currentEntry); // force sort contacts to apply changes } ContactManager.sortContactListByName(this.contactOList); // save on file ASAP // enable/disable controls as needed ContactManager.writeConfig(ContactManager.convertToContactMap( this.contactNumber.setDisable(currentEntry != null); this.contactOList));

87 } // enable/disable controls as needed catch(IOException e) { } logger.trace("got IOException!!!"); e.printStackTrace(); @FXML } public void handleOnCloseRequest(WindowEvent event) { // unbind button properties first Map config = this.unpairButton.disableProperty().unbind(); new HashMap<>(ContactManager.defaultConfig); this.exportButton.disableProperty().unbind(); Map> configRoot = null; this.saveButton.disableProperty().unbind(); this.contactEditErrorText.visibleProperty().unbind(); if(ContactManager.defaultConfigRoot != null) { this.contactExistsErrorText.visibleProperty().unbind(); configRoot = new HashMap<>(ContactManager.defaultConfigRoot); // then reset our entry markers } this.currentEntry = null; else { this.entryMap = null; configRoot = new HashMap<>(); } } } if(configExists) { // read config from file try { config = (Map)(ContactManager.yaml.load( ------new FileInputStream(f))); Filename: src/main/java/com/transmisms/ui/javafx/ContactManager.java } ------catch(FileNotFoundException e) { } package com.transmisms.ui.javafx; // refresh configRoot since we used a new config import org.yaml.snakeyaml.DumperOptions; Object cfRObj = config.get(ContactManager.ROOT_KEY); import org.yaml.snakeyaml.Yaml; if(cfRObj == null) { import org.apache.logging.log4j.Logger; configExists = false; } import java.io.File; else { import java.io.FileInputStream; configRoot = (Map>)(cfRObj); import java.io.FileWriter; } import java.io.InputStream; import java.util.ArrayList; } import java.util.Collections; else { // load from default yaml if not yet existing import java.util.HashMap; // we have loaded defaultConfig as config previously import java.util.List; // so we won't do anything here import java.util.Map; } import java.io.IOException; // write new config/fixes for contacts import java.io.FileNotFoundException; FileWriter fw = null; try { fw = new FileWriter(f); ContactManager.yaml.dump(config, fw); public final class ContactManager { fw.flush(); public final static String ROOT_KEY = "keystore"; } public final static String MSISDN_KEY = "number"; catch(IOException e) { public final static String NAME_KEY = "name"; e.printStackTrace(); public final static String KEYS_KEY = "keys"; } public final static String HOST_PUB_ENC_KEY = "host-pub-enc"; finally { public final static String HOST_PRIV_ENC_KEY = "host-priv-enc"; try { public final static String HOST_PUB_AUTH_KEY = "host-pub-auth"; fw.close(); public final static String HOST_PRIV_AUTH_KEY = "host-priv-auth"; } public final static String PEER_PUB_ENC_KEY = "peer-pub-enc"; catch(IOException e) { public final static String PEER_PUB_AUTH_KEY = "peer-pub-auth"; e.printStackTrace(); } private final static String CONFIG_FILE_NAME = "contacts.yaml"; }

private static Yaml yaml; return configRoot; private static Map defaultConfig; } public static Map> defaultConfigRoot; public static boolean writeConfig( static { Map> configRoot) { DumperOptions options = new DumperOptions(); Map config = new HashMap<>(); options.setDefaultFlowStyle(DumperOptions.FlowStyle.BLOCK); config.put(ContactManager.ROOT_KEY, new HashMap<>(configRoot)); ContactManager.yaml = new Yaml(options); boolean isSuccessful = true; ContactManager.defaultConfig = FileWriter fw = null; (Map)(ContactManager.yaml.load( try { ContactManager.yaml.getClass().getClassLoader() File f = new File(new File("conf"), .getResourceAsStream("transmisms-javafx-keystore.yaml"))); ContactManager.CONFIG_FILE_NAME); fw = new FileWriter(f); ContactManager.defaultConfigRoot = ContactManager.yaml.dump(config, fw); (Map>) fw.flush(); (ContactManager.defaultConfig.get( } ContactManager.ROOT_KEY)); catch(IOException e) { } isSuccessful = false; } public static Map> initAndReadContacts( finally { Logger logger) { if(fw != null) { // verify config directory's and config's existence try { File dir = new File("conf"); fw.close(); boolean dirExists = !dir.mkdir(); } boolean configExists = false; catch(IOException e) { File f = new File(dir, ContactManager.CONFIG_FILE_NAME); isSuccessful = false; try { } configExists = !f.createNewFile(); } logger.debug(configExists ? "Reading contacts file..." : } "Creating new contacts file...");

88 return isSuccessful; } } catch(IOException e) { System.out.println( //// utility functions here "Caught IOException when loading dialog header templates"); public static void sortContactListByName( e.printStackTrace(); List> l) { } Collections.sort(l, (m1, m2) -> { // set css for the parent return ((String)(m1.get( header.getStylesheets().add("dialog.css"); ContactManager.NAME_KEY))).compareToIgnoreCase( // set icon for dialog ((String)(m2.get(ContactManager.NAME_KEY)))); ImageView iv = (ImageView)header.lookup(".dialogIcon"); }); iv.setImage(img); } // set necessary text properties this.primaryLabel = (Label)header.lookup(".dialogPrimaryLabel"); public static List> convertToContactList( this.secondaryLabel = (Text)header.lookup(".dialogSecondaryLabel"); Map> m) { this.emphasisLabel = (Text)header.lookup(".dialogEmphasisLabel"); List> l = new ArrayList<>(); primaryLabel.setText(primaryText); m.forEach((k, v) -> { secondaryLabel.setText(secondaryText); Map n = new HashMap<>(); // add the header to dialog pane n.put(ContactManager.MSISDN_KEY, k); this.getDialogPane().setHeader(header); n.put(ContactManager.NAME_KEY, (String)(v.get(NAME_KEY))); n.put(ContactManager.KEYS_KEY, v.get(KEYS_KEY)); // misc modifications for the dialog l.add(n); this.initStyle(StageStyle.UTILITY); }); } // sort created list first ContactManager.sortContactListByName(l); public void setEmphasisText(String text) { return l; this.emphasisLabel.setText(text); } } } public static Map> convertToContactMap( List> l) { Map> m = new HashMap<>(); // loop for each element and reconstruct the map for(Map item : l) { ------Object number = item.get(ContactManager.MSISDN_KEY); Filename: src/main/java/com/transmisms/ui/javafx/Main.java Object name = item.get(ContactManager.NAME_KEY); ------

// check for validity and skip if invalid package com.transmisms.ui.javafx; if(!(name instanceof String && number instanceof String)) { continue; } import javafx.application.Application; import javafx.application.Platform; Map n = new HashMap<>(); import javafx.fxml.FXMLLoader; n.put(ContactManager.NAME_KEY, (String)name); import javafx.scene.Parent; n.put(ContactManager.KEYS_KEY, item.get(ContactManager.KEYS_KEY));import javafx.scene.Scene; import javafx.stage.Modality; m.put((String)number, n); import javafx.stage.Stage; } import javafx.stage.StageStyle; return m; import javafx.stage.WindowEvent; } } import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger;

import java.lang.reflect.Method; import java.util.Map; ------Filename: src/main/java/com/transmisms/ui/javafx/FingerprintDialog.java import java.io.IOException; ------package com.transmisms.ui.javafx; public class Main extends Application { import javafx.scene.Parent; // initialize logging import javafx.scene.control.ButtonType; private final Logger mainLogger = LogManager.getLogger("com.transmisms"); import javafx.scene.control.Dialog; private final Logger protocolLogger = import javafx.scene.control.Label; LogManager.getLogger("com.transmisms.smsftp.protocol"); import javafx.scene.image.Image; import javafx.scene.image.ImageView; import javafx.scene.text.Text; import javafx.stage.StageStyle; public class JavaFXUpdateMethodContainer { Method updateMessage; import javafx.fxml.FXMLLoader; Method updateTitle; Method updateLongProgress; import java.io.IOException; Method updateDoubleProgress;

public JavaFXUpdateMethodContainer(Method updateMessage, Method updateTitle, Method updateLongProgress, public class FingerprintDialog extends Dialog { Method updateDoubleProgress) { private final Label primaryLabel; this.updateMessage = updateMessage; private final Text secondaryLabel; this.updateTitle = updateTitle; private final Text emphasisLabel; this.updateLongProgress = updateLongProgress; this.updateDoubleProgress = updateDoubleProgress; } } public FingerprintDialog(String primaryText, String secondaryText, Image img) { public interface TransmismsLogger { // prepare and set the header public void debug(CharSequence message); FXMLLoader dialogTemplateLoader = new FXMLLoader( public void error(CharSequence message); MainWindowController.class.getClassLoader() public void fatal(CharSequence message); .getResource("dialog-fingerprint-template.fxml")); public void info(CharSequence message); Parent header = null; public void trace(CharSequence message); try { public void warn(CharSequence message); header = (Parent)(dialogTemplateLoader.load()); }

89 prefsStage.setScene(new Scene(prefsParent, 640, 520)); prefsStage.initOwner(primaryStage); prefsStage.initModality(Modality.APPLICATION_MODAL); public static void main(String[] args) { contactEditStage.setTitle("Contact Details"); Application.launch(args); contactEditStage.setScene(new Scene(contactEditParent, 480, 300)); } contactEditStage.initOwner(primaryStage); contactEditStage.initModality(Modality.APPLICATION_MODAL); primaryStage.setTitle("TransmiSMS"); primaryStage.setScene(new Scene(primaryParent, 640, 480)); // looking for public static void main()? here it is: primaryStage.show(); // finally, show the stage @Override public void start(Stage primaryStage) { this.mainLogger.trace( "Reached end of pseudo main(), JavaFX probably has taken over"); this.mainLogger.info("Starting transmisms-javafx v0.7..."); }

// load and initialize yaml files on startup Map configRoot = ConfigManager.initAndReadConfig(this.mainLogger); @Override Map> contactsRoot = public void stop() { ContactManager.initAndReadContacts(this.mainLogger); this.mainLogger.trace("Exiting application..."); } this.mainLogger.trace("Entered UI Session loop"); } // set stylesheet setUserAgentStylesheet(STYLESHEET_MODENA);

// initialize other stages Stage prefsStage = new Stage(StageStyle.UTILITY); ------Stage contactEditStage = new Stage(StageStyle.UTILITY); Filename: src/main/java/com/transmisms/ui/javafx/MainWindowController.java ------// get primaryLoader for JavaFx FXMLLoader primaryLoader = new FXMLLoader(getClass().getClassLoader().package com.transmisms.ui.javafx; getResource("fxml-main.fxml")); FXMLLoader prefsLoader = new FXMLLoader(getClass().getClassLoader().import com.transmisms.core.protocol.Connection; getResource("prefs-window.fxml")); import com.transmisms.core.protocol.PDUType; FXMLLoader contactEditLoader = import com.transmisms.core.protocol.Connection.ConnectionState; new FXMLLoader(getClass().getClassLoader(). import com.transmisms.core.util.crypto.BiometricFingerprint; getResource("contact-edit-window.fxml")); import com.transmisms.smsftp.protocol.SmsftpFacade; Parent primaryParent = null; import com.transmisms.smsftp.protocol.SmsftpPDUType; Parent prefsParent = null; import com.transmisms.smsftp.protocol.SmsftpPresenter; Parent contactEditParent = null; import com.transmisms.smsftp.protocol.SmsftpSession; try { import com.transmisms.smsftp.protocol.SmsftpSession.Role; primaryParent = primaryLoader.load(); import com.transmisms.smsftp.protocol.SmsftpSession.SessionIntent; prefsParent = prefsLoader.load(); import com.transmisms.smsftp.protocol.SmsftpSession.SessionState; contactEditParent = contactEditLoader.load(); import com.transmisms.smsftp.temputil.SmsftpLoggerPresenter; } import com.transmisms.ui.javafx.smsservice.AndroidSmsService; catch(IOException e) { import com.transmisms.ui.javafx.smsservice.ExtendedSmsService; this.mainLogger.error(e.getMessage()); import com.transmisms.ui.javafx.smsservice.ExtendedSmsService.ExtendedReason; e.printStackTrace(); import com.transmisms.ui.javafx.smsservice.DummySmsService; Platform.exit(); // exit ASAP import com.transmisms.ui.javafx.smsservice.GammuSmsService; } import org.apache.logging.log4j.Logger; // set css for the parents primaryParent.getStylesheets().add("main.css"); import javafx.application.Platform; prefsParent.getStylesheets().add("main.css"); import javafx.concurrent.Task; prefsParent.getStylesheets().add("prefs-window.css"); import javafx.event.ActionEvent; contactEditParent.getStylesheets().add("main.css"); import javafx.fxml.FXML; contactEditParent.getStylesheets().add("contact-edit-window.css");import javafx.fxml.FXMLLoader; import javafx.geometry.Side; // get controllers and initialize them import javafx.scene.Node; PrefsController prefsController = import javafx.scene.Parent; (PrefsController)prefsLoader.getController(); import javafx.scene.control.Button; ContactEditController contactEditController = import javafx.scene.control.ButtonBar.ButtonData; (ContactEditController)contactEditLoader.getController(); import javafx.scene.control.ButtonType; MainWindowController primaryController = import javafx.scene.control.ContextMenu; (MainWindowController)primaryLoader.getController(); import javafx.scene.control.Dialog; primaryController.initialize(configRoot, contactsRoot, prefsStage,import javafx.scene.control.Label; contactEditController, this.mainLogger, this.protocolLogger);import javafx.scene.control.ListCell; prefsController.initialize(configRoot, primaryController); import javafx.scene.control.ListView; contactEditController.initialize(contactEditStage, import javafx.scene.control.MenuItem; primaryController); import javafx.scene.control.ProgressBar; import javafx.scene.control.TextArea; // set close handler import javafx.scene.image.Image; primaryStage.setOnCloseRequest((e) -> { import javafx.scene.image.ImageView; mainLogger.trace("Main Window closed"); import javafx.scene.layout.HBox; primaryController.shutdown(); import javafx.scene.layout.VBox; }); import javafx.scene.text.Text; prefsStage.setOnCloseRequest((e) -> { import javafx.stage.FileChooser; prefsController.handleOnCloseRequest(e); import javafx.stage.Stage; }); import javafx.stage.StageStyle; contactEditStage.setOnCloseRequest((e) -> { contactEditController.handleOnCloseRequest(e); import javafx.beans.binding.Bindings; }); import javafx.beans.binding.BooleanBinding; import javafx.beans.property.SimpleBooleanProperty; // set static sizes import javafx.beans.property.SimpleDoubleProperty; primaryStage.setMinWidth(300); import javafx.beans.property.SimpleStringProperty; primaryStage.setMinHeight(200); import javafx.collections.FXCollections; prefsStage.setMinWidth(300); import javafx.collections.ObservableList; prefsStage.setMinHeight(200); import java.io.File; // set stage UI import java.io.FilenameFilter; prefsStage.setTitle("Preferences"); import java.nio.file.Files;

90 import java.nio.file.Paths; if(sType == StatusType.STATE_CHANGED) { import java.util.Arrays; if(args == null || args.length < 1 || import java.util.HashMap; !(args[0] instanceof SessionState)) { import java.util.List; return; // exit ASAP import java.util.Map; } import java.util.concurrent.CountDownLatch; SessionState newState = (SessionState)(args[0]); import java.util.concurrent.Executors; if(sState.equals(newState)) { import java.util.concurrent.ExecutorService; return; // exit on nothing changed condition import java.util.function.Consumer; } switch(newState) { import com.transmisms.core.protocol.SmsServiceException; case CONNECTED: { import java.io.IOException; Platform.runLater(() -> { this.mwc.connectionProgress.set(-1); import java.util.concurrent.ExecutionException; this.mwc.connectionStatus.set( "Connected to peer"); }); break; public class MainWindowController { } protected static class ContactListCell case INIT_RECEIVED: extends ListCell> { case INIT_SENT: { private Node rootGraphic; Platform.runLater(() -> private final MainWindowController mwc; this.mwc.connectionStatus.set( this.smsftpFacade public ContactListCell(MainWindowController mwc) { .smsftpSession.getEncryptionFlag() ? this.mwc = mwc; "Exchanging connection and security details" : } "Exchanging connection details")); break; @Override } public void updateItem(Map item, boolean empty) { case SENDING_DATA: { super.updateItem(item, empty); Platform.runLater(() -> if(!empty && item != null) { this.mwc.connectionStatus.set( try { "Sending data")); FXMLLoader lcLoader = new FXMLLoader(getClass() break; .getClassLoader() } .getResource("contact-listcell-template.fxml")); case RECEIVING_DATA: { lcLoader.setController(this.mwc); Platform.runLater(() -> this.rootGraphic = (VBox)(lcLoader.load()); this.mwc.connectionStatus.set( } "Receiving data")); catch(IOException e) { break; System.out.println( } "Caught IOException when loading list cell templates"); case INITIATOR_WAIT: { e.printStackTrace(); Platform.runLater(() -> } this.mwc.connectionStatus.set( "Waiting for peer to respond")); //// configure members' properties break; String msisdn = (String)(item.get(ContactManager.MSISDN_KEY)); } Label cname = (Label)(this.rootGraphic.lookup(".contactName")); case INITIATOR_KEY_EXCHANGE: cname.setText((String)(item.get(ContactManager.NAME_KEY))); case RESPONDER_KEY_EXCHANGE: { Label cnum = (Label)(this.rootGraphic.lookup(".contactNum")); Platform.runLater(() -> cnum.setText(msisdn); this.mwc.connectionStatus.set( Label s = (Label)(this.rootGraphic.lookup( "Exchanging keys with peer")); ".contactStatus")); break; ProgressBar p = (ProgressBar)(this.rootGraphic.lookup( } ".operationProgress")); case COMPLETED: { Button b = (Button)(this.rootGraphic.lookup( Platform.runLater(() -> ".cancelOperationButton")); this.mwc.connectionStatus.set( // set id bindings "Verified. Finalizing transfer")); s.idProperty().bind(Bindings.concat(msisdn, "StatusLabel")); break; p.idProperty().bind(Bindings.concat(msisdn, "ProgressBar")); } b.idProperty().bind(Bindings.concat(msisdn, "CancelButton")); case COMPLETE_WAIT: { // set other bindings Platform.runLater(() -> { s.textProperty().bind(this.mwc.connectionStatus); this.mwc.connectionProgress.set(-1); p.progressProperty().bind(this.mwc.connectionProgress); this.mwc.connectionStatus.set( "Sending data; waiting for verification"); // finally, set graphic }); this.setGraphic(this.rootGraphic); break; } } else { case TERMINATING: { // clear contents on empty Platform.runLater(() -> { this.setText(null); this.mwc.connectionProgress.set(-1); this.setGraphic(null); this.mwc.connectionStatus.set( } "Disconnecting"); } }); } break; } private static class PresenterBase extends SmsftpPresenter { default: { final private MainWindowController mwc; return; // exit ASAP final protected SmsftpFacade smsftpFacade; } private String statusCode = null; } private String statusMessage = null; } else if(sType == StatusType.NOTICE) { public PresenterBase(SmsftpFacade smsftpFacade, if(this.mwc.currentFacade.smsftpSession MainWindowController mwc) { .getSessionIntent().equals( super(smsftpFacade); SessionIntent.PAIR)) { this.mwc = mwc; if(sState.equals(SessionState.COMPLETED) || this.smsftpFacade = smsftpFacade; sState.equals(SessionState.COMPLETE_WAIT)) { } final SmsftpSession session = this.mwc.currentFacade.smsftpSession; @Override // extracted keys from session public void onStatusUpdate(StatusType sType, SessionState sState, final byte[] hostPubEnc = Object ... args) { session.localEncKeypair.getPublic().getEncoded();

91 final byte[] hostPrivEnc = ContactManager.convertToContactMap( session.localEncKeypair.getPrivate().getEncoded(); this.mwc.contactOList)); final byte[] hostPubAuth = this.mwc.refreshContactList = true; session.localAuthKeypair.getPublic().getEncoded(); }); final byte[] hostPrivAuth = } session.localAuthKeypair.getPrivate().getEncoded(); } final byte[] peerPubEnc = session.peerEnckey; } final byte[] peerPubAuth = session.peerAuthkey; }

@Override // compute fingerprint public void onStatusUpdate(StatusType sType, ConnectionState oldState, final byte[] localPubKeys = Object ... args) { new byte[hostPubEnc.length+hostPubAuth.length]; if(args == null || args.length < 1) { System.arraycopy(hostPubEnc, 0, localPubKeys, return; // exit ASAP 0, hostPubEnc.length); } System.arraycopy(hostPubAuth, 0, localPubKeys, if(sType == StatusType.STATE_CHANGED && hostPubEnc.length, hostPubAuth.length); args[0] instanceof ConnectionState) { final byte[] peerPubKeys = ConnectionState newState = (ConnectionState)(args[0]); new byte[peerPubEnc.length+peerPubAuth.length]; if(oldState == null || newState == null || System.arraycopy(peerPubEnc, 0, peerPubKeys, oldState.equals(newState)) { 0, peerPubEnc.length); return; // exit ASAP System.arraycopy(peerPubAuth, 0, peerPubKeys, } peerPubEnc.length, peerPubAuth.length); switch(newState) { final byte[] part1 = case CLOSED: { session.getRole().equals(Role.INITIATOR) ? Platform.runLater(() -> { localPubKeys : peerPubKeys; this.mwc.connectionStatus.set("Disconnected"); final byte[] part2 = }); session.getRole().equals(Role.INITIATOR) ? // then clean up afterwards peerPubKeys : localPubKeys; this.mwc.cleanupTransmission(); final byte[] combined = // force refresh by "sorting contact list" new byte[part1.length+part2.length]; Platform.runLater(() -> { System.arraycopy(part1, 0, combined, if(this.mwc.refreshContactList) { 0, part1.length); ContactManager.sortContactListByName( System.arraycopy(part2, 0, combined, this.mwc.contactOList); part1.length, part2.length); this.mwc.refreshContactList = false; final String bfp = } BiometricFingerprint.getBiometricFingerprint( }); combined); break; } // prompt user for fingerprint verification/if keys case ESTABLISHED: { // should be saved Platform.runLater(() -> Platform.runLater(() -> { this.mwc.connectionStatus.set( String aContact = this.mwc.activeContact.get(); "Connection established")); SimpleBooleanProperty saveKeys = break; new SimpleBooleanProperty(false); } this.mwc.fpConfirmation.setEmphasisText(bfp); case LISTENING: { this.mwc.fpConfirmation.showAndWait() Platform.runLater(() -> { .filter(response -> response == this.mwc.connectionProgress.set(-1); MainWindowController.dialogSavekeysButton) this.mwc.connectionStatus.set( .ifPresent(response -> { "Waiting for connection requests"); saveKeys.set(true); }); }); break; // return asap on cancel } if(!saveKeys.get()) { case REQUEST_RECEIVED: { return; Platform.runLater(() -> } this.mwc.connectionStatus.set( "Received connection request")); // get the corresponding keymap for activeContact break; Map keyMap = null; } for(Map m : this.mwc.contactOList) { case REQUEST_SENT: Object currentMinO = case REQUEST_SENT_2: m.get(ContactManager.MSISDN_KEY); case REQUEST_SENT_3: { if(currentMinO != null && Platform.runLater(() -> { currentMinO instanceof String && this.mwc.connectionProgress.set(-1); aContact.equals( this.mwc.connectionStatus.set( currentMinO)) { "Waiting for connection response"); keyMap = (Map) }); m.get(ContactManager.KEYS_KEY); break; // create keyMap if not yet existing } if(keyMap == null) { case CLOSE_WAIT: { keyMap = new HashMap<>(); Platform.runLater(() -> { m.put(ContactManager.KEYS_KEY, keyMap); this.mwc.connectionProgress.set(-1); } this.mwc.connectionStatus.set( break; // exit the loop ASAP "Disconnecting"); } // handle non-success status codes for smsftp } if(this.statusCode != null && // fill up map with the necessary keys !this.statusCode.startsWith("0")) { keyMap.put(ContactManager.HOST_PUB_ENC_KEY, // set proper text via status code and msg hostPubEnc); this.mwc.peerDisconnectNotification keyMap.put(ContactManager.HOST_PRIV_ENC_KEY, .setSecondaryText( hostPrivEnc); "Peer disconnected the connection " + keyMap.put(ContactManager.HOST_PUB_AUTH_KEY, "with the reason: " + hostPubAuth); this.statusCode + " " + keyMap.put(ContactManager.HOST_PRIV_AUTH_KEY, this.statusMessage); hostPrivAuth); this.mwc.peerDisconnectNotification.show(); keyMap.put(ContactManager.PEER_PUB_ENC_KEY, } peerPubEnc); }); keyMap.put(ContactManager.PEER_PUB_AUTH_KEY, break; peerPubAuth); } // save on file ASAP case FIN_WAIT: ContactManager.writeConfig( case CLOSING_1:

92 case CLOSING_2: { .getRole().equals(Role.RESPONDER)) { Platform.runLater(() -> { this.mwc.currentFacade this.mwc.connectionProgress.set(-1); .continueOperationAfterPrompt(); this.mwc.connectionStatus.set( return; "Requesting disconnection"); } }); // then prompt user afterwards break; this.mwc.connectionErrorNotification.showAndWait() } .ifPresent(response -> { case TIME_WAIT: { if(response == Platform.runLater(() -> { MainWindowController.dialogRetryButton) { this.mwc.connectionProgress.set(-1); this.mwc.currentFacade this.mwc.connectionStatus.set( .continueOperationAfterPrompt(); "Finalizing disconnection"); } }); else if(response == break; MainWindowController.dialogCancelButton) { } this.mwc.currentFacade default: { .cancelOperationAfterPrompt(); return; // exit ASAP } } else { // NOTE: code should never reach here } assert false; } } else if(sType == StatusType.NOTICE) { }); if(args.length >= 2 && args[0] instanceof String && }); args[1] instanceof String) { break; switch(oldState) { } case CLOSE_WAIT: { default: { // populate status code and message return; // just do nothing and return ASAP if(this.statusCode == null) { } this.statusCode = (String)args[0]; } this.statusMessage = (String)args[1]; } } break; // we won't override onLogMessage since we already delegated the task } // to the LoggerPresenters default: { } return; // exit ASAP } private static class ReceiverPresenter extends PresenterBase { } final private MainWindowController mwc; } } public ReceiverPresenter(SmsftpFacade smsftpFacade, } MainWindowController mwc) { super(smsftpFacade, mwc); @Override this.mwc = mwc; protected void onUserPrompt(PromptType pType, String message) { } switch(pType) { case CONNECTION_REQUEST: { @Override break; protected void onUserPrompt(PromptType pType, String message) { } switch(pType) { case SMS_SERVICE_ERROR: { case CONNECTION_REQUEST: { Platform.runLater(() -> { // DATA_TRANSFER-specific variables this.mwc.errorNotification boolean isEncrypted = false; .setPrimaryText(message); byte[] encKey = null; this.mwc.errorNotification byte[] authKey = null; .setSecondaryText(message); // PAIR-specific variables // set as not ready (error state) this.mwc.smsServiceIsReady.set(false); // try to get sender info from contacts map }); // temporary container for keys, if needed break; Map itemKeys = null; } final Map> tempMap = case TIMEOUT: ContactManager.convertToContactMap( case PEER_ERROR_EXCEEDED_LIMIT: { this.mwc.contactOList); Platform.runLater(() -> { // extract contact, if existing from the contacts map if(pType.equals(PromptType.TIMEOUT)) { final String peer = Utils.normalizeMsisdn( this.mwc.connectionErrorNotification this.mwc.smsService.getPeer(), .setPrimaryText("Connection Timed Out"); this.mwc.configRoot.get( this.mwc.connectionErrorNotification ConfigManager.COUNTRYCODE_KEY).toString()); .setSecondaryText( final Map tempItem = tempMap.get(peer); "Peer has not responded for a while. " + final boolean contactExists = (tempItem != null); "Do you want to retry connecting?"); if(contactExists) { } // check if keys are available else if(pType.equals( itemKeys = (Map)(tempItem.get( PromptType.PEER_ERROR_EXCEEDED_LIMIT)) { ContactManager.KEYS_KEY)); this.mwc.connectionErrorNotification } .setPrimaryText("Error Limit Exceeded"); final boolean paired = (itemKeys != null && this.mwc.connectionErrorNotification itemKeys.containsKey( .setSecondaryText( ContactManager.PEER_PUB_ENC_KEY) && "The application has noticed erratic " + itemKeys.containsKey( "behavior from peer. This might be an " + ContactManager.PEER_PUB_AUTH_KEY)); "indication of a problematic network " + "or a tampered connection. " + // check intent and do appropriate actions needed "Do you still want to retry connecting?"); if(this.smsftpFacade.smsftpSession } .getSessionIntent().equals( else { SessionIntent.DATA_TRANSFER)) { assert false; // this should never happen isEncrypted = this.smsftpFacade return; // exit ASAP .smsftpSession.getEncryptionFlag(); } // do some sanity checks and autoreject on failure if(isEncrypted && !paired) { // don't prompt if just listening; just retry } if(this.mwc.currentFacade.smsftpSession .getSessionState().equals( // then set the proper keys, if any SessionState.CONNECTED) && encKey = (isEncrypted && paired) ? this.mwc.currentFacade.smsftpSession itemKeys.get(

93 ContactManager.PEER_PUB_ENC_KEY) : "Unknown", names)); null; n.put(ContactManager.KEYS_KEY, null); authKey = (isEncrypted && paired) ? // add to map itemKeys.get( this.mwc.contactOList.add(n); ContactManager.PEER_PUB_AUTH_KEY) : // then save changes to file null; ContactManager.writeConfig(ContactManager } .convertToContactMap( else if(this.smsftpFacade.smsftpSession this.mwc.contactOList)); .getSessionIntent().equals( } SessionIntent.PAIR)) { this.mwc.isReceiving.set(true); // do nothing; not applicable this.mwc.activeContact.set(peer); } else { // show connection controls later after assert false; // should never happen // initialization of other components throw new IllegalStateException( Platform.runLater(() -> "Invalid sessionIntent on request: " + this.mwc.showActiveConnectionControls() this.smsftpFacade.smsftpSession ); .getSessionIntent()); } // finally, accept the request if(this.smsftpFacade.smsftpSession // final variables to comply with lambda restrictions .getSessionIntent().equals( final boolean fIsEncrypted = isEncrypted; SessionIntent.DATA_TRANSFER)) { final byte[] fEncKey = encKey; this.smsftpFacade.acceptRequestAfterPrompt( final byte[] fAuthKey = authKey; fEncKey, fAuthKey); } Platform.runLater(() -> { // run on EDT else { // customize acceptConfirmation before displaying this.smsftpFacade.acceptRequestAfterPrompt( // customize with contact null, null); this.mwc.acceptConfirmation.setSecondaryText( } (contactExists ? } (String)(tempItem.get( else if(response == ContactManager.NAME_KEY)) : MainWindowController.dialogRejectButton) { peer)); // stop facade asap // customize according to intent this.mwc.currentFacade.disconnect("1003"); if(this.smsftpFacade.smsftpSession } .getSessionIntent().equals( else { // NOTE: code should never reach here SessionIntent.DATA_TRANSFER)) { assert false; this.mwc.acceptConfirmation.setPrimaryText( return; "Accept file transfer request?"); } this.mwc.acceptConfirmation.appendSecondaryText( }); " is trying to send you a file named \"" + }); break; this.smsftpFacade.smsftpSession.getFilename() + } "\". " + case TIMEOUT: (this.smsftpFacade case SMS_SERVICE_ERROR: .smsftpSession.getEncryptionFlag() ? case PEER_ERROR_EXCEEDED_LIMIT: "This will be transmitted over a " + default: { "secure connection. " : super.onUserPrompt(pType, message); "This will be transmitted over an" + } " UNSECURE connection. ") + } "Do you want to accept the request?"); } } else { // SessionIntent.PAIR @Override this.mwc.acceptConfirmation.setPrimaryText( public void onStatusUpdate(StatusType sType, SessionState sState, "Accept pairing request?"); Object ... args) { this.mwc.acceptConfirmation.appendSecondaryText( // check if event is still valid (paired ? // also prevents 'dead' connections from sending status updates " is trying to renew their pairing " + if(this.mwc.currentFacade == null || "with you. Please make sure to verify " + !this.mwc.currentFacade.smsftpSession.connection "their identity and their intent to " + .getConnectionState().equals( "redo the pairing procedure. " : ConnectionState.ESTABLISHED)) { " is trying to pair with you. " + return; "This method is not as secure as " + } "manually exchanging keys off-air, " + "however. ") + // apply supermethod first "Do you want to accept the request?"); super.onStatusUpdate(sType, sState, args); } // apply our own changes, overriding previous changes, if any if(sType == StatusType.NOTICE) { // finally show acceptConfirmation dialog // for receiving DATA this.mwc.acceptConfirmation.showAndWait() if(sState.equals(SessionState.COMPLETED) && .ifPresent(response -> { this.mwc.currentFacade.smsftpSession.getSessionIntent() // if user accepted the request .equals(SessionIntent.DATA_TRANSFER)) { if(response == if(args != null && args.length >= 1) { MainWindowController.dialogAcceptButton) { if(args[0] == null) { // failure // auto add contact if user accepts request Platform.runLater(() -> if(!contactExists && !fIsEncrypted) { this.mwc.ftpFailureNotification.show()); // add 'unknown' contact first } Map n = else if(args[0] instanceof byte[]) { // success new HashMap<>(); try { n.put(ContactManager.MSISDN_KEY, peer); String downloadPath = // add a 'unique' "Unknown" entry (String)this.mwc.configRoot.get( String[] names = new String[ ConfigManager.DOWNLOADLOC_KEY); this.mwc.contactOList.size()]; Files.write( for(int i = 0; i < names.length; i++) { Paths.get(downloadPath + names[i] = (String) File.separator + (this.mwc.contactOList // returnUnique for automatic renaming .get(i).get( // of duplicates ContactManager.NAME_KEY)); MainWindowController.returnUnique( } this.mwc.currentFacade n.put(ContactManager.NAME_KEY, .smsftpSession.getFilename(), MainWindowController.returnUnique( new File(downloadPath))),

94 (byte[])args[0]); switch(newState) { } case TERMINATING: { catch(IOException e) { if(this.mwc.currentFacade.smsftpSession } .getSessionIntent().equals( // finally, alert user SessionIntent.DATA_TRANSFER)) { Platform.runLater(() -> Platform.runLater(() -> this.mwc.ftpRecvSuccessNotification.show()); this.mwc.ftpSentSuccessNotification.show()); } break; } } } } else if(sState.equals(SessionState.RECEIVING_DATA)) { default: { // no need to check args, we'll compute progress here // do nothing } // get skipped segnums, max, and current } double segCount = this.mwc.currentFacade.smsftpSession } .getDataSegCount(); } double lastSeg = this.mwc.currentFacade.smsftpSession .getLastDataSegNumReceived(); @Override double missingNos = this.mwc.currentFacade.smsftpSession public void onStatusUpdate(StatusType sType, ConnectionState oldState, .getSkippedSegNumsLength(); Object ... args) { // lastSeg+2 to accomodate 0-based array and possibly super.onStatusUpdate(sType, oldState, args); // non-executed last call if(sType == StatusType.NOTICE) { double progress = (lastSeg+2 - missingNos)/segCount; SessionState sState = this.mwc.currentFacade.smsftpSession.getSessionState(); // compare old value to new value to prevent if(sState.equals(SessionState.SENDING_DATA) || // unnecessary updates sState.equals(SessionState.COMPLETE_WAIT)) { double curProgress = this.mwc.connectionProgress.get(); // extract segment for retransmission and if(progress != curProgress) { // update lastSegmentReq when necessary Platform.runLater(() -> { PDUType t; this.mwc.connectionProgress.set(progress); int n; // override cases where > 100 percent misses double missingNos = 0; // we'll leave this as zero for now int progressPercent = (int)(progress*100); if(args.length == 2 && args[0] instanceof PDUType && progressPercent = (progressPercent > 100) ? args[1] instanceof Integer && 100 : progressPercent; args[0] != null && args[1] != null) { // set status with percentage t = (PDUType)(args[0]); this.mwc.connectionStatus.set( n = (Integer)(args[1]); "Receiving data - " + n = (n > this.lastSegmentReq) ? progressPercent + "%"); n : this.lastSegmentReq; }); this.lastSegmentReq = n; } } } else { } return; // exit ASAP on failure else if(sType == StatusType.STATE_CHANGED) { } } if(t.equals(SmsftpPDUType.SMSFTP_DATA)) { } // compute estimated progress, etc. double segCount = this.mwc.currentFacade @Override .smsftpSession.getDataSize(); public void onStatusUpdate(StatusType sType, ConnectionState oldState, // this is just an estimate anyway Object ... args) { final double progress = (n - missingNos)/segCount; super.onStatusUpdate(sType, oldState, args); // then modify related ui components } Platform.runLater(() -> { } this.mwc.connectionProgress.set(progress); // override cases where > 100 percent misses private static class SenderPresenter extends PresenterBase { int progressPercent = (int)(progress*100); final private MainWindowController mwc; progressPercent = (progressPercent > 100) ? private int lastSegmentReq = -1; 100 : progressPercent; // set status with percentage public SenderPresenter(SmsftpFacade smsftpFacade, this.mwc.connectionStatus.set( MainWindowController mwc) { "Sending data - " + super(smsftpFacade, mwc); progressPercent + "%"); this.mwc = mwc; }); } } } @Override } protected void onUserPrompt(PromptType pType, String message) { } switch(pType) { } case CONNECTION_REQUEST: { return; // do nothing; this should never happen } case TIMEOUT: // toolbar buttons case SMS_SERVICE_ERROR: @FXML case PEER_ERROR_EXCEEDED_LIMIT: private Button addContactButton; default: { @FXML super.onUserPrompt(pType, message); private Button editContactButton; } @FXML } private Button removeContactButton; } @FXML private Button sendFileButton; @Override @FXML public void onStatusUpdate(StatusType sType, SessionState sState, private Button sendFileInsecurelyButton; Object ... args) { @FXML super.onStatusUpdate(sType, sState, args); private Button appMenuButton; if(sType == StatusType.STATE_CHANGED) { // infobars if(args == null || args.length < 1 || @FXML !(args[0] instanceof SessionState)) { private Parent acceptConnInfobar; return; // exit ASAP @FXML } private Button toggleAcceptButton; SessionState newState = (SessionState)(args[0]); @FXML if(sState.equals(newState)) { private Parent errorInfobar; return; // exit on nothing changed condition // panes } @FXML

95 private HBox welcomePane; this.prefsStage = prefsStage; @FXML this.contactEditController = contactEditController; private ListView contactList; this.mainLogger = mainLogger; // misc defined properties this.protocolLogger = protocolLogger; @FXML private String startRcvLblStr; // set properties for contactList @FXML this.contactOList.addAll( private String stopRcvLblStr; ContactManager.convertToContactList(contactsRoot)); @FXML this.contactList.setItems(this.contactOList); private String startRcvBtnStr; this.contactList.setCellFactory((lview) -> { @FXML ContactListCell lcell = private String stopRcvBtnStr; new ContactListCell(this); // log-related controls return lcell; @FXML }); private Button showHideButton; @FXML // set bindings private TextArea logTarget; this.acceptConnInfobar.visibleProperty().bind( this.smsServiceIsReady.and(this.activeContact.isNull())); final private ContextMenu appMenuCM = new ContextMenu(); this.acceptConnInfobar.managedProperty().bind( private Stage prefsStage; this.smsServiceIsReady.and(this.activeContact.isNull())); private ContactEditController contactEditController; this.errorInfobar.visibleProperty().bind( private StandardDialog removeContactConfirmation = null; this.smsServiceIsReady.not()); private StandardDialog peerDisconnectNotification = null; this.errorInfobar.managedProperty().bind( private StandardDialog connectionErrorNotification = null; this.smsServiceIsReady.not()); private StandardDialog errorNotification = null; private StandardDialog acceptConfirmation = null; this.welcomePane.visibleProperty().bind( private StandardDialog disconnectConfirmation = null; Bindings.isEmpty(contactOList)); private StandardDialog ftpRecvSuccessNotification = null; this.contactList.visibleProperty().bind( private StandardDialog ftpSentSuccessNotification = null; Bindings.isNotEmpty(contactOList)); private StandardDialog ftpFailureNotification = null; private FingerprintDialog fpConfirmation = null; ObservableList> selectedItems = private static final ButtonType dialogHideButton = this.contactList.getSelectionModel().getSelectedItems(); new ButtonType("Hide", ButtonData.LEFT); BooleanBinding noContactSelection = Bindings.isEmpty(selectedItems); private static final ButtonType dialogCancelButton = BooleanBinding currentItemNotActiveContact = new ButtonType("Disconnect", ButtonData.NO); new BooleanBinding() { private static final ButtonType dialogRetryButton = { // this is an instance initializer new ButtonType("Retry", ButtonData.YES); super.bind(selectedItems, private static final ButtonType dialogDeleteButton = MainWindowController.this.activeContact); new ButtonType("Delete", ButtonData.OK_DONE); } private static final ButtonType dialogAcceptButton = new ButtonType("Accept", ButtonData.YES); @Override private static final ButtonType dialogRejectButton = protected boolean computeValue() { new ButtonType("Reject", ButtonData.NO); // check for empty cases first private static final ButtonType dialogSavekeysButton = return selectedItems.isEmpty() || new ButtonType("Save keys", ButtonData.YES); (MainWindowController.this.activeContact.get() private static final ButtonType dialogDisconnectButton = != null && new ButtonType("Disconnect", ButtonData.OK_DONE); // then check if selectedItem == activeContact private final FileChooser sendFileChooser = new FileChooser(); MainWindowController.this.activeContact.get() .equals(selectedItems.get(0).get( final private SimpleDoubleProperty connectionProgress = ContactManager.MSISDN_KEY))); new SimpleDoubleProperty(-1); } final private SimpleStringProperty connectionStatus = }; new SimpleStringProperty(""); this.editContactButton.disableProperty().bind( final private SimpleBooleanProperty isReceiving = noContactSelection.or(currentItemNotActiveContact)); new SimpleBooleanProperty(false); this.removeContactButton.disableProperty().bind( final private SimpleBooleanProperty isAccepting = noContactSelection.or(currentItemNotActiveContact)); new SimpleBooleanProperty(true); this.sendFileInsecurelyButton.disableProperty().bind( final private SimpleBooleanProperty isPostponingAccepting = noContactSelection.or( new SimpleBooleanProperty(false); this.activeContact.isNotNull().or( final private SimpleBooleanProperty smsServiceIsReady = this.smsServiceIsReady.not().or( new SimpleBooleanProperty(false); Bindings.isNotNull(this.activeContact))))); this.sendFileButton.disableProperty().bind( protected Map configRoot; noContactSelection.or( @FXML this.activeContact.isNotNull().or( protected ObservableList> contactOList; this.smsServiceIsReady.not().or( final protected SimpleStringProperty activeContact = Bindings.isNotNull(this.activeContact)).or( new SimpleStringProperty(null); new BooleanBinding() { { // this is an instance initializer private ExtendedSmsService smsService = null; super.bind(selectedItems); private String localMin = null; } private SmsftpFacade currentFacade = null; @Override private volatile CountDownLatch acCDL = null; protected boolean computeValue() { private volatile boolean refreshContactList = false; return selectedItems.isEmpty() || (null == selectedItems.get(0).get( private ExecutorService pool = Executors.newCachedThreadPool(); ContactManager.KEYS_KEY)); private Logger mainLogger; } private Logger protocolLogger; }))));

//// initialize Dialogs this.peerDisconnectNotification = /** new StandardDialog( * Initializes the controller for objects that are needed from "Peer Disconnected", * external sources as well as the properties of its members "", // populated with custom status code and messages from * // CLOSE_WAIT NOTICE */ new Image( public void initialize(Map configRoot, this.getClass().getClassLoader().getResourceAsStream( Map> contactsRoot, "icons/dialog-information.png")) Stage prefsStage, ContactEditController contactEditController, ); Logger mainLogger, Logger protocolLogger) { this.errorNotification = this.configRoot = ConfigManager.mapDeepCopy(configRoot); new StandardDialog(

96 // NOTE: this is populated with custom messages from errors this.removeContactConfirmation.getDialogPane().getButtonTypes() // thrown by SmsService .add(ButtonType.CANCEL); "", this.acceptConfirmation.getDialogPane().getButtonTypes() "", .add(MainWindowController.dialogAcceptButton); new Image( this.acceptConfirmation.getDialogPane().getButtonTypes() this.getClass().getClassLoader().getResourceAsStream( .add(MainWindowController.dialogRejectButton); "icons/dialog-error.png")) this.disconnectConfirmation.getDialogPane().getButtonTypes() ); .add(MainWindowController.dialogDisconnectButton); this.connectionErrorNotification = this.disconnectConfirmation.getDialogPane().getButtonTypes() new StandardDialog( .add(ButtonType.CANCEL); // NOTE: this is populated with custom messages from errors this.ftpRecvSuccessNotification.getDialogPane().getButtonTypes() // thrown by SmsftpFacades .add(ButtonType.OK); "", this.ftpSentSuccessNotification.getDialogPane().getButtonTypes() "", .add(ButtonType.OK); new Image( this.ftpFailureNotification.getDialogPane().getButtonTypes() this.getClass().getClassLoader().getResourceAsStream( .add(ButtonType.OK); "icons/dialog-error.png")) this.fpConfirmation.getDialogPane().getButtonTypes() ); .add(MainWindowController.dialogSavekeysButton); this.removeContactConfirmation = new StandardDialog( this.fpConfirmation.getDialogPane().getButtonTypes() "Delete this contact?", .add(ButtonType.CANCEL); "Deleting this contact will also delete the associated " + "pairing keys, if any. This action cannot be undone.", // initialize FileChoosers new Image( this.sendFileChooser.setTitle("Select file to be sent"); this.getClass().getClassLoader().getResourceAsStream( "icons/dialog-warning.png")) ); // task for tailing TextArea logTarget this.acceptConfirmation = new StandardDialog( this.pool.submit(new Task() { "Accept file transfer request?", @Override "", // NOTE: text here is generated dynamically protected Void call() throws Exception { new Image( boolean logLoop = true; this.getClass().getClassLoader().getResourceAsStream( while(logLoop) { "icons/dialog-information.png")) String logText = ""; ); try { // try to retrieve log text this.disconnectConfirmation = new StandardDialog( logText = QueueAppender.pop(); "Stop file transfer?", } "You will lose all your progress for this session and the " + catch(InterruptedException e) { "connection will be terminated.", logLoop = false; // stop looping new Image( // and greedily consume all remaining messages this.getClass().getClassLoader().getResourceAsStream( StringBuffer buff = new StringBuffer(logText); "icons/dialog-warning.png")) String message = QueueAppender.poll(); ); while(message != null) { this.ftpRecvSuccessNotification = new StandardDialog( buff.append(message); "File has been successfully received", message = QueueAppender.poll(); "The file transfer has been completed and the received " + } "data has been verified.", logText = buff.toString(); new Image( } this.getClass().getClassLoader().getResourceAsStream( finally { "icons/dialog-information.png")) String logTextCopy = new String(logText); ); logText = ""; // try to flush for sudden interruptions this.ftpSentSuccessNotification = new StandardDialog( Platform.runLater(() -> { // append and scroll to bottom "File has been successfully sent", logTarget.appendText(logTextCopy); "The receiver has successfuly received the file and " + logTarget.setScrollTop(Double.MAX_VALUE); "verified the data.", }); new Image( } this.getClass().getClassLoader().getResourceAsStream( } "icons/dialog-information.png")) return null; // for the sake that we return a "Void" ); } this.ftpFailureNotification = new StandardDialog( }); "File transfer failed", "The file transfer failed verification. This could " + // initialize ContextMenu for AppMenu "be caused by a faulty network connection " + MenuItem preferencesItem = new MenuItem("Preferences…"); "or a possible MITM attack from a third-party. Please " + MenuItem aboutItem = new MenuItem("About"); "check your connection and try again.", preferencesItem.setOnAction((e) -> { new Image( MainWindowController.this.prefsStage.show(); this.getClass().getClassLoader().getResourceAsStream( }); "icons/dialog-warning.png")) aboutItem.setOnAction((e) -> { ); }); this.fpConfirmation = new FingerprintDialog( this.appMenuCM.getItems().add(preferencesItem); "Key exchange verification", this.appMenuCM.getItems().add(aboutItem); "Use this to further verify that the keys exchanged " + "are identical. This step is optional.\n\n" + // initialize current SmsService "Make sure that the words shown on the peer matches " + this.initializeSmsService(); "the ones below:\n\n\n", // start accepting connections new Image( if(this.smsServiceIsReady.get()) { this.getClass().getClassLoader().getResourceAsStream( this.startAcceptingConnections(); "icons/dialog-password.png"))); } // add buttons to dialogs } this.peerDisconnectNotification.getDialogPane().getButtonTypes() .add(ButtonType.OK); /** this.errorNotification.getDialogPane().getButtonTypes() * Shuts down sms service, and executor service .add(MainWindowController.dialogHideButton); */ this.errorNotification.getDialogPane().getButtonTypes() public void shutdown() { .add(MainWindowController.dialogCancelButton); // try to stop existing and any attempt to accept new connections this.errorNotification.getDialogPane().getButtonTypes() if(this.isReceiving.get() || this.isAccepting.get()) { .add(MainWindowController.dialogRetryButton); this.isReceiving.set(false); this.connectionErrorNotification.getDialogPane().getButtonTypes() this.isAccepting.set(false); .add(MainWindowController.dialogCancelButton); this.stopAcceptingConnections(); this.connectionErrorNotification.getDialogPane().getButtonTypes() } .add(MainWindowController.dialogRetryButton); // try to stop running smsServices this.removeContactConfirmation.getDialogPane().getButtonTypes() if(this.smsService != null) { .add(MainWindowController.dialogDeleteButton); this.smsService.stop();

97 } this.pool.submit(new Task() { // finally stop all other threads @Override this.pool.shutdownNow(); protected Void call() { } MainWindowController.this.currentFacade.insecureSendFile(); return null; // for the sake that we return a "Void" @FXML } protected void handleSendFileButton(ActionEvent event) { }); final List> selectedItems = }); this.contactList.getSelectionModel().getSelectedItems(); }

this.handleSendAction(event, (sendFile) -> { private void handleSendAction(ActionEvent event, Consumer c) { String recvMsisdn = Utils.getIntFormat(this.activeContact.get(), File sendFile = this.sendFileChooser.showOpenDialog( this.configRoot.get( ((Node)event.getTarget()).getScene().getWindow()); ConfigManager.COUNTRYCODE_KEY).toString()); List> selectedItems = byte[] dataBytes = null; this.contactList.getSelectionModel().getSelectedItems(); try { dataBytes = Files.readAllBytes(sendFile.toPath()); if(sendFile != null && !selectedItems.isEmpty()) { } Map item = selectedItems.get(0); catch(IOException e) { if(item.get(ContactManager.MSISDN_KEY) == null) { // this covers SecurityException, OutOfMemoryError, and return; // IOException } e.printStackTrace(); } this.refreshServiceAndFacade(() -> { if(this.acCDL != null) { // initialize core objects try { Connection sConn = Connection.createInitiatorConnection( this.acCDL.await(); MainWindowController.this.localMin, recvMsisdn); this.acCDL = null; SmsftpSession sSession = } SmsftpSession.generateSenderDataSession( catch(InterruptedException e) { sConn, sendFile.getName(), dataBytes, return; ((Map)(selectedItems.get(0) } .get(ContactManager.KEYS_KEY))) } .get(ContactManager.HOST_PRIV_ENC_KEY), // setup necessary UI elements ((Map)(selectedItems.get(0) MainWindowController.this.activeContact.set( .get(ContactManager.KEYS_KEY))) (String)(item.get(ContactManager.MSISDN_KEY))); .get(ContactManager.HOST_PRIV_AUTH_KEY)); MainWindowController.this.showActiveConnectionControls(); this.currentFacade = new SmsftpFacade(sSession, this.smsService); MainWindowController.this.smsService.setPeer( // add presenters Utils.getIntFormat( this.currentFacade.addObserver( MainWindowController.this.activeContact.get(), new SenderPresenter(this.currentFacade, this)); MainWindowController.this.configRoot.get( this.currentFacade.addObserver( ConfigManager.COUNTRYCODE_KEY).toString())); new SmsftpLoggerPresenter(this.protocolLogger)); c.accept(sendFile); }); // start the transmission } this.pool.submit(new Task() { } @Override protected Void call() { @FXML MainWindowController.this.currentFacade.secureSendFile(); protected void handleCancelButtonAction(ActionEvent event) { return null; // for the sake that we return a "Void" // prompt for user when cancelling first } this.disconnectConfirmation.showAndWait() }); .filter(response -> response == }); MainWindowController.dialogDisconnectButton) } .ifPresent(response -> { this.currentFacade.disconnect("1002"); // stop facade asap @FXML }); protected void handleSendFileInsecurelyButton(ActionEvent event) { } // copied from handleSendFileButton final List> selectedItems = @FXML this.contactList.getSelectionModel().getSelectedItems(); protected void handleToggleAcceptButtonAction(ActionEvent event) { // disable toggleAcceptButton first this.handleSendAction(event, (sendFile) -> { this.toggleAcceptButton.setDisable(true); String recvMsisdn = Utils.getIntFormat(this.activeContact.get(), if(this.isAccepting.get()) { // true -> false this.configRoot.get( // stop accepting connections first ConfigManager.COUNTRYCODE_KEY).toString()); this.stopAcceptingConnections(); byte[] dataBytes = null; this.isAccepting.set(false); try { if(!this.isAccepting.get()) { // change if only successful dataBytes = Files.readAllBytes(sendFile.toPath()); this.acceptConnInfobar.getStyleClass().remove("infoInfobar"); } ((Label)(this.acceptConnInfobar.lookup( catch(IOException e) { ".infobarLabel"))).setText(this.startRcvLblStr); // this covers SecurityException, OutOfMemoryError, and this.toggleAcceptButton.setText(this.startRcvBtnStr); // IOException } e.printStackTrace(); } } else { // false -> true this.isAccepting.set(this.startAcceptingConnections()); // initialize core objects if(this.isAccepting.get()) { // change if only successful Connection sConn = Connection.createInitiatorConnection( this.acceptConnInfobar.getStyleClass().add("infoInfobar"); MainWindowController.this.localMin, recvMsisdn); ((Label)(this.acceptConnInfobar.lookup( SmsftpSession sSession = ".infobarLabel"))).setText(this.stopRcvLblStr); SmsftpSession.generateSenderDataSession( this.toggleAcceptButton.setText(this.stopRcvBtnStr); sConn, sendFile.getName(), dataBytes, } null, null); } this.currentFacade = new SmsftpFacade(sSession, // finally, reenable toggleAcceptButton this.smsService); this.toggleAcceptButton.setDisable(false); // add presenters } this.currentFacade.addObserver( new SenderPresenter(this.currentFacade, this)); @FXML this.currentFacade.addObserver( protected void handleShowErrorDetailsButtonAction(ActionEvent event) { new SmsftpLoggerPresenter(this.protocolLogger)); this.errorNotification.showAndWait().filter( response -> response == MainWindowController.dialogRetryButton) // start the transmission // or dialogCancelButton or dialogHideButton

98 .ifPresent(response -> { @FXML }); protected void handleAppMenuButtonAction(ActionEvent event) { } this.appMenuCM.show(appMenuButton, Side.LEFT, appMenuButton.getWidth(), 0); @FXML } protected void handleAddContactButtonAction(ActionEvent event) { this.contactEditController.prepareAndShowStage(null); @FXML } protected void handleShowHideButtonAction(ActionEvent event) { if(logTarget.isVisible()) { @FXML logTarget.getMinHeight(); protected void handleEditContactButtonAction(ActionEvent event) { logTarget.setMinHeight(-10000); // we assume that selectedItems is always not empty logTarget.setMaxHeight(-10000); List> selectedItems = showHideButton.setText("Show logs"); this.contactList.getSelectionModel().getSelectedItems(); } else { this.contactEditController.prepareAndShowStage(selectedItems.get(0)); logTarget.setMinHeight(100); } logTarget.setMaxHeight(Long.MAX_VALUE); showHideButton.setText("Hide logs"); @FXML } protected void handleRemoveContactButtonAction(ActionEvent event) { logTarget.setVisible(!logTarget.isVisible()); List> selectedItems = } this.contactList.getSelectionModel().getSelectedItems(); if(!selectedItems.isEmpty()) { this.removeContactConfirmation.showAndWait() .filter(response -> response == //// utility functions MainWindowController.dialogDeleteButton) /** .ifPresent(response -> { * Initializes or reinitializes this controller's SmsService. This also // remove contact from list * stops the existing SmsService, if any, and starts the newly initialized this.contactOList.remove(selectedItems.get(0)); * service. // save on file ASAP */ ContactManager.writeConfig( protected void initializeSmsService() { ContactManager.convertToContactMap( // stop current SmsService first this.contactOList)); if(this.smsService != null) { }); this.smsService.stop(); } } } // determine which type of SmsService should be initialized protected void handleExternalPairButtonAction(ActionEvent event) { String backendType = (String)(this.configRoot.get( // store activeContact temporarily to prevent resets caused ConfigManager.BACKENDTYPE_KEY)); // by stopAcceptingConnections() Map backendConfigRoot = String tempAC = this.activeContact.get(); (Map)((Map) String recvMsisdn = Utils.getIntFormat(this.activeContact.get(), this.configRoot.get(ConfigManager.BACKENDSETTINGS_KEY)).get( this.configRoot.get( "backend-" + backendType); ConfigManager.COUNTRYCODE_KEY).toString()); switch(backendType) { // initialize corresponding SmsService this.refreshServiceAndFacade(() -> { case ConfigManager.BACKEND_DUMMY_VALUE: { if(this.acCDL != null) { String host = (String)(backendConfigRoot.get( try { ConfigManager.BACKEND_DUMMY_HOST_KEY)); this.acCDL.await(); int port = (Integer)backendConfigRoot.get( this.acCDL = null; ConfigManager.BACKEND_DUMMY_PORT_KEY); } this.localMin = (String)(backendConfigRoot.get( catch(InterruptedException e) { ConfigManager.BACKEND_DUMMY_LOCALMIN_KEY)); return; boolean isSender = "sender".equals(backendConfigRoot.get( } ConfigManager.BACKEND_DUMMY_ROLE_KEY)); } this.smsService = new DummySmsService(host, port, localMin, isSender); // restore activeContact value we have stored earlier break; this.activeContact.set(tempAC); } MainWindowController.this.smsService.setPeer(recvMsisdn); case ConfigManager.BACKEND_GAMMUPSQL_VALUE: { String host = (String)(backendConfigRoot.get( // initialize core objects ConfigManager.BACKEND_GAMMUPSQL_HOST_KEY)); Connection sConn = Connection.createInitiatorConnection( int port = (Integer)backendConfigRoot.get( MainWindowController.this.localMin, recvMsisdn); ConfigManager.BACKEND_GAMMUPSQL_PORT_KEY); SmsftpSession sSession = String name = (String)(backendConfigRoot.get( SmsftpSession.generateInitiatorPairSession(sConn); ConfigManager.BACKEND_GAMMUPSQL_NAME_KEY)); MainWindowController.this.currentFacade = String user = (String)(backendConfigRoot.get( new SmsftpFacade(sSession, ConfigManager.BACKEND_GAMMUPSQL_USER_KEY)); MainWindowController.this.smsService); String pw = (String)(backendConfigRoot.get( MainWindowController.this.currentFacade.addObserver( ConfigManager.BACKEND_GAMMUPSQL_PASS_KEY)); new SenderPresenter( this.localMin = (String)(backendConfigRoot.get( MainWindowController.this.currentFacade, ConfigManager.BACKEND_GAMMUPSQL_LOCALMIN_KEY)); MainWindowController.this)); this.smsService = new GammuSmsService( MainWindowController.this.currentFacade.addObserver( host, port, name, user, pw, this.localMin); new SmsftpLoggerPresenter( break; MainWindowController.this.protocolLogger)); } case ConfigManager.BACKEND_ANDROID_VALUE: { // show connection controls later after initialization of String host = (String)(backendConfigRoot.get( // other components ConfigManager.BACKEND_ANDROID_HOST_KEY)); MainWindowController.this.showActiveConnectionControls(); int port = (Integer)backendConfigRoot.get( ConfigManager.BACKEND_ANDROID_PORT_KEY); // start pairing this.smsService = new AndroidSmsService(host, port); MainWindowController.this.pool.submit(new Task() { break; @Override } protected Void call() { default: MainWindowController.this.currentFacade.pair(); assert false; return null; // for the sake that we return a "Void" return; // do nothing and return ASAP } } }); // set country code first }); this.smsService.setCountryCode( } this.configRoot.get(ConfigManager.COUNTRYCODE_KEY).toString()); // finally, start SmsService

99 try { } this.smsServiceIsReady.set(this.smsService.start()); } // set localMin via service if not yet configured on // this app's' preferences protected synchronized void hideActiveConnectionControls() { if(this.localMin == null) { if(this.activeContact.get() != null) { this.localMin = this.smsService.getLocalMin(); String prefix = "#" + this.activeContact.get(); } this.contactList.lookup(prefix+"StatusLabel").setVisible(false); } this.contactList.lookup(prefix+"ProgressBar").setVisible(false); catch(SmsServiceException e) { this.contactList.lookup(prefix+"CancelButton").setVisible(false); // er must always be type of ExtendedReason } ExtendedReason er = (ExtendedReason)e.getReason(); } // update error dialog box with details from the exception's // reason's properties protected void cleanupTransmission() { this.errorNotification.setPrimaryText(er.getMessage()); if(this.currentFacade != null) { this.errorNotification.setSecondaryText(er.getDetails()); this.currentFacade = null; // "destroy" currentFacade this.smsServiceIsReady.set(false); // set as not ready this.hideActiveConnectionControls(); // finally, some UI cleanups } this.activeContact.set(null); // reset activeContact to null } if(this.smsService != null) { // reset peer to null, if applicable this.smsService.setPeer(null); protected boolean startAcceptingConnections() { } if(!(this.smsServiceIsReady.get()) || currentFacade != null) { return false; if(this.acCDL != null) { } this.acCDL.countDown(); } // initialize smsftp connection and session objects Connection recvConn = // listen again if set to accept connections automatically Connection.createResponderConnection(this.localMin); if(this.isPostponingAccepting.get()) { SmsftpSession recvSession = // reset flag for use next time SmsftpSession.generateReceiverSession(recvConn); this.isPostponingAccepting.set(false); this.currentFacade = new SmsftpFacade(recvSession, } this.smsService); else { // if not set to postpone accepting // add presenters if(this.isAccepting.get()) { this.currentFacade.addObserver( this.mainLogger.info("Listening for connections again"); new ReceiverPresenter(this.currentFacade, this)); this.startAcceptingConnections(); this.currentFacade.addObserver( } new SmsftpLoggerPresenter(this.protocolLogger)); } } // run the receive method in the background } this.pool.submit(new Task() { @Override protected Void call() { public static String returnUnique(String u, String ... a) { MainWindowController.this.currentFacade.receive(); if(a == null) { return null; // for the sake that we return a "Void" return u; } } }); for(String s : a) { if(s.equals(u)) { // found a conflict return true; String[] parts = s.split(" "); } if(parts.length >= 2) { // check if last part is numeric, and update i if needed protected boolean stopAcceptingConnections() { try { if(this.currentFacade != null) { int j = Integer.parseInt(parts[parts.length-1]); // NOTE: 1002 - Cancelled by user // then make i the bigger number this.currentFacade.disconnect("1002"); // stop facade asap parts[parts.length-1] = Integer.toString(j+1); return true; // finally just return true return String.join(" ", parts); } } return false; // return false on further attempts catch(NumberFormatException e) {} } } // just recurse appending " 2" private void refreshServiceAndFacade(Runnable newAction) { return MainWindowController.returnUnique(u+" 2", a); // preemptive checks } if(!(this.currentFacade == null ^ this.isAccepting.get())) { } return; return u; } } if(this.smsService != null) { this.acCDL = new CountDownLatch(1); public static String returnUnique(String u, File dir) { // stop accepting connections and interrupt any incomplete // we always assume dir is non-null // receiving procedures in progress, if any String conflicts[] = dir.list(new FilenameFilter() { if(this.smsService.isRunning()) { @Override if(this.isAccepting.get()) { public boolean accept(File dir, String name) { this.stopAcceptingConnections(); if(u.equals(name)) { this.isPostponingAccepting.set(true); return true; } } } return false; else { } } }); newAction.run(); // run the new action (i.e. send file, pair) if(conflicts.length >= 1) { } // split filename into its extension and non-extension parts else { String v = u; return; // we cannot do anything with broken sms services String[] parts = u.split("\\."); } if(parts.length >= 2) { } v = String.join(".", Arrays.copyOf(parts, parts.length-1)); protected synchronized void showActiveConnectionControls() { } if(this.activeContact.get() != null) { // try to append 2, 3, etc. and recurse again String prefix = "#" + this.activeContact.get(); return returnUnique((returnUnique(v, v)) + (v.equals(u) ? this.contactList.lookup(prefix+"StatusLabel") "" : "."+parts[parts.length-1]), dir); .setVisible(true); } this.contactList.lookup(prefix+"ProgressBar") else { .setVisible(true); return u; this.contactList.lookup(prefix+"CancelButton") } .setVisible(true); }

100 } if(str.length() == 0) { // needed to accomodate blank strings return ""; } // check for seemingly valid cases, but still need to fail // check for sole "+" cases ------if((str.length() == 1 && str.charAt(0) == '+') || Filename: src/main/java/com/transmisms/ui/javafx/MappedStringConverter.java // check for "++..." cases ------(str.length() > 1 && str.charAt(0) == str.charAt(1) && package com.transmisms.ui.javafx; str.charAt(0) == '+')) { throw new IllegalArgumentException(); import javafx.beans.property.SimpleObjectProperty; } import javafx.util.StringConverter; // check if it is a parseable number Long l = this.lsc.fromString(str); import java.util.Map; if(l == null || l.longValue() < 0) { // check for negative cases as well throw new IllegalArgumentException(); } /** return str; * StringConverter implementation for custom mapped String values. } */ } public class MappedStringConverter extends StringConverter { private final SimpleObjectProperty> lookupMap = new SimpleObjectProperty<>();

------Filename: src/main/java/com/transmisms/ui/javafx/NonemptyStringConverter.java @Override ------public String fromString(String value) { return value; package com.transmisms.ui.javafx; } import javafx.util.converter.DefaultStringConverter; @Override public String toString(String object) { Map m = this.lookupMap.get(); if(m == null) { /** return ""; * StringConverter implementation for non-emtpy String values. } */ String str = m.get(object); public class NonemptyStringConverter extends DefaultStringConverter { if(str == null) { private String prevValue = ""; return ""; } return str; } @Override public String fromString(String value) { public Map getLookupMap() { return super.fromString(value); return this.lookupMap.get(); } } @Override public void setLookupMap(Map lookupMap) { public String toString(String object) { this.lookupMap.set(lookupMap); if(object != null && !(object.equals(""))) { } this.prevValue = object; } return super.toString(object); } return super.toString(this.prevValue); } } ------Filename: src/main/java/com/transmisms/ui/javafx/MsisdnStringConverter.java ------package com.transmisms.ui.javafx; ------Filename: src/main/java/com/transmisms/ui/javafx/PortStringConverter.java import javafx.util.converter.DefaultStringConverter; ------import javafx.util.converter.LongStringConverter; package com.transmisms.ui.javafx;

/** * StringConverter implementation for MSISDN values. /** */ * StringConverter implementation for port number values. public class MsisdnStringConverter extends DefaultStringConverter { */ private final LongStringConverter lsc = new LongStringConverter(); public class PortStringConverter extends PositiveIntegerStringConverter { private int prevValue = 0; @Override public String fromString(String str) { if(str == null) { return ""; @Override } public Integer fromString(String value) { return this.normalize(str); return this.normalize(super.fromString(value)); } }

@Override @Override public String toString(String value) { public String toString(Integer value) { if(value == null) { if(value == null) { return ""; return super.toString(this.normalize(this.prevValue)); } } return value; return super.toString(this.normalize(value)); } }

private String normalize(String str) { private int normalize(int val) {

101 if(val >= 0 && val < 65536) { @FXML this.prevValue = val; private ComboBox backendDummyRole; return val; @FXML } private TextField backendAndroidHost; else { @FXML return this.prevValue; private TextField backendAndroidPort; } @FXML } private TextField backendGammuHost; } @FXML private TextField backendGammuPort; @FXML private TextField backendGammuName; @FXML private TextField backendGammuUser; ------@FXML Filename: private TextField backendGammuPassword; src/main/java/com/transmisms/ui/javafx/PositiveIntegerStringConverter.java @FXML ------private TextField backendGammuLocalmin; package com.transmisms.ui.javafx; private final DirectoryChooser downloadLocationDirectoryChooser = new DirectoryChooser(); import javafx.util.converter.IntegerStringConverter; private MainWindowController primaryController; private Map prevConfig;

/** * StringConverter implementation for positive Integer (and int primitive) * values. public void initialize(Map configRoot, */ MainWindowController primaryController) { public class PositiveIntegerStringConverter extends IntegerStringConverter { // set properties @Override this.primaryController = primaryController; public Integer fromString(String value) { return Math.abs(super.fromString(value)); // load config } this.loadConfig(configRoot);

@Override // set listeners for controls public String toString(Integer value) { return super.toString(Math.abs(value)); // initialize misc properties } this.downloadLocationDirectoryChooser.setTitle( } "Save received files to"); this.downloadLocation.end(); // move caret to end // set directory chooser's initialDirectory this.downloadLocationDirectoryChooser.setInitialDirectory( new File((String)(configRoot.get( ConfigManager.DOWNLOADLOC_KEY)))); ------this.refreshBackendSettingsLabel(null); Filename: src/main/java/com/transmisms/ui/javafx/PrefsController.java } ------public void loadConfig(Map configRoot) { package com.transmisms.ui.javafx; this.prevConfig = new HashMap<>(configRoot); // NOTE: this assumes configRoot is complete and properly checked import javafx.collections.ObservableMap; // beforehand import javafx.event.ActionEvent; import javafx.fxml.FXML; // root preferences import javafx.scene.Node; this.downloadLocation.setText( import javafx.scene.control.ComboBox; (String)(configRoot.get(ConfigManager.DOWNLOADLOC_KEY))); import javafx.scene.control.Label; this.retryOnErrors.setValue( import javafx.scene.control.TextField; (String)(configRoot.get(ConfigManager.RETRY_ON_ERRORS_KEY))); import javafx.stage.DirectoryChooser; this.countryCode.setText( import javafx.stage.Stage; (String)(configRoot.get(ConfigManager.COUNTRYCODE_KEY))); import javafx.stage.WindowEvent; this.backendType.setValue( (String)(configRoot.get(ConfigManager.BACKENDTYPE_KEY))); import java.io.File; import java.util.HashMap; Map backendPrefsRoot = import java.util.Map; (Map)(configRoot.get( ConfigManager.BACKENDSETTINGS_KEY)); import java.io.IOException; Map backendDummyPrefsRoot = (Map)(backendPrefsRoot.get( ConfigManager.BACKEND_DUMMY_KEY)); Map backendGammuPrefsRoot = public class PrefsController { (Map)(backendPrefsRoot.get( @FXML ConfigManager.BACKEND_GAMMUPSQL_KEY)); private ObservableMap backendTypeConverterMap; Map backendAndroidPrefsRoot = (Map)(backendPrefsRoot.get( @FXML ConfigManager.BACKEND_ANDROID_KEY)); private TextField downloadLocation; @FXML // dummy backend settings private ComboBox retryOnErrors; this.backendDummyHost.setText( @FXML (String)(backendDummyPrefsRoot.get( private TextField countryCode; ConfigManager.BACKEND_DUMMY_HOST_KEY))); @FXML this.backendDummyPort.setText( private ComboBox backendType; (String)(backendDummyPrefsRoot.get( @FXML ConfigManager.BACKEND_DUMMY_PORT_KEY)).toString()); private Label backendSettingsLabel; this.backendDummyLocalmin.setText( (String)(backendDummyPrefsRoot.get( @FXML ConfigManager.BACKEND_DUMMY_LOCALMIN_KEY))); private TextField backendDummyHost; this.backendDummyRole.setValue( @FXML (String)(backendDummyPrefsRoot.get( private TextField backendDummyPort; ConfigManager.BACKEND_DUMMY_ROLE_KEY))); @FXML private TextField backendDummyLocalmin; // gammu backend settings

102 this.backendGammuHost.setText( sanitizeAndGetText(this.backendDummyLocalmin)); (String)(backendGammuPrefsRoot.get( backendDummyPrefsRoot.put(ConfigManager.BACKEND_DUMMY_ROLE_KEY, ConfigManager.BACKEND_GAMMUPSQL_HOST_KEY))); this.backendDummyRole.getValue()); this.backendGammuPort.setText( ((Integer)(backendGammuPrefsRoot.get( // gammu backend settings ConfigManager.BACKEND_GAMMUPSQL_PORT_KEY))).toString()); backendGammuPrefsRoot.put(ConfigManager.BACKEND_GAMMUPSQL_HOST_KEY, this.backendGammuName.setText( sanitizeAndGetText(this.backendGammuHost)); (String)(backendGammuPrefsRoot.get( backendGammuPrefsRoot.put(ConfigManager.BACKEND_GAMMUPSQL_PORT_KEY, ConfigManager.BACKEND_GAMMUPSQL_NAME_KEY))); Integer.decode(sanitizeAndGetText(this.backendGammuPort))); this.backendGammuUser.setText( backendGammuPrefsRoot.put(ConfigManager.BACKEND_GAMMUPSQL_NAME_KEY, (String)(backendGammuPrefsRoot.get( sanitizeAndGetText(this.backendGammuName)); ConfigManager.BACKEND_GAMMUPSQL_USER_KEY))); backendGammuPrefsRoot.put(ConfigManager.BACKEND_GAMMUPSQL_USER_KEY, this.backendGammuPassword.setText( sanitizeAndGetText(this.backendGammuUser)); (String)(backendGammuPrefsRoot.get( backendGammuPrefsRoot.put(ConfigManager.BACKEND_GAMMUPSQL_PASS_KEY, ConfigManager.BACKEND_GAMMUPSQL_PASS_KEY))); sanitizeAndGetText(this.backendGammuPassword)); this.backendGammuLocalmin.setText( backendGammuPrefsRoot.put( (String)(backendGammuPrefsRoot.get( ConfigManager.BACKEND_GAMMUPSQL_LOCALMIN_KEY, ConfigManager.BACKEND_GAMMUPSQL_LOCALMIN_KEY))); sanitizeAndGetText(this.backendGammuLocalmin));

// android backend settings // android backend settings this.backendAndroidHost.setText( backendAndroidPrefsRoot.put(ConfigManager.BACKEND_ANDROID_HOST_KEY, (String)(backendAndroidPrefsRoot.get( sanitizeAndGetText(this.backendAndroidHost)); ConfigManager.BACKEND_ANDROID_HOST_KEY))); backendAndroidPrefsRoot.put(ConfigManager.BACKEND_ANDROID_PORT_KEY, this.backendAndroidPort.setText( Integer.decode( ((Integer)(backendAndroidPrefsRoot.get( sanitizeAndGetText(this.backendAndroidPort))); ConfigManager.BACKEND_ANDROID_PORT_KEY))).toString()); } // finally write to file and save curConfig public void handleDownloadLocationSelectorButton(ActionEvent event) { ConfigManager.writeConfig(configRoot); // write to file File newDownloadLocDir = // compare changes and apply changes ASAP if needed this.downloadLocationDirectoryChooser.showDialog( boolean smsServiceChanged = !this.prevConfig.get( ((Node)event.getTarget()).getScene().getWindow()); ConfigManager.BACKENDTYPE_KEY).equals( /* configRoot.get(ConfigManager.BACKENDTYPE_KEY)); this.downloadLocationDirectoryChooser.showDialog( if(!smsServiceChanged) { this.prefsStage); // check for changes in the currently selected */ // SmsService's settings Map if(newDownloadLocDir != null) { String backendKey = "backend-" + // change downloadLocation.text afterwards configRoot.get(ConfigManager.BACKENDTYPE_KEY); try { Map oldConf = this.downloadLocation.setText( (Map)(((Map) newDownloadLocDir.getCanonicalPath()); this.prevConfig.get( this.downloadLocationDirectoryChooser.setInitialDirectory( ConfigManager.BACKENDSETTINGS_KEY)).get(backendKey)); null); // unset initialDirectory Map newConf = this.downloadLocation.end(); // move caret to end (Map)(((Map) } configRoot.get( catch(IOException e) { ConfigManager.BACKENDSETTINGS_KEY)).get(backendKey)); return; // exit ASAP on error Map diff = } ConfigManager.mapDiff(oldConf, newConf); } smsServiceChanged = (diff.size() > 0); } } // save everything to prevConfig public void refreshBackendSettingsLabel(ActionEvent event) { this.prevConfig = configRoot; this.backendSettingsLabel.setText( // finally, copy changes over primaryController this.backendTypeConverterMap.get(this.backendType.getValue()) + this.primaryController.configRoot = ConfigManager.mapDeepCopy( " Backend Settings"); configRoot, this.primaryController.configRoot); } // reinitialize and restart service if relevant values are changed public void handleOnCloseRequest(WindowEvent event) { if(smsServiceChanged) { { // read changes and save back to config file this.primaryController.initializeSmsService(); Map configRoot = } ConfigManager.mapDeepCopy(this.prevConfig); } } //// read changes and commit to configRoot configRoot.put(ConfigManager.DOWNLOADLOC_KEY, //// utility functions sanitizeAndGetText(this.downloadLocation)); private static String sanitizeAndGetText(TextField t) { configRoot.put(ConfigManager.RETRY_ON_ERRORS_KEY, t.commitValue(); // reformat text first this.retryOnErrors.getValue()); return t.getText(); configRoot.put(ConfigManager.COUNTRYCODE_KEY, } sanitizeAndGetText(this.countryCode)); } configRoot.put(ConfigManager.BACKENDTYPE_KEY, this.backendType.getValue());

Map backendPrefsRoot = (Map)(configRoot.get( ------ConfigManager.BACKENDSETTINGS_KEY)); Filename: src/main/java/com/transmisms/ui/javafx/QueueAppender.java Map backendDummyPrefsRoot = ------(Map)(backendPrefsRoot.get( ConfigManager.BACKEND_DUMMY_KEY)); package com.transmisms.ui.javafx; Map backendGammuPrefsRoot = (Map)(backendPrefsRoot.get( ConfigManager.BACKEND_GAMMUPSQL_KEY)); import org.apache.logging.log4j.core.Filter; Map backendAndroidPrefsRoot = import org.apache.logging.log4j.core.Layout; (Map)(backendPrefsRoot.get( import org.apache.logging.log4j.core.LogEvent; ConfigManager.BACKEND_ANDROID_KEY)); import org.apache.logging.log4j.core.appender.AbstractAppender; import org.apache.logging.log4j.core.appender.AppenderLoggingException; // dummy backend settings import org.apache.logging.log4j.core.config.plugins.Plugin; backendDummyPrefsRoot.put(ConfigManager.BACKEND_DUMMY_HOST_KEY,import org.apache.logging.log4j.core.config.plugins.PluginAttribute; sanitizeAndGetText(this.backendDummyHost)); import org.apache.logging.log4j.core.config.plugins.PluginElement; backendDummyPrefsRoot.put(ConfigManager.BACKEND_DUMMY_PORT_KEY,import org.apache.logging.log4j.core.config.plugins.PluginFactory; Integer.decode(sanitizeAndGetText(this.backendDummyPort)));import org.apache.logging.log4j.core.layout.PatternLayout; backendDummyPrefsRoot.put(ConfigManager.BACKEND_DUMMY_LOCALMIN_KEY,

103 import java.io.Serializable; import com.google.gson.Gson; import java.util.Queue; import org.zeromq.ZMQ; import java.util.concurrent.BlockingQueue; import org.zeromq.ZMQ.Context; import java.util.concurrent.LinkedBlockingQueue; import org.zeromq.ZMQ.Poller; import java.util.concurrent.TimeUnit; import org.zeromq.ZMQ.Socket;

import java.util.Date; import java.util.Queue; @Plugin(name="QueueAppender", category="Core", elementType="appender", import java.util.concurrent.BlockingQueue; printObject=true) import java.util.concurrent.ConcurrentLinkedQueue; public class QueueAppender extends AbstractAppender { import java.util.concurrent.CountDownLatch; import java.util.concurrent.LinkedBlockingQueue; private static BlockingQueue queue = import java.util.concurrent.TimeUnit; new LinkedBlockingQueue(); import java.util.concurrent.atomic.AtomicReference;

public static String pop() throws InterruptedException { import com.transmisms.core.protocol.SmsServiceException; return QueueAppender.queue.take(); import zmq.ZError; }

public static String poll() throws InterruptedException { return QueueAppender.queue.poll(0l, TimeUnit.MILLISECONDS); public class AndroidSmsService extends ExtendedSmsService { } public enum Reason implements SmsServiceException.Reason, ExtendedSmsService.ExtendedReason { private QueueAppender(String name, Filter filter, IOERROR ("Problem connecting to device", Layout layout, "Please check device connectivity and try again."), final boolean ignoreExceptions) { NOPONG ("Device is not responding", super(name, filter, layout, ignoreExceptions); "Please check the device and try again."); }

@Override public void append(LogEvent event) { private final String message; try { private final String details; QueueAppender.queue.offer(new String(this.getLayout().toByteArray( event)), 1000l, TimeUnit.MILLISECONDS); } // default access control since enums are restricted catch(InterruptedException e) { Reason(String message, String details) { return; // do nothing and exit ASAP this.message = message; } this.details = details; catch(Exception ex) { } if (!ignoreExceptions()) { throw new AppenderLoggingException(ex); } @Override } public Reason valueOf() { } return valueOf(this.name()); } @PluginFactory public static QueueAppender createAppender( @Override @PluginAttribute("name") String name, public final String getMessage() { @PluginElement("Filter") final Filter filter, return this.message; @PluginElement("Layout") Layout layout, } @PluginAttribute("ignoreExceptions") boolean ignoreExceptions, @Override @PluginAttribute("otherAttribute") String otherAttribute) { public final String getDetails() { // we basically do what other implementations of return this.details; // @PluginFactory createAppender() do: some checks and behave as } } // a normal factory method if (name == null) { LOGGER.error("No name provided for QueueAppender"); return null; /** } * Copied from com.transimsms.androidcompanion.ClientAction. if (layout == null) { * This must be always up to date to the Transmisms Android Companion's layout = PatternLayout.createDefaultLayout(); * ClientAction class. } */ return new QueueAppender(name, filter, layout, ignoreExceptions); public static class ClientAction { } final public String action;

@PluginFactory // send public static QueueAppender createAppender( final public String recipient; @PluginAttribute("name") String name, final public String message; @PluginElement("Filter") final Filter filter, @PluginElement("Layout") Layout layout, // ping @PluginAttribute("otherAttribute") String otherAttribute) { // msisdn return QueueAppender.createAppender(name, filter, layout, true, // NOTHING FOLLOWS otherAttribute); } public ClientAction() { this.action = null; } this.recipient = null; this.message = null; }

// send ------public ClientAction(String recipient, String message) { Filename: this.action = "send"; src/main/java/com/transmisms/ui/javafx/smsservice/AndroidSmsService.java this.recipient = recipient; ------this.message = message; } package com.transmisms.ui.javafx.smsservice; // msisdn import com.transmisms.core.protocol.SmsEntry; public ClientAction(String s) { import com.transmisms.core.protocol.SmsService; this.action = "msisdn"; this.recipient = null;

104 this.message = null; private String peerMin = null; } private String localMin = null;

// ping private final Queue commandQueue = public ClientAction(char c) { new ConcurrentLinkedQueue(); this.action = "ping"; private final Queue sendQueue = this.recipient = null; new ConcurrentLinkedQueue(); this.message = null; private final BlockingQueue recvQueue = } new LinkedBlockingQueue(); } private final AtomicReference currentException = new AtomicReference<>(null); /** * Copied from com.transimsms.androidcompanion.ServerAction . private CountDownLatch cdl = new CountDownLatch(1); * This must be always up to date to the Transmisms Android Companion's * ServerAction class. */ public static class ServerAction { public AndroidSmsService(String host, int port) { final public String action; this.zmqAddress = "tcp://" + host + ":" + port; } // recv final public String sender; final public String message; @Override final public Long timestamp; public String getLocalMin() { this.sendMsisdnRequest(); // error if(this.cdl != null) { final public String code; try { final public String[] alternativecodes; // optional this.cdl.await(); } // pong catch(InterruptedException e) {} // do nothing on interruption // NOTHING FOLLOWS this.cdl = null; } public ServerAction() { return this.localMin; this.action = null; } this.code = null; this.alternativecodes = null; @Override this.sender = null; public void setPeer(String peerMin) { this.message = null; super.setPeer(peerMin); this.timestamp = null; this.peerMin = peerMin; } }

// recv @Override public ServerAction(String sender, public String getPeer() { String message, long timestamp) { return this.peerMin; this.action = "recv"; } this.sender = sender; this.message = message; @Override this.timestamp = timestamp; protected void sendMessageToBackend(String message) { this.code = null; if(this.isStarted && this.peerMin != null) { this.alternativecodes = null; this.sendQueue.offer(message); } } } // error public ServerAction(String code, String[] alternativecodes) { @Override this.action = "error"; public SmsEntry pollMessageFromBackend(long timeout, TimeUnit unit) this.code = code; throws SmsServiceException, InterruptedException { this.alternativecodes = alternativecodes; // throw exceptions generated from our polling loop, if any this.sender = null; SmsServiceException e = this.currentException.get(); this.message = null; if(e != null) { this.timestamp = null; this.currentException.set(null); // clear exception } throw e; } // msisdn // for normal cases public ServerAction(String msisdn) { if(this.isStarted) { this.action = "msisdn"; return this.recvQueue.poll(timeout, unit); this.message = msisdn; } // no fields return null; this.code = null; } this.alternativecodes = null; this.sender = null; @Override this.timestamp = null; public boolean start() throws SmsServiceException { } super.start(); if(!this.isStarted) { // pong final CountDownLatch cdl = new CountDownLatch(1); public ServerAction(char c) { AtomicReference ses = new AtomicReference<>(); this.action = "pong"; // start a separate thread to consume and produce entries // no fields // this also includes the closing procedures this.code = null; (new Thread() { this.alternativecodes = null; @Override this.sender = null; public void run() { this.message = null; // setup necessary connections this.timestamp = null; final Context zmqContext = ZMQ.context(1); } final Socket zmqSocket = zmqContext.socket(ZMQ.PAIR); } // set linger to stop connection ASAP zmqSocket.setLinger(0); // connect or bind depending on the role zmqSocket.connect(AndroidSmsService.this.zmqAddress); public final static int PING_TIMEOUT = 1000; AndroidSmsService.this.isStarted = false; private final Gson gson = new Gson(); // NOTE: all messages prior to 'starting' this service private final String zmqAddress; // are DISCARDED private volatile boolean isStarted = false; // PING receiver and wait for PONG

105 String jsonStr = AndroidSmsService.this.gson.toJson( sAction.message, new ClientAction('p')); // PING ClientAction new Date(sAction.timestamp))); zmqSocket.send(jsonStr); } else if(sAction.action.equals("msisdn")) { // attempt getting a PONG response for LIMIT times AndroidSmsService.this.localMin = int LIMIT = 3; sAction.message; for(int i = 0; i < LIMIT; i++) { if(AndroidSmsService.this.cdl != null) { Poller plr = zmqContext.poller(); AndroidSmsService.this.cdl.countDown(); plr.register(zmqSocket, Poller.POLLIN); } try { } plr.poll(PING_TIMEOUT); // wait for PING_TIMEOUT ms else if(sAction.action.equals("error")) { } } catch(ZError.IOException e) { } System.out.println("got ZError.IOException " + plr.close(); "on connectivity test: " + e); } ses.set(new SmsServiceException( Reason.IOERROR)); // finally, close everything afterwards } try { if(plr.pollin(0)) { if(zmqSocket != null) { String s = zmqSocket.recvStr(0); zmqSocket.close(); ServerAction sAction = } AndroidSmsService.this.gson.fromJson(s, } ServerAction.class); catch(ZError.IOException e) { if(sAction != null && sAction.action != null & & System.out.println("got ZError.IOException on " + sAction.action.equals("pong")) { "closing zmqSocket: " + e); plr.close(); // preemptive close before exiting } AndroidSmsService.this.isStarted = true; try { break; // exit ASAP if(!zmqContext.isTerminated()) { } zmqContext.term(); // otherwise, discard received data and try again } else { } // 'infinite' retries/exhaustion of entries catch(ZError.IOException e) { i--; System.out.println("got ZError.IOException on " + } "closing zmqContext: " + e); } } plr.close(); // because we need more close() // set other variables as well } AndroidSmsService.this.isStarted = false; if(!AndroidSmsService.this.isStarted && } ses.get() == null) { }).start(); ses.set(new SmsServiceException(Reason.NOPONG)); try { } cdl.await(); cdl.countDown(); // ready for examining our isStarted flag } catch(InterruptedException e) { // loop until isStarted flag is revoked return false; while(AndroidSmsService.this.isStarted) { } // send all queued commands if(!this.isStarted) { for(String message = // do cleanup and throw exception AndroidSmsService.this.commandQueue.poll(); this.sendQueue.clear(); message != null; this.recvQueue.clear(); message = throw ses.get(); AndroidSmsService.this.commandQueue.poll()) { } zmqSocket.send(message); return this.isStarted; } } // send all pending messages return false; for(String message = } AndroidSmsService.this.sendQueue.poll(); message != null; message = @Override AndroidSmsService.this.sendQueue.poll()) { public boolean stop() { ClientAction cAction = new ClientAction( super.stop(); AndroidSmsService.this.peerMin, message); if(this.isStarted) { String str = AndroidSmsService.this.gson.toJson( this.isStarted = false; cAction, ClientAction.class); // clear sendQueue and recvQueue zmqSocket.send(str); this.sendQueue.clear(); } this.recvQueue.clear(); // poll for the time being } Poller plr = zmqContext.poller(); return false; plr.register(zmqSocket, Poller.POLLIN); } try { plr.poll(100); // poll for 100ms @Override } public boolean isRunning() { catch(ZError.IOException e) { return this.isStarted; System.out.println("got ZError.IOException: " + e); } AndroidSmsService.this.stop(); // stop ASAP // then set currentException to be thrown on private void sendMsisdnRequest() { // pollMessage()/pollMessageFromBackend() later if(this.localMin == null) { AndroidSmsService.this.currentException.set( ClientAction cAction = new ClientAction("msisdn"); new SmsServiceException(Reason.IOERROR)); String str = this.gson.toJson(cAction, ClientAction.class); break; this.commandQueue.offer(str); } } if(plr.pollin(0)) { } String str = zmqSocket.recvStr(0); } ServerAction sAction = AndroidSmsService.this.gson.fromJson(str, ServerAction.class); // check for errors first if(sAction.action == null) { ------continue; // do nothing and loop back Filename: } src/main/java/com/transmisms/ui/javafx/smsservice/ControlPrioritySmsService.java if(sAction.action.equals("recv")) { ------AndroidSmsService.this.recvQueue.offer( new SmsEntry(sAction.sender, package com.transmisms.ui.javafx.smsservice;

106 } import com.transmisms.core.protocol.SmsEntry; import com.transmisms.core.protocol.SmsService; public ControlPrioritySmsService() { import com.transmisms.core.protocol.SmsServiceException; this.loopTimeout = 5000; import com.transmisms.core.util.commons.FIFOEntry; } import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.PriorityBlockingQueue; @Override import java.util.concurrent.TimeUnit; public final void sendMessage(String message) { if(this.isStarted) { if(StringWrapper.getWeight(message) > 10) { this.pbq.offer(new FIFOEntry(new StringWrapper(message))); public abstract class ControlPrioritySmsService implements SmsService { } private static class StringWrapper implements Comparable { else { final public String value; this.lpq.offer(message); } public StringWrapper(String value) { } this.value = value; } } @Override @Override public synchronized boolean start() throws SmsServiceException { public int compareTo(StringWrapper o) { if(!this.isStarted) { // NOTE: this assumes that o will never be null this.isStarted = true; return StringWrapper.getWeight(o.value) - // send message on a loop on a separate thread StringWrapper.getWeight(this.value); this.sendLoopThread = new Thread(() -> { } while(this.isStarted) { try { @Override FIFOEntry entry = this.pbq.poll( public boolean equals(Object o) { this.loopTimeout, TimeUnit.MILLISECONDS); if(o == null || !(o instanceof StringWrapper)) { if(entry != null) { return false; String message = entry.getEntry().value; } this.sendMessageToBackend(message); else { for(int i = 0; i < this.MAX_BUFFER_SIZE-1; i++) { return 0 == this.compareTo((StringWrapper)o); entry = this.pbq.poll(); } // break when we run out of entries } if(entry == null) { break; public static int getWeight(String s) { } // NOTE: this assumes that s will never be null message = entry.getEntry().value; // NOTE: this is based on static initialization of PDU class this.sendMessageToBackend(message); if(s.toLowerCase().startsWith("no")) { } return 50; } } else { else if(s.startsWith("?transmismsv")) { String message = this.lpq.poll(); return 40; if(message != null) { } this.sendMessageToBackend(message); else if(s.startsWith("f") || s.startsWith("F")) { } return 40; } } } else if(s.startsWith("r")) { catch(InterruptedException e) { return 30; break; } } else if(s.startsWith("a") || s.startsWith("b") || } s.startsWith("c")) { }); return 20; this.sendLoopThread.setDaemon(true); } this.sendLoopThread.start(); else if(s.startsWith("d") || s.startsWith("i") || return true; s.startsWith("j") || s.startsWith("e") || } s.startsWith("E")) { return false; return 10; } } return 0; @Override } public synchronized boolean stop() { } if(this.isStarted) { this.isStarted = false; this.sendLoopThread.interrupt(); this.pbq.clear(); final private int MAX_BUFFER_SIZE = 5; return true; } final private int loopTimeout; return false; final private PriorityBlockingQueue> pbq = } new PriorityBlockingQueue<>(); final private Queue lpq = new ConcurrentLinkedQueue<>(); @Override protected boolean isStarted = false; public abstract SmsEntry pollMessage(long timeout, TimeUnit unit) private Thread sendLoopThread = null; throws SmsServiceException, InterruptedException;

@Override public abstract boolean isRunning(); protected abstract void sendMessageToBackend(String message); }

public abstract String getLocalMin();

public abstract void setPeer(String peerMin); ------public abstract String getPeer(); Filename: src/main/java/com/transmisms/ui/javafx/smsservice/DummySmsService.java ------

package com.transmisms.ui.javafx.smsservice; public ControlPrioritySmsService(int loopTimeout) { this.loopTimeout = loopTimeout; import com.transmisms.core.protocol.SmsEntry;

107 import com.transmisms.core.protocol.SmsService; @Override public void setPeer(String peerMin) { import org.zeromq.ZMQ; super.setPeer(peerMin); import org.zeromq.ZMQ.Context; this.peerMin = peerMin; import org.zeromq.ZMQ.Poller; } import org.zeromq.ZMQ.Socket; @Override import java.util.Date; public String getPeer() { import java.util.Queue; return this.peerMin; import java.util.concurrent.BlockingQueue; } import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.CountDownLatch; @Override import java.util.concurrent.LinkedBlockingQueue; protected void sendMessageToBackend(String message) { import java.util.concurrent.TimeUnit; if(this.isStarted && this.peerMin != null) { import java.util.concurrent.atomic.AtomicReference; this.sendQueue.offer(this.getLocalMin() + ":" + message); } import com.transmisms.core.protocol.SmsServiceException; } import zmq.ZError; @Override public SmsEntry pollMessageFromBackend(long timeout, TimeUnit unit) throws SmsServiceException, InterruptedException { public class DummySmsService extends ExtendedSmsService { // throw exceptions generated from our polling loop, if any public enum Reason implements SmsServiceException.Reason, SmsServiceException e = this.currentException.get(); ExtendedSmsService.ExtendedReason { if(e != null) { IOERROR ("Problem connecting to Dummy device", this.currentException.set(null); // clear exception "Please check device connectivity and try again."), throw e; NOPONG ("Dummy device is not responding", } "Please check the device and try again."); // for normal cases if(this.isStarted) { return this.recvQueue.poll(timeout, unit); } private final String message; return null; // return null if not yet started or on failures private final String details; }

@Override // default access control since enums are restricted public boolean start() throws SmsServiceException { Reason(String message, String details) { super.start(); this.message = message; if(!this.isStarted) { this.details = details; final CountDownLatch cdl = new CountDownLatch(1); } AtomicReference ses = new AtomicReference<>( new SmsServiceException(Reason.NOPONG)); // start a separate thread to consume and produce entries @Override // this also includes the closing procedures public Reason valueOf() { (new Thread() { return valueOf(this.name()); @Override } public void run() { // setup necessary connections @Override final Context zmqContext = ZMQ.context(1); public final String getMessage() { final Socket zmqSocket = zmqContext.socket(ZMQ.PAIR); return this.message; // set linger to stop connection ASAP } zmqSocket.setLinger(0); @Override // connect or bind depending on the role public final String getDetails() { if(DummySmsService.this.isSender) { return this.details; zmqSocket.connect(DummySmsService.this.zmqAddress); } } } else { zmqSocket.bind(DummySmsService.this.zmqAddress); }

public final static int PING_TIMEOUT = 1000; DummySmsService.this.isStarted = false; // NOTE: all messages prior to 'starting' this service private final String zmqAddress; // are DISCARDED private final boolean isSender; if(DummySmsService.this.isSender) { private volatile boolean isStarted = false; // PING receiver and wait for PONG private String peerMin = null; zmqSocket.send("PING"); private String localMin = null; // attempt getting a PONG response for LIMIT times private final Queue sendQueue = int LIMIT = 3; new ConcurrentLinkedQueue(); for(int i = 0; i < LIMIT; i++) { private final BlockingQueue recvQueue = Poller plr = zmqContext.poller(); new LinkedBlockingQueue(); plr.register(zmqSocket, Poller.POLLIN); private final AtomicReference currentException = try { new AtomicReference<>(null); plr.poll(PING_TIMEOUT); // wait for PING_TIMEOUT ms } catch(ZError.IOException e) { public DummySmsService(String host, int port, String localMin, System.out.println("got ZError.IOException " + boolean isSender) { "on connectivity test: " + e); super(100); // set loopTimeout as 100ms ses.set(new SmsServiceException( this.zmqAddress = "tcp://" + host + ":" + port; Reason.IOERROR)); this.localMin = localMin; } this.isSender = isSender; if(plr.pollin(0)) { } String raw = zmqSocket.recvStr(0); if(raw.equals("PONG")) { plr.close(); // preemptive close before exiting @Override DummySmsService.this.isStarted = true; public String getLocalMin() { break; // exit ASAP return this.localMin; } } // otherwise, discard received data and try again

108 } @Override plr.close(); // because we need more close() public boolean stop() { } super.stop(); } if(this.isStarted) { cdl.countDown(); // ready for examining our isStarted flag this.isStarted = false; // clear sendQueue and recvQueue // loop until isStarted flag is revoked this.sendQueue.clear(); while(DummySmsService.this.isStarted) { this.recvQueue.clear(); // send all pending messages } for(String message = return false; DummySmsService.this.sendQueue.poll(); } message != null; message = DummySmsService.this.sendQueue.poll()) { @Override zmqSocket.send(message); public boolean isRunning() { } return this.isStarted; // poll for the time being } Poller plr = zmqContext.poller(); } plr.register(zmqSocket, Poller.POLLIN); try { plr.poll(100); // poll for 100ms } catch(ZError.IOException e) { ------System.out.println("got ZError.IOException: " Filename:+ e); DummySmsService.this.stop(); // stop ASAP src/main/java/com/transmisms/ui/javafx/smsservice/ExtendedSmsService.java // then set currentException to be thrown on ------// pollMessage()/pollMessageFromBackend() later DummySmsService.this.currentException.set( package com.transmisms.ui.javafx.smsservice; new SmsServiceException(Reason.IOERROR)); break; import com.transmisms.core.protocol.SmsEntry; } import com.transmisms.ui.javafx.Utils; if(plr.pollin(0)) { // process PING/PONG requests first import java.util.concurrent.TimeUnit; String raw = zmqSocket.recvStr(0); if(raw.equals("PING")) { // reply with PONG import com.transmisms.core.protocol.SmsServiceException; zmqSocket.send("PONG"); continue; } else if(raw.equals("PONG")) { public abstract class ExtendedSmsService extends ControlPrioritySmsService { // just ignore and try again public interface ExtendedReason { continue; public String getMessage(); } public String getDetails(); String[] parts = raw.split(":", 2); } String sender = parts[0]; String msg = parts[1]; private String countryCode = null; plr.close(); private String normalizedPeer = null; DummySmsService.this.recvQueue.offer( new SmsEntry(sender, msg, new Date())); } plr.close(); public ExtendedSmsService() {} } public ExtendedSmsService(int loopTimeout) { // finally, close everything afterwards super(loopTimeout); try { } if(zmqSocket != null) { zmqSocket.close(); } } public final SmsEntry pollMessage(long timeout, TimeUnit unit) catch(ZError.IOException e) { throws SmsServiceException, InterruptedException { System.out.println("got ZError.IOException on " + SmsEntry entry = this.pollMessageFromBackend(timeout, unit); "closing zmqSocket: " + e); // set as the peer if there's still no peer set previously } if(entry != null && this.getPeer() == null) { try { if(this.countryCode == null) { if(!zmqContext.isTerminated()) { this.setPeer(entry.getSender()); zmqContext.term(); } } else { } this.setPeer(Utils.getIntFormat(entry.getSender(), catch(ZError.IOException e) { this.countryCode)); System.out.println("got ZError.IOException on " + } "closing zmqContext: " + e); } } if(entry != null && this.getPeer() != null) { // set other variables as well // return null on non-peer senders DummySmsService.this.isStarted = false; if(!entry.getSender().endsWith(this.normalizedPeer)) { } return null; }).start(); } try { } cdl.await(); return entry; } } catch(InterruptedException e) { return false; public abstract SmsEntry pollMessageFromBackend(long timeout, } TimeUnit unit) if(!this.isStarted) { throws SmsServiceException, InterruptedException; // do cleanup and throw exception this.sendQueue.clear(); public void setCountryCode(String countryCode) { this.recvQueue.clear(); this.countryCode = countryCode; throw ses.get(); } } return this.isStarted; public void setPeer(String peer) { } if(peer != null) { return false; this.normalizedPeer = Utils.normalizeMsisdn(peer, } this.countryCode); }

109 else { "SELECT * FROM phones WHERE \"TimeOut\" > " + this.normalizedPeer = null; "(CURRENT_TIMESTAMP - INTERVAL '2 MINUTE')"; } private static final String insertQueryStr = } "INSERT INTO outbox (\"TextDecoded\", \"DestinationNumber\", " + } "\"CreatorID\", \"Priority\") VALUES (?, ?, ?, ?)"; private static final String getCurtimestampQueryStr = "SELECT CURRENT_TIMESTAMP AT TIME ZONE current_setting('TimeZone')"; public final static int PING_TIMEOUT = 3000;

------private boolean isStarted = false; Filename: src/main/java/com/transmisms/ui/javafx/smsservice/GammuSmsService.java private Timestamp startTime; ------private String peerMin = null; private int lastMessageId = -1; package com.transmisms.ui.javafx.smsservice; private final String dbHost; private final int dbPort; import com.transmisms.core.protocol.SmsEntry; private final String dbName; import com.transmisms.core.protocol.SmsService; private final String dbUser; private final String dbPassword; import java.sql.Connection; private final String localMin; import java.sql.Timestamp; import java.sql.DriverManager; private Connection dbConnection = null; import java.sql.PreparedStatement; private PreparedStatement messageQuery = null; import java.sql.ResultSet; private PreparedStatement statusQuery = null; import java.sql.Statement; private PreparedStatement insertQuery = null; import java.util.Queue; private PreparedStatement getCurtimestampQuery = null; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.Executors; private final Queue entryQueue = new ConcurrentLinkedQueue<>(); import java.util.concurrent.ExecutorService; private final ExecutorService executor = import java.util.concurrent.Future; Executors.newSingleThreadExecutor(); import java.util.concurrent.TimeUnit; private Future queryFuture = null; import com.transmisms.core.protocol.SmsServiceException; import java.sql.SQLException; import java.util.concurrent.CancellationException; public GammuSmsService(String dbHost, int dbPort, String dbName, import java.util.concurrent.ExecutionException; String dbUser, String dbPassword, String localMin) { import java.util.concurrent.TimeoutException; super(3000); // set loopTimeout as 3000ms this.dbHost = dbHost; this.dbPort = dbPort; this.dbName = dbName; public class GammuSmsService extends ExtendedSmsService { this.dbUser = dbUser; public enum Reason implements SmsServiceException.Reason, this.dbPassword = dbPassword; ExtendedSmsService.ExtendedReason { this.localMin = localMin; NOCONN ("Problem connecting to database backend", } "Please check database connectivity and try again."), NODEVICE ("Device not found", "Please check the device and try again."); public String getLocalMin() { return this.localMin; } private final String message; private final String details; @Override public void setPeer(String peerMin) { super.setPeer(peerMin); // default access control since enums are restricted this.peerMin = peerMin; Reason(String message, String details) { try { this.message = message; if(peerMin != null) { this.details = details; this.messageQuery = this.dbConnection.prepareStatement( } GammuSmsService.messageQueryFilteredStr); } else { @Override this.messageQuery = this.dbConnection.prepareStatement( public Reason valueOf() { GammuSmsService.messageQueryUnfilteredStr); return valueOf(this.name()); } } } catch(SQLException e) { @Override e.printStackTrace(); public final String getMessage() { } return this.message; } } @Override @Override public final String getDetails() { public String getPeer() { return this.details; return this.peerMin; } } } @Override protected synchronized void sendMessageToBackend(String message) { if(this.isStarted && this.getPeer() != null) { try { private static final String messageQueryOrderStr = this.insertQuery.setString(1, message); " ORDER BY \"UpdatedInDB\", \"ID\" ASC"; this.insertQuery.setString(2, this.getPeer()); // NOTE: This filtering only works on mobile-terminated numbers // NOTE: 'default' is used only on single-modem setups. This // (i.e. phones). This might be good on CPs with customized suffix // cannot be used on Gammu databases for use with // numbers, but not on the rest. // multiple modems private static final String messageQueryFilteredStr = this.insertQuery.setString(3, "default"); "SELECT * FROM inbox WHERE \"ID\" > ? AND " + this.insertQuery.setInt(4, "\"UpdatedInDB\" > ? AND \"SenderNumber\" LIKE ?" + ControlPrioritySmsService.StringWrapper.getWeight( messageQueryOrderStr; message)); private static final String messageQueryUnfilteredStr = // we won't get the return value of the statement below "SELECT * FROM inbox WHERE \"ID\" > ? AND " + this.insertQuery.executeUpdate(); "\"UpdatedInDB\" > ?" + messageQueryOrderStr; } private static final String statusQueryStr = catch(SQLException e) {

110 e.printStackTrace(); if(e != null && e.getCause() != null) { return; e.getCause().printStackTrace(); } } } } } catch(CancellationException e) { if(e != null && e.getCause() != null) { @Override e.getCause().printStackTrace(); public SmsEntry pollMessageFromBackend(long timeout, TimeUnit unit) } throws SmsServiceException, InterruptedException { } if(this.isStarted) { finally { SmsEntry entry = null; this.queryFuture = null; // try to get a pending entry first } entry = this.entryQueue.poll(); return entry; if(entry != null) { } return entry; return null; // just return null otherwise (or on timeout/error) } }

// create a separate task for querying @Override this.queryFuture = this.executor.submit(() -> { public synchronized boolean start() throws SmsServiceException { while(!Thread.interrupted()) { super.start(); ResultSet rs = null; if(!this.isStarted) { boolean hasResults = false; // start the connection String sender = null; String jdbcUrl = "jdbc:postgresql://" + this.dbHost + ":" String message = null; + this.dbPort + "/" + this.dbName; Timestamp timestamp = null; ResultSet rs = null; int messageId = -1; try { try { // prepare connection this.messageQuery.setInt(1, this.lastMessageId); this.dbConnection = DriverManager.getConnection( this.messageQuery.setTimestamp(2, this.startTime); jdbcUrl, this.dbUser, this.dbPassword); if(this.getPeer() != null) { // prepare PreparedStatements this.messageQuery.setString(3, "%"+this.getPeer()); this.messageQuery = this.dbConnection.prepareStatement( } GammuSmsService.messageQueryUnfilteredStr); this.statusQuery = this.dbConnection.prepareStatement( rs = this.messageQuery.executeQuery(); GammuSmsService.statusQueryStr); hasResults = rs.next(); this.insertQuery = this.dbConnection.prepareStatement( if(hasResults) { GammuSmsService.insertQueryStr); sender = rs.getString("SenderNumber"); this.getCurtimestampQuery = this.dbConnection.prepareStatement( message = rs.getString("TextDecoded"); GammuSmsService.getCurtimestampQueryStr); timestamp = rs.getTimestamp("UpdatedInDB"); messageId = rs.getInt("ID"); // get CURRENT_TIMESTAMP and use it for querying messages // update lastMessageId for next queries rs = this.getCurtimestampQuery.executeQuery(); this.lastMessageId = messageId; if(rs.next()) { } this.startTime = rs.getTimestamp(1); } } catch(SQLException e) { e.printStackTrace(); // attempt checking for connected 'phones' within the break; // exit on errors // last 2 minutes for LIMIT times } int LIMIT = 3; finally { for(int i = 0; i < LIMIT; i++) { // clean up first rs = this.statusQuery.executeQuery(); try { if(rs.next()) { // got a 'phone' if(rs != null) { this.isStarted = true; rs.close(); break; } } } // sleep for a while before retrying catch(SQLException e) { try { e.printStackTrace(); Thread.sleep(PING_TIMEOUT); break; // exit on errors } } catch(InterruptedException e) { break; // return if there are results, loop again otherwise } if(hasResults) { } // then return a single entry if(!this.isStarted) { this.entryQueue.offer(new SmsEntry( this.stop(); // stop the service ASAP sender, message, timestamp)); throw new SmsServiceException(Reason.NODEVICE); return null; } } } else { catch(SQLException e) { try { // just sleep for a while and try again this.stop(); // stop the service ASAP Thread.sleep(1000); // sleep for a second throw new SmsServiceException(Reason.NOCONN); } } catch(InterruptedException e) { finally { // try to close everything break; // exit on break try { } if(rs != null) { } rs.close(); } } } } return null; // just return null on timeout/failure catch(SQLException e) { }); e.printStackTrace(); } try { // execute and wait for results until timeout // finally, set the isStarted flag to true this.queryFuture.get(timeout, unit); } entry = this.entryQueue.poll(); return this.isStarted; } } catch(ExecutionException e) { else { if(e != null && e.getCause() != null) { return false; e.getCause().printStackTrace(); } } } } catch(TimeoutException e) { @Override

111 public boolean stop() { e.printStackTrace(); super.stop(); } if(this.isStarted) { // set css for the parent // stop the execution of current queries header.getStylesheets().add("dialog.css"); if(this.queryFuture != null) { // set icon for dialog this.queryFuture.cancel(true); ImageView iv = (ImageView)header.lookup(".dialogIcon"); } iv.setImage(img); try { // close the connection and other related objects // set necessary text properties if(this.dbConnection != null) { this.primaryLabel = (Label)header.lookup(".dialogPrimaryLabel"); this.dbConnection.close(); this.secondaryLabel = (Text)header.lookup(".dialogSecondaryLabel"); } primaryLabel.setText(primaryText); if(this.messageQuery != null) { secondaryLabel.setText(secondaryText); this.messageQuery.close(); // add the header to dialog pane } this.getDialogPane().setHeader(header); if(this.statusQuery != null) { this.statusQuery.close(); // misc modifications for the dialog } this.initStyle(StageStyle.UTILITY); if(this.insertQuery != null) { } this.insertQuery.close(); } } public void setPrimaryText(String text) { catch(SQLException e) { this.primaryLabel.setText(text); e.printStackTrace(); } } finally { public void setSecondaryText(String text) { // then let JVM GC the remnants this.secondaryLabel.setText(text); this.dbConnection = null; } this.messageQuery = null; this.statusQuery = null; public void appendSecondaryText(String text) { this.insertQuery = null; this.secondaryLabel.setText(this.secondaryLabel.getText() + text); this.getCurtimestampQuery = null; } // finally, set the isStarted flag to false } this.isStarted = true; } return true; } else { ------return false; Filename: src/main/java/com/transmisms/ui/javafx/Utils.java } ------} package com.transmisms.ui.javafx; @Override public boolean isRunning() { return this.isStarted; } public class Utils { } public static final int MIN_MIN_LENGTH = 10;

/** * Normalizes MSISDN and other numbers into 10-digit MINs * ------* @param msisdn number to be normalized into 10-digit MINs Filename: src/main/java/com/transmisms/ui/javafx/StandardDialog.java */ ------public static String normalizeMsisdn(String msisdn, String countryCode) { // if carrier-provided access code, return ASAP package com.transmisms.ui.javafx; if(msisdn == null || msisdn.length() < MIN_MIN_LENGTH) { return msisdn; import javafx.scene.Parent; } import javafx.scene.control.ButtonType; import javafx.scene.control.Dialog; String min = msisdn; import javafx.scene.control.Label; // remove leading '+', if any import javafx.scene.image.Image; if(min.startsWith("+")) { import javafx.scene.image.ImageView; min = min.substring(1); import javafx.scene.text.Text; } import javafx.stage.StageStyle; // remove leading zeroes min = min.replaceFirst("^0+(?!$)", ""); import javafx.fxml.FXMLLoader; if(min.startsWith(countryCode)) { min = min.substring(countryCode.length()); import java.io.IOException; } return min; } public class StandardDialog extends Dialog { /** private final Label primaryLabel; * Converts mobile numbers into international formats private final Text secondaryLabel; */ public static String getIntFormat(String min, String countryCode) { // if carrier-provided access code, return ASAP if(min == null || min.length() < MIN_MIN_LENGTH) { public StandardDialog(String primaryText, String secondaryText, return min; Image img) { } // prepare and set the header FXMLLoader dialogTemplateLoader = new FXMLLoader( String intNumber = min; MainWindowController.class.getClassLoader() // try to normalize first .getResource("dialog-template.fxml")); intNumber = Utils.normalizeMsisdn(min, countryCode); Parent header = null; // then append the country code try { intNumber = "+"+countryCode+intNumber; header = (Parent)(dialogTemplateLoader.load()); } return intNumber; catch(IOException e) { } //mainLogger.error( } System.out.println( "Caught IOException when loading dialog header templates");

112 GridPane.rowIndex="6" GridPane.columnIndex="0" /> ------

}
-fx-wrap-text: true; -fx-font-size: 12px; }