Add COPYING licence file (GPLv3)
[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 expiration date
199         if( expDate.endsWith( "99" ) ) { // expires on the birth day and month in the given year
200                 expDate.replace( 2, 4, bday.mid( 4, 4 ) );
201                 expDate.prepend( "20" );        
202
203                 card->expirationDate = QDate::fromString( expDate, "yyyyMMdd" );
204         } else if( !expDate.endsWith( "77" ) ) {
205                 if( expDate.endsWith( "88" ) )// expires on last day of the month of birth in given year
206                         expDate.replace( 2, 2, bday.mid( 4, 2 ) );
207
208                 expDate.prepend( "20" );
209                 card->expirationDate = QDate::fromString( expDate, "yyyyMM" );
210                 card->expirationDate.addDays( card->expirationDate.daysInMonth() - 1 );
211         }
212
213 }
214
215 void CardDetect::creditCardCheck() {
216         int acctLen = card->accountNumber.length();
217         if( acctLen == 16 ) {
218                 if( card->accountNumber.startsWith( '4' ) ) {
219                         card->type = MagCard::CARD_VISA;
220                         card->accountIssuer = "Visa";
221                 } else if( card->accountNumber.left( 2 ) >= "51" && card->accountNumber.left( 2 ) <= "55" ) {
222                         card->type = MagCard::CARD_MC;
223                         card->accountIssuer = "MasterCard";
224                 } else if( card->accountNumber.startsWith( "6011" ) || card->accountNumber.startsWith( "65" ) ) {
225                         card->type = MagCard::CARD_DISC;
226                         card->accountIssuer = "Discover";
227                 }
228         } else if( acctLen == 15 ) {
229                 if( card->accountNumber.startsWith( "34" ) || card->accountNumber.startsWith( "37" ) ) {
230                         card->type = MagCard::CARD_CC | MagCard::CARD_AMEX;
231                         card->accountIssuer = "American Express";
232                 }
233         }
234
235         if( card->encoding == IATA && card->type & MagCard::CARD_CC ) {
236                 card->accountHolder = card->charStream.section( '^', 1, 1 ).trimmed();
237                 if( card->accountHolder.contains( '/' ) ) {  // fix the formatting from "LAST/FIRST" on some cards
238                         card->accountHolder = card->accountHolder.section( '/', 1, 1 ) + ' ' + card->accountHolder.section( '/', 0, 0 );
239                 }
240         }
241 }
242
243 /* verifies the card->accountNumber against the luhn algorithm and sets card->accountValid */
244 void CardDetect::luhnCheck() {
245         int total = 0;
246         for( int i = 0; i < card->accountNumber.length(); i++ ) {
247                 if( i & 1 ) { // if odd
248                         total += card->accountNumber.at( i ).digitValue();
249                 } else {
250                         int x = card->accountNumber.at( i ).digitValue() * 2;
251                         total += x % 10;
252                         if( x > 9 )
253                                 total++;
254                 }
255         }
256
257         card->accountValid = !( total % 10 );
258 }