从磁条中解析信用卡输入

25

有人知道如何解析磁条卡读卡器输入的信用卡字符串吗?

我尝试了一个JavaScript解析器,但从未使其正常工作。以下是输入的样子:

%BNNNNNNNNNNNNNNNN^DOE/JOHN
^1210201901000101000100061000000?;NNNNNNNNNNNNNNNN=12102019010106111001?

N代表信用卡号码。


11
也许他想进入信用卡终端业务? - Tor Valamo
2
我妻子是一名摄影师,而我是一名开发者。我们刚刚注册了PayPal网站支付专业版,并希望能够在路上(贸易展览等)处理信用卡。 - Chase Florell
我在 FireFox 上找到了一个 GreaseMonkey 插件,可以让我在 PayPal 虚拟终端向右滑动。虽然不是最理想的解决方案,但如果我无法解决这个问题,它会派上用场。 - Chase Florell
6个回答

26
请参考维基百科关于磁条卡的条目

Track one,格式B:

  • 起始哨兵——一个字符(通常为“%”)
  • 格式代码="B"——一个字符(仅为字母)
  • 主帐号(PAN)——最多19个字符。通常与信用卡正面印刷的卡号相匹配,但不总是
  • 字段分隔符——一个字符(通常为“^”)
  • 姓名——两到26个字符
  • 字段分隔符——一个字符(通常为“^”)
  • 过期日期——YYMM格式的4个字符
  • 服务代码——三个字符
  • 自由数据——可能包括Pin Verification Key Indicator(PVKI,1个字符)、PIN Verification Value(PVV,4个字符)、Card Verification Value或Card Verification Code(CVV或CVK,3个字符)
  • 结束哨兵——一个字符(通常为“?”)
  • 纵向冗余校验(LRC)——一个字符(大多数读卡器设备在刷卡到表示层时不返回此值,并且仅在内部验证输入时使用它)

我希望数据是假的,否则任何人都可以获取:

  • 姓名
  • 过期日期
  • CVV

我不确定但我认为信用卡号码(或可能性的#)可以使用LRC计算。


如果你正在写入磁条读卡器,并且使用原始写入模式,那么计算LRC算法可能会有点困惑,你必须自己完成。如果你使用正常的写入模式,刷卡机会替你处理。但是对于读取操作,刷卡机将自动计算LRC以验证刷卡是否成功。 - t0mm13b
CVV不会显示在磁道上 - 它是否可以计算? - quantumpotato
CVV不在磁道中,这是有意的。网络知道它,并用于验证交易发生时卡片是否存在。 - Chris Betti

8
我给你做得更好:我制作了一个视频,展示如何使用ASP.Net/c#来完成这个功能:

http://www.markhagan.me/Samples/CreditCardSwipeMagneticStripProcessing

以下是您可能关心的代码部分:
    protected void CardReader_OTC(object sender, EventArgs e)
    {
        bool CaretPresent = false;
        bool EqualPresent = false;

        CaretPresent = CardReader.Text.Contains("^");
        EqualPresent = CardReader.Text.Contains("=");

        if (CaretPresent)
        {
            string[] CardData = CardReader.Text.Split('^');
            //B1234123412341234^CardUser/John^030510100000019301000000877000000?

            PersonName.Text = FormatName(CardData[1]);
            CardNumber.Text = FormatCardNumber(CardData[0]);
            CardExpiration.Text = CardData[2].Substring(2, 2) + "/" + CardData[2].Substring(0, 2);
        }
        else if (EqualPresent)
        {
            string[] CardData = CardReader.Text.Split('=');
            //1234123412341234=0305101193010877?

            CardNumber.Text = FormatCardNumber(CardData[0]);
            CardExpiration.Text = CardData[1].Substring(2, 2) + "/" + CardData[1].Substring(0, 2);
        }
    }

完整的代码在我上面链接的那个网站上。

2

据我所记:

磁条数据有两个轨道-第一轨以%开头,以?结尾,第二轨以;开头,以?结尾。这些是起始/结束标记。

第一轨是字母数字混合的,第二轨是数字的,还有第三轨也是数字的(如果我的记忆正确的话)。

起始/结束标记之间的数据可以根据磁条的记录密度而变化。密度越高,则可以在一个轨道上记录更多的数据。

使用正则表达式来获取数据可能不是一种可靠的方法来提取所需的信息。

并非所有的信用卡都只有两个轨道,有些使用了三个轨道。


1

这是我的代码:

首先监听获取数据... 这些数据需要验证,我正在寻求帮助。良好的滑动效果很好,但是不良的滑动会导致解析器出错。

$('#cc-dialog-form').keypress(function(e) 
{

    var charCode = e.which;
    //ie? evt = e || window.event;
    track_start = '%';
    finished = false;
    timeout = 100;
    track_start_code = track_start.charCodeAt(0);
    //console.log('Track_start_code: ' + track_start_code);

    //console.log('keycode ' + e.keycode);


    //console.log('charcode ' + charCode);
    //console.log('track_start_code ' + track_start_code);
    if (charCode == track_start_code)
    {
        collect_track_data = true;
            $('#offline_cc_entry').hide();
            $('#cc_online').hide();
            $('#Manual_CC_DATA').hide();
            $('#cc_loading_image').show();      

    }
    if (collect_track_data)
    {   
        if (charCode == $.ui.keyCode.ENTER) 
        {
            //all done
            //console.log( card_data);
            collect_track_data = false;
            $('#cc_loading_image').hide();
            $('#Manual_CC_DATA').show();
            //console.log("Track Data: " + card_data);


            process_swipe_cc_payment(card_data);
            card_data = '';

        }
        else
        {
            card_data = card_data + String.fromCharCode(charCode);
            console.log(card_data);
            if (e.preventDefault) e.preventDefault();
            e.returnValue=false;
            return false;
        }
    }
    else
    {
        //i am guessing this will be regular input?
        if (charCode == $.ui.keyCode.ENTER) 
        {
             process_keyed_or_offline_CC_payment();
        }
    }
    //console.log("which: " + e.which);
    //console.log("keyCode: " + e.keyCode);
    //track and collect data here?

});

以下是解析器...请注意,我将它们全部放在一个函数中,这样可以销毁所有的变量,以免在浏览器中滞留。

    parse_data = true;
if (parse_data)
{

var parsed_card_data = {};
parsed_card_data['card_data'] = card_data;
var tracks = card_data.split("?");

//console.log ("tracks");
//console.log (tracks);
parsed_card_data['track1'] = tracks[0];
parsed_card_data['track2'] = tracks[1];
//if there is a third track we might find it under tracks[2]

//splitting the card data OPTION 1

var track1_parsed = tracks[0].split("^");

//console.log (track1_parsed);



//track1 data....
var card_number_track1 = track1_parsed[0].substring(2);


parsed_card_data['card_number_track1'] = card_number_track1;

var details2_1 = tracks[1].split(";");
details2_1 = details2_1[1].split("=");


var exp_date_track_1 = details2_1[1];
exp_date_track_1 = exp_date_track_1.substring(0, exp_date_track_1.length - 1);
exp_date_track_1 = exp_date_track_1.substring(2, 4) + "/" + exp_date_track_1.substring(0,2);
parsed_card_data['exp_track1'] = exp_date_track_1;



//now check if track one matches track 2...

track2_parsed = tracks[1].split("=");


card_number_track_2 = track2_parsed[0].substring(1);



parsed_card_data['card_number_track_2'] = card_number_track_2;
exp_date_track_2 = track2_parsed[1].substring(0,4);
exp_date_track_2 = exp_date_track_2.substring(2, 4) + "/" + exp_date_track_2.substring(0,2);
parsed_card_data['exp_date_track_2'] = exp_date_track_2;


var primary_account_number =  card_number_track1.substring(0,1);


if(card_number_track1 == card_number_track_2 &&  exp_date_track_1 == exp_date_track_2)
{
        //now make a security feature showing the last 4 digits only....
    parsed_card_data['secure_card_number'] = "xxxx " + card_number_track1.substring(card_number_track1.length-4, card_number_track1.length);




    if(card_number_track1.length == 15)
    {
        parsed_card_data['card_type'] = "American Express"; 
    }
    else if(primary_account_number == 4)
    {
        parsed_card_data['card_type'] = "Visa";
    }
    else if(primary_account_number == 5)
    {
        parsed_card_data['card_type'] = "Master Card";
    }
    else if(primary_account_number == 6)
    {
        parsed_card_data['card_type'] = "Discover";
    }
    else
    {
        parsed_card_data['card_type'] = false;
    }

    var names_1 = track1_parsed[1].split("/");
    parsed_card_data['first_name'] = names_1[1].trim();
    parsed_card_data['last_name'] = names_1[0].trim();


    //console.log("return Data");
    //console.log(return_data);

}
else
{
    parsed_card_data = false;
}

    //zero out the variables...

    tracks = '';
    track1_parsed = '';
    card_number_track1 = '';
    details2_1 = '';
    exp_date_track_1 = '';
    track2_parsed = '';
    card_number_track_2 = '';
    exp_date_track_2 = '';
    primary_account_number = '';
}

if(parsed_card_data)
{
    //console.log(parsed_card_data);
    $('#card_type').val(parsed_card_data['card_type']);
    $('#credit_card_number').val(parsed_card_data['secure_card_number']);
    $('#expiration').val(parsed_card_data['exp']);
    $('#card_holder').val(parsed_card_data['first_name']+ " " + parsed_card_data['last_name']);

    //parsed_card_data['track1'] is basically what we want???

    $('#CC_SWIPE_INSTRUCTIONS').hide();
    $('#CC_DATA').hide();
    $('#cc_loading_image').show();



    var post_string = {};
    post_string['ajax_request'] = 'CREDIT_CARD_PAYMENT';
    post_string['amount'] = $('#cc_input').val();
    post_string['card_data'] = parsed_card_data;
    post_string['pos_sales_invoice_id'] = pos_sales_invoice_id;
    post_string['pos_payment_gateway_id'] = $('#pos_payment_gateway_id').val();
    post_string['line'] = 'online';
    post_string['swipe'] = 'swipe';

    card_data = '';
                parsed_card_data = {};
    var url = 'ajax_requests.php';
    $.ajax({
            type: 'POST',
            url: url,
            data: post_string,
            async: true,
            success:    function(response) 
            {
                $('#cc_loading_image').hide();
                console.log(response);
                $('#CC_RESPONSE').show();
                $('#CC_RESPONSE').html(response);
                //here we would update the payment table - currently we will just refresh

                post_string = '';

            }
            });
    post_string = '';
}
else
{
    //error
    alert("Read Error");
    $( "#cc-dialog-form" ).dialog( "close" );
}

我添加了if(card_data.indexOf('=') === -1 || card_data.indexOf('^')===-1)来验证刷卡数据,这可以消除错误的刷卡,但这并不完全符合auth.net要求的纵向冗余校验:“必须为从轨道读取的数据计算纵向冗余校验(LRC),并将其与从轨道读取的LRC进行比较。当未检测到字符奇偶校验错误且计算和读取的LRC匹配时,假定已无误地读取轨道数据。”-卡片现场指南 - Iannazzi

1
通常对于非面对面交易(即MOTO交易),您需要信用卡号码,到期日和可能的CVV(也称为CVC2等)。您可以从刷卡器中获取前两个,因为这是轨道数据。CVV打印在卡上。
卡上的姓名并不那么重要。除非您的收单机构和持卡人正在使用地址验证,但您可以在^^之间找到它,它可能具有空格填充,您可以删除它。
您想要的部分是track2 NNNNNNNNNNNNNNNN=1210,其中NNNNN = 卡号PAN,1210 = 到期日期。
即使track1为空(有时它不会被处理),您仍将获得;?,因此您可以使用第二个;的索引作为字符串的起始点,并使用=作为cc#字符串的结束点。使用=后的4个字符作为到期日。
我建议让持卡人签署某些记录交易的文件,否则他们可能会争论该卡并进行退款。

并非所有信用卡都有两个轨道,有些使用三个轨道。

只有track2用于处理,并且具有标准化格式。

借记卡通常无法处理(除非它们具有 Visa 借记卡或类似卡)。

P.S. 不应以明文形式存储信用卡数据,因此请尽量将所有内容保存在内存中或进行强加密。


1

第一个链接是我到处寻找的东西。它似乎具有轨道数据验证,甚至还有超时功能。幸运或不幸的是,这是一段相当令人印象深刻的代码,超出了我的实现能力。你能否添加一个如何使用CardReader函数的示例?在我的磁道读取代码中,我有一个监听器,在CardReader中没有看到。 - Iannazzi
刚刚在下面发布了我的轨道读取器和解析器代码...需要集成CardReader验证。 - Iannazzi

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接