encryptionpaymentpci-compliancepci-dssrncryptor

How to store Credit Card info on iOS / Android in a PCI-compliant manner


I'm building a mobile app that accepts payments. The user enters their CC details, and the payment info is submitted to a retailer's POS system over HTTPS. The POS processes the payment directly and needs the actual credit card info to work, therefore we cannot use services like Stripe that will store the card, and give us back a token to process payments.

Because of the nature of the app, users will be making regular payments and so I want to store their CC info for convenience. However this is not recurring billing, the user will be initiating the transaction at will. Therefore I don't need to keep CC centrally on a server and I'm considering storing individual cards on each user's device using this method:

The idea being, that if the user knows the CVC they likely possess the card anyway, hence they don't need to try hacking the device.

For encryption, I'm considering using the RNCryptor lib. One of its primary features is the automatic conversion of common passwords to cryptographically 'random' byte sequences of two 256-bit keys, for encryption and authentication. The key stretching is implemented through 10k rounds of PBKDF2. The implementation details are in the link, but in short:


Questions:

I don't understand the math well-enough to judge if seeding RNCryptor's key-stretching implementation with just the 3 CVC digits, would produce a statistically random enough key. I haven't been able to find any docs on what password specifications are required for RNCryptor to stay secure. Any thoughts on that would be appreciated. Using this lib is as simple as this:

// Encryption
NSData *data = ...
NSString *password = @"Secret password";
NSData *ciphertext = [RNCryptor encryptData:data password:password];

// Decryption
NSError *error = nil;
NSData *plaintext = [RNCryptor decryptData:ciphertext password:password error:&error];
if (error != nil) {
    NSLog(@"ERROR:%@", error);
    return
}
// ...

When storying the AES256 encrypted CC info on iOS keychain (or droid equivalent), does it matter if there is no device lock passcode enabled? My thinking is, the info is already AES256 encrypted, it could be stored on the device without keychain's encryption anyway?

What sections of PCI compliance are relevant in this case, given that there is no central server storing lots of CC numbers? I tried reading the PCI specs but the docs are a maze to navigate :(


Solution

    1. As Ebbe points out using just a CVC as the key, even derived from the CVC with PBKDF2 is not secure, there are only 1000 possible keys. Something else must be included in the key.

    2. The date acts as a partial crib since it has a known format and limited values. Also be careful not to include any other cribs such as field separators or field indicators.

    3. The Credit Card Account Number Verification Check Digit is also a crib.

    4. In order for the Keychain to be secure the user must have entered a device lock passcode.

    5. Jailbroken and rooted devices must be avoided and that is difficult to determine.

    6. As long as only the CC# and expiration date are saved, not any track 2 data and it is encrypted to PCI standards you should be OK.

    7. See the PCI Point to Point Encryption Standard, it is free on the PCI site. See Table 2, Application Developer.

    8. Finally get a PCI auditor to review your scheme, this is just "best information" with the information provided and without a through evaluation.

    Update based on the OP's comment:

    There are 1000 CVCs to try. RNCryptor will take ~200ms per CVC try, that means all 1000 can be tried in ~4 minutes—ouch. RNCryptor has authentication so it will be immediacy known when the correct CVC is tried. This not not secure in the least and the authentication is working against you.

    Without authentication then cribs need to be relied on. The first crib is the check digit, that will rule out ~900 CVCs leaving 100.

    But is is is really worse depending the format that is encrypted. Decryption with an incorrect key will return essentially random bytes. If the CC# and date are a string the odds of the result being a digit string are virtually zip so again a correct decryption will be immediately known. The best bet is to convert the CC# to a large integer and the date to a numeric day+year and encrypt this. But even then there is the check digit crib and a valid date to use to validate the decryption. It is actually more secure to not include the expiration date in the encryption.

    In the end using a CVC as the key will not be secure, a much longer key is needed.

    The Keychain does not protect its contents from the device owner and the device owner in reality is whoever has access, the passcode is what determined access and thus the device owner.