BbTS notes
Oh look, information I'm not supposed to have.
This page is a collection of notes and observations related to Blackboard Transact.
To sort
BbTS Service Point API
Blackboard runs a few hosted services for universities that use Transact. The first of these is eAccounts, which is an online service for checking account balances (and a few other optional features, like making deposits). A web interface for eAccounts is hosted at a domain like eacct-<institution>-sp.blackboard.com
. It's not very interesting, containing no real API.
What's more interesting is the API used by the eAccounts mobile app, located at <institution>-sp.blackboard.com
. This requires OAuth v1 credentials to use (hardcoded into the mobile app). Aside from the eAccounts API on this domain, the Service Point API (official Blackboard documentation) is hosted here. Making requests to the endpoints listed there using the
BbTS Transaction protocol
This is what I've been able to find about how transactions are made. At some point I should probably clean this up.
Source
The source of all of the information here is ext-payment-gateway.jar
from the Papercut print management system. I had to run the installer binary for the payment gateway module in a Windows VM, then found this jar in %INSTALLDIR%\lib-ext
.
Summary
The protocol is a simple request-response protocol. Requests have a sequence number; the Transact server responds with this sequence number so that the client can correlate responses to requests. Messages can be encrypted with a shared AES key (in CBC mode, with custom padding). Messages are not authenticated, only encrypted.
Since the messages are not authenticated, and the AES IV is constant, and timestamps aren't verified, replay attacks on the protocol are possible even when the messages are encrypted.
Fields contained in a message
A request message consists of a few fields:
- Whether or not the message is encrypted, represented as ASCII "1" (0x31) or "0" (0x30).
- The "vendor number" which seems to be an arbitrary integer
- The "terminal number" which is a value identifying the point of sale terminal
- The encrypted fields length (before padding), or 0
- The encrypted values:
- The string "2"
- The transaction type
- The "sequence number" (described above)
- The "tender number" which describes the account to operate on (UAH flex dollars are 11)
- A timestamp in yyyyMMddHHmmss format (i.e. 20170417015040 is 2017-04-17 01:50:40)
- The string "T" (an unknown boolean field...)
- The transaction amount, in cents, or 0 if not applicable
- The currency ("USD")
- "T" if the card number was manually entered, otherwise "F"
- The student ID / card number
- The student's BbTS PIN, or "0" if there is not one
- The string "0"
The actual request message is sent in this format:
- The length of the message, padded to 4 bytes
- The length of the message is calculated as such: (length of message byte array + length of checksum + 4 (the length of this length field) + 1 (for the field separator that comes after this length field))
- A field separator "~"
- The actual message
- A checksum of the actual message
- CRC16 if the message is encrypted (haven't figured out if it's a standard polynomial or not), length 2
- LRC (xor all bytes together starting with 0), length 1
- There is not a field separator here
Fields are separated by ASCII "~" (0x7e). Field separators are in the encrypted field, and are encrypted along with the rest of the data. There is a field separator after the last element of data.
Example
0068~0~3112~44~0~2~2~1010~11~20170417020627~T~0~USD~F~25244293~0~0~k
This is a balance inquiry for account 25244293 (me). It is 62 bytes long, plus 4 for the length, plus 1 for the separator and 1 for the checksum, making 68 be the final length of the message. It is not encrypted, uses vendor ID 3112 and terminal ID 44, and since it's not encrypted, there is no encrypted field length (it's 0). The arbitrary string "2" comes next, then the string "2" indicating that this is a balance inquiry. We're on sequence number 1010, and would like to query account 11 (flex dollars). The time is 2017-04-17 02:06:27. The unknown boolean field comes next, then the value of the transaction in cents (which is 0 since this is a balance inquiry). Next is the currency, then whether or not this card number was manually entered, and then the actual card number. We finish with the PIN (there isn't one, so just zero), then the unknown field value "0". XORing all the bytes together in the message (not including the length field and field separator) gives us 0x6b = 'k'.
Crypto
- AES with IV of all null bytes and a 16-byte key, no padding (AES/CBC/NoPadding)
- Data to encrypt is right-padded with null bytes to a multiple of 16 bytes
Appendix
Tables
vendor number | organization |
---|---|
2513 | goprint |
3112 | papercut |
3133 | tapingo |
type code | meaning |
---|---|
1 | debit account |
2 | balance inquiry |
3 | "count" (unknown) |
4 | unknown/unused? |
5 | unknown/unused? |
6 | credit account |
7 | query credit limit |
OAuth credentials
- consumer key:
8ecc26d3-55cc-4f3f-8205-77429d1a78b6
- consumer secret:
68f6qW7iwJet7JMBrX59
- authentication mechanism: query string