Fixed issue where the LRC of a card was the reverse of the start
[magread] / carddetect.cpp
1 /*
2     This file is part of MagRead.
3
4     MagRead is free software: you can redistribute it and/or modify
5     it under the terms of the GNU General Public License as published by
6     the Free Software Foundation, either version 3 of the License, or
7     (at your option) any later version.
8
9     MagRead is distributed in the hope that it will be useful,
10     but WITHOUT ANY WARRANTY; without even the implied warranty of
11     MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12     GNU General Public License for more details.
13
14     You should have received a copy of the GNU General Public License
15     along with MagRead.  If not, see <http://www.gnu.org/licenses/>.
16     
17     Written by Jeffrey Malone <ieatlint@tehinterweb.com>
18     http://blog.tehinterweb.com
19 */
20 #include "carddetect.h"
21
22 CardDetect::CardDetect( MagCard *_card ) {
23         aamvaIssuerList();
24         if( _card )
25                 setCard( _card );
26 }
27
28 void CardDetect::setCard( MagCard *_card ) {
29         card = _card;
30         processCard();
31 }
32
33 void CardDetect::processCard() {
34         if( !card )
35                 return;
36         
37         card->type = MagCard::CARD_UNKNOWN;
38         
39         /* Fill in the fields.. */
40         QString expDate;
41         int acctLen = 0;
42         if( card->encoding == ABA && card->charStream.startsWith( ';' ) ) {
43                 acctLen = card->charStream.indexOf( '=' ) - 1;
44                 if( acctLen > 0 ) {
45                         card->accountNumber = card->charStream.mid( 1, acctLen );
46
47                         expDate = card->charStream.mid( acctLen + 2, 4 );
48                         card->miscData = card->charStream.mid( acctLen + 6 ).remove( '?' );
49                 }
50         } else if( card->encoding == IATA && card->charStream.startsWith( "%B" ) ) {
51                 acctLen = card->charStream.indexOf( '^', 1 ) - 2;
52                 if( acctLen > 0 ) {
53                         card->accountNumber = card->charStream.mid( 2, acctLen );
54
55                         card->miscData = card->charStream.section( '^', 2 ).remove( '?' );
56                         expDate = card->miscData.left( 4 );
57                         card->miscData.remove( 0, 4 );
58                 }
59
60         }
61         
62
63         //if the above didn't yield us an account number, just copy the charStream */
64         if( card->accountNumber.isEmpty() ) {
65                 card->accountNumber = card->charStream;
66                 card->charStream.remove( ';' );
67                 card->charStream.remove( '%' );
68                 card->charStream.remove( '?' );
69         }
70
71         if( acctLen )
72                 luhnCheck();
73         
74         if( card->accountValid )
75                 creditCardCheck();
76         
77         /* This code allows partial credit card swipes to be treated as valid, as long at least 3 digits
78          * of extra data that passes checksum are present */
79         if( card->accountValid && !card->swipeValid && card->type & MagCard::CARD_CC ) {
80                 if( 3 > card->miscData.length() || card->miscData.left( 3 ).contains( BAD_CHAR ) ) {
81                         card->accountValid = false;
82                 }
83         }
84
85         if( card->swipeValid && card->type == MagCard::CARD_UNKNOWN )
86                 aamvaCardCheck( expDate );
87         
88         if( !expDate.isEmpty() && card->type != MagCard::CARD_AAMVA ) {
89                 card->expirationDate = QDate::fromString( expDate, "yyMM" );
90                 if( card->expirationDate.year() < 1980 )//qdate defaults to 19nn.  Adjust for this.
91                         card->expirationDate = card->expirationDate.addYears( 100 );
92
93                 if( card->expirationDate.isValid() ) {//Make it so it expires on the last day of the given month
94                         card->expirationDate = card->expirationDate.addDays( card->expirationDate.daysInMonth() - 1);
95                 } else {
96                         card->type = MagCard::CARD_UNKNOWN; // must contain a valid date to be known
97                 }
98         }
99 }
100
101 void CardDetect::aamvaIssuerList() {
102         issuerList[ "636026" ] =  (struct issuer) { "Arizona", "AZ", "L" };
103         issuerList[ "0636021" ] = (struct issuer) { "Arkansas", "AR", "" };
104         issuerList[ "636014" ] =  (struct issuer) { "California", "CA", "L" };
105         issuerList[ "636020" ] =  (struct issuer) { "Colorado", "CO", "NN-NNN-NNNN" };
106         issuerList[ "636010" ] =  (struct issuer) { "Florida", "FL", "LNNN-NNN-NN-NNN-N" };
107         issuerList[ "636018" ] =  (struct issuer) { "Iowa", "IA", "NNNLLNNNN" };
108         issuerList[ "636022" ] =  (struct issuer) { "Kansas", "KS", "KNN-NN-NNNN" };
109         issuerList[ "636007" ] =  (struct issuer) { "Louisiana", "LA", "" };
110         issuerList[ "636003" ] =  (struct issuer) { "Maryland", "MD", "L-NNN-NNN-NNN-NNN" };
111         issuerList[ "636032" ] =  (struct issuer) { "Michigan", "MI", "L NNN NNN NNN NNN" };
112         issuerList[ "636038" ] =  (struct issuer) { "Minnesota", "MN", "L" };
113         issuerList[ "636030" ] =  (struct issuer) { "Missouri", "MO", "L" };
114         issuerList[ "636039" ] =  (struct issuer) { "New Hampshire", "NH", "NNLLLNNNN" };
115         issuerList[ "636009" ] =  (struct issuer) { "New Mexico", "NM", "" };
116         issuerList[ "636023" ] =  (struct issuer) { "Ohio", "OH", "LLNNNNNN" };
117         issuerList[ "636025" ] =  (struct issuer) { "Pennsylvania", "PA", "NN NNN NNN" };
118         issuerList[ "636005" ] =  (struct issuer) { "South Carolina", "SC", "" };
119         issuerList[ "636015" ] =  (struct issuer) { "Texas", "TX", "" };
120         issuerList[ "636024" ] =  (struct issuer) { "Vermont", "VT", "NNNNNNNL" };
121         issuerList[ "636031" ] =  (struct issuer) { "Wisconsin", "WI", "LNNN-NNNN-NNNN-NN" };
122         issuerList[ "636027" ] =  (struct issuer) { "State Dept (USA)", "US-DoS", "" };
123         
124         /* Exceptions:
125          * Arkansas --  Inexplicably, they put a leading 0 on their IIN.  This
126          *              is handled below in the lookup.
127          * Vermont --   I'm told they have additional format to the one listed
128          *              above that is 8 numbers (no letter).  As the two
129          *              formats have different field widths, I automatically
130          *              handle this in the formatting algorithm.
131          */
132
133         //Formatting information is not available for Canada right now
134         issuerList[ "636028" ] =  (struct issuer) { "British Columbia", "BC", "" };
135         issuerList[ "636017" ] =  (struct issuer) { "New Brunswick", "NB", "" };
136         issuerList[ "636016" ] =  (struct issuer) { "Newfoundland", "NL", "" };
137         issuerList[ "636013" ] =  (struct issuer) { "Nova Scotia", "NS", "" };
138         issuerList[ "636012" ] =  (struct issuer) { "Ontario", "ON", "" };
139         issuerList[ "636044" ] =  (struct issuer) { "Saskatchewan", "SK", "" };
140
141         //These may or may not have magstripes.  I'm including them in case they do
142         issuerList[ "604427" ] =  (struct issuer) { "American Samoa", "AS", "" };
143         issuerList[ "636019" ] =  (struct issuer) { "Guam", "GU", "" };
144         issuerList[ "636062" ] =  (struct issuer) { "US Virgin Islands", "US-VI", "" };
145         issuerList[ "636056" ] =  (struct issuer) { "Coahuila", "MX-COA", "" };
146         issuerList[ "636057" ] =  (struct issuer) { "Hidalgo", "MX-HID", "" };
147
148 }
149 void CardDetect::aamvaCardCheck( QString expDate ) {
150         if( card->encoding == IATA )
151                 return; //we're only going to support ABA for now
152         struct issuer issuerInfo;
153
154         QString iin = card->accountNumber.left( 6 );
155
156         issuerInfo = issuerList.value( iin );
157         if( issuerInfo.name.isEmpty() ) {
158                 iin = card->accountNumber.mid( 1, 6 );
159                 issuerInfo = issuerList.value( iin );
160                 if( issuerInfo.name.isEmpty() )
161                         return; // this is not a known AAMVA card, abort
162         }
163
164         card->type = MagCard::CARD_AAMVA;
165
166         card->aamvaIssuer = iin;
167         card->aamvaIssuerName = issuerInfo.name;
168         card->aamvaIssuerAbr = issuerInfo.abbreviation;
169
170         card->accountNumber.remove( iin );
171         if( card->miscData.length() > 8 )
172                 card->accountNumber.append( card->miscData.mid( 8 ) );
173
174         //format the id number if applicable
175         if( !issuerInfo.format.isEmpty() ) {
176                 for( int i = 0; i < issuerInfo.format.length(); i++ ) {
177                         if( issuerInfo.format.at( i ) == 'L' ) {
178                                 if( card->accountNumber.length() - i < 2 )
179                                         continue;
180                                 QChar letter = card->accountNumber.mid( i, 2 ).toInt() + 64;
181                                 if( letter.isLetter() )
182                                         card->accountNumber.replace( i, 2, letter );
183                         } else if( issuerInfo.format.at( i ) != 'N' ) {
184                                 card->accountNumber.insert( i, issuerInfo.format.at( i ) );
185                         }
186                 }
187         }
188
189         //set the birthday
190         QString bday = card->miscData.left( 8 );
191         if( bday.mid( 4, 2 ) > "12" ) { //some (Calif) violate AAMVA standard and switch the exp and bday month values
192                 QString exp = bday.mid( 4, 2 );
193                 bday.replace( 4, 2, expDate.right( 2 ) );
194                 expDate.replace( 2, 2, exp );
195         }
196         card->aamvaBirthday = QDate::fromString( bday, "yyyyMMdd" );
197
198         //set the age
199         card->aamvaAge = QDate::currentDate().year() - card->aamvaBirthday.year();
200         QDate curBday;
201         curBday.setDate( QDate::currentDate().year(), card->aamvaBirthday.month(), card->aamvaBirthday.day() );
202         if( curBday > QDate::currentDate() )
203                 card->aamvaAge--;
204
205         //set the expiration date
206         if( expDate.endsWith( "99" ) ) { // expires on the birth day and month in the given year
207                 expDate.replace( 2, 4, bday.mid( 4, 4 ) );
208                 expDate.prepend( "20" );        
209
210                 card->expirationDate = QDate::fromString( expDate, "yyyyMMdd" );
211         } else if( !expDate.endsWith( "77" ) ) {
212                 if( expDate.endsWith( "88" ) )// expires on last day of the month of birth in given year
213                         expDate.replace( 2, 2, bday.mid( 4, 2 ) );
214
215                 expDate.prepend( "20" );
216                 card->expirationDate = QDate::fromString( expDate, "yyyyMM" );
217                 card->expirationDate.addDays( card->expirationDate.daysInMonth() - 1 );
218         }
219
220 }
221
222 void CardDetect::creditCardCheck() {
223         int acctLen = card->accountNumber.length();
224         if( acctLen == 16 ) {
225                 if( card->accountNumber.startsWith( '4' ) ) {
226                         card->type = MagCard::CARD_VISA;
227                         card->accountIssuer = "Visa";
228                 } else if( card->accountNumber.left( 2 ) >= "51" && card->accountNumber.left( 2 ) <= "55" ) {
229                         card->type = MagCard::CARD_MC;
230                         card->accountIssuer = "MasterCard";
231                 } else if( card->accountNumber.startsWith( "6011" ) || card->accountNumber.startsWith( "65" ) ) {
232                         card->type = MagCard::CARD_DISC;
233                         card->accountIssuer = "Discover";
234                 }
235         } else if( acctLen == 15 ) {
236                 if( card->accountNumber.startsWith( "34" ) || card->accountNumber.startsWith( "37" ) ) {
237                         card->type = MagCard::CARD_CC | MagCard::CARD_AMEX;
238                         card->accountIssuer = "American Express";
239                 }
240         }
241
242         if( card->encoding == IATA && card->type & MagCard::CARD_CC ) {
243                 card->accountHolder = card->charStream.section( '^', 1, 1 ).trimmed();
244                 if( card->accountHolder.contains( '/' ) ) {  // fix the formatting from "LAST/FIRST" on some cards
245                         card->accountHolder = card->accountHolder.section( '/', 1, 1 ) + ' ' + card->accountHolder.section( '/', 0, 0 );
246                 }
247         }
248 }
249
250 /* verifies the card->accountNumber against the luhn algorithm and sets card->accountValid */
251 void CardDetect::luhnCheck() {
252         int total = 0;
253         for( int i = 0; i < card->accountNumber.length(); i++ ) {
254                 if( i & 1 ) { // if odd
255                         total += card->accountNumber.at( i ).digitValue();
256                 } else {
257                         int x = card->accountNumber.at( i ).digitValue() * 2;
258                         total += x % 10;
259                         if( x > 9 )
260                                 total++;
261                 }
262         }
263
264         card->accountValid = !( total % 10 );
265 }