Search code examples
androidnfcgoogle-paygoogle-wallet

Generate and Read NFC Smart Tap Generic Pass in Google Wallet


I'm trying to integrate and use Google Wallet (API and Android SDK) for generating a Generic Pass with Smart Tap enabled, read it using another mobile phone and decrypt the payload.

PRE:

  1. Given that there is already a google provided sample: https://github.com/google-pay/smart-tap-sample-app
  2. The given sample works on my mobile phone reading NFC demo pass from google (see README link of the github project)
  3. Given that, at the url https://pub1.pskt.io/c/gn1v07 is is possible to generate random NFC Passes that have publicly available required info (CollectorID, Private Key, Public Key)
  4. Given that with pskt passes (3) and sample app (1) i can read those passes

I can't read my custom passes, or, to be precise, I don't receive the correct payload.

My current passes are in "Demo Mode", because I want to be sure they work before ask for publishing. The google documentation is not clear wether or not this is a blocker or what to do in these case. I tried to upload the pskt (3) public key (with a different version) in my wallet console, but still doesn't work. The collector ID is the one I have in my google wallet console (converted to byte) and my issuerID has been added to redemptionIssuers. it seems that smartTapRedemptionValue is not written in the tag, but if I query wallet API it is there:

{
  "cardTitle": {
    "defaultValue": {
      "kind": "walletobjects#translatedString",
      "language": "it",
      "value": "$TITLE"
    },
    "kind": "walletobjects#localizedString"
  },
  "classId": "$ISSUER_ID.$CLASS_NAME",
  "genericType": "genericTypeUnspecified",
  "hasUsers": true,
  "header": {
    "defaultValue": {
      "kind": "walletobjects#translatedString",
      "language": "it",
      "value": "$HEADER"
    },
    "kind": "walletobjects#localizedString"
  },  
  "hexBackgroundColor": "#ffffff",
  "id": "$ISSUER_ID.newPassObject3",
  "smartTapRedemptionValue": "if_you_read_this_it's_great!",
  "state": "active",
  "subheader": {
    "defaultValue": {
      "kind": "walletobjects#translatedString",
      "language": "it",
      "value": "$SUBHEADER"
    },
    "kind": "walletobjects#localizedString"
  }
}

If anyone has any clue, thanks for the help!


Solution

  • I found what was the issue, and it wasn't an issue of the pass.

    TL:DR -> Sample app is partial and full of missing pieces.

    Digging in documentation and various reverse engineering, I found a 'Service Type' code usage, that was hardcoded in the app: https://github.com/google-pay/smart-tap-sample-app/blob/192d1760bd8f44e8142dda6611c2a1314b35595b/app/src/main/java/com/google/smarttapsample/GetDataCommand.java#L37

    Here is set to Loyalty Cards (0x03). If you want to make it work with generic passes, you have to put Generic (0x12). In addition, there is an update to GetDataResponse method getDecryptedPayload: https://github.com/google-pay/smart-tap-sample-app/blob/192d1760bd8f44e8142dda6611c2a1314b35595b/app/src/main/java/com/google/smarttapsample/GetDataResponse.java#L258

    You should add an "else if" for managing generic cards:

    // Iterate over service NDEF records
    for (NdefRecord serviceRecord : serviceNdefRecord.getRecords()) {
        // Check for `ly` type.   
        if (Arrays.equals(serviceRecord.getType(), new byte[]{(byte) 0x6c, (byte) 0x79})) {
            //.... processLoyaltyServiceRecord(serviceRecord);
        } else if (Arrays.equals(serviceRecord.getType(), new byte[]{(byte) 103, (byte) 114})) {
            processGenericServiceRecord(serviceRecord);
        }
    }
    
        private void processGenericServiceRecord(NdefRecord serviceRecord) throws FormatException {
            //in case of general pass `gr` type
            // Get the generic record payload
            NdefMessage genericRecordPayload = new NdefMessage(serviceRecord.getPayload());
            for (NdefRecord generic : genericRecordPayload.getRecords()) {
                // Check for `n` ID = 6e
                if (Arrays.equals(generic.getId(), new byte[]{(byte) 0x6e})) {
                    // Get the Smart Tap redemption value
                    decryptedSmartTapRedemptionValue = new String(Arrays.copyOfRange(generic.getPayload(), 1, generic.getPayload().length));
                }
            }
        }
    

    And now reading the pass works correctly!