Rp SDK is an App2App SDK for Samsung Wallet driver's license service online scenarios.
This SDK provides an implementation for direct communication between the Samsung Wallet and Partner applications.
binding.button.setOnClickListener {
rpClientApis.request("com.samsung.android.spay", UUID.randomUUID().toString(), appLink, object : RpClientApis.OnResponseListener {
override fun onGetMdocRequestData(deviceEngagementBytes: ByteArray): ByteArray? {
Log.i(TAG, "onGetMdocRequestData($deviceEngagementBytes)")
/**
* 1. prepare mDoc request data (ISO-18013-5)
* 2. build sessionEstablishmentBytes (ISO-18013-5)
* 3. Encrypt it with HKDF (ISO-18013-5, 9.1.1.5 Cryptographic operations)
**/
return "encryptedSessionEstablishmentBytes"
}
override fun onMdocResponse(encryptedResponse: ByteArray) {
Log.i(TAG, "onMdocResponse($encryptedResponse)")
/**
* 1. Decrypt it with HKDF (ISO-18013-5, 9.1.1.5 Cryptographic operations)
* 2. CBOR decode it
**/
}
override fun onMdocResponseFailed(exception: Exception) {
Log.i(TAG, "onMdocResponseFailed($exception)")
}
})
}
Error Code Explanation
The below exceptions might occur through the onMdocResponseFailed callback
Exceptions name
Description
RpCommunicationException
This error occurs when the data requested by the listener is incorrect.
RpConnectionFailedException
This occurs when the App 2 App communication between apps is not working. This usually occurs when the target Package Name is written incorrectly
Web2App API Integration Specs
The API specifications that need to be implemented by the RP partner are described below:
Called by Samsung to the RP partner
Send Key
Send the Wallet application key info and return the data field types requested to the client for authentication of the mDL
[Request]
Type
Value
Description
Method
POST
URL
{Partner server URL}/rp/v1.0/{cardId}/{RefId}/key
Headers
Authorization String(1024)
(Required) Credential token. The token can have the prefix "Bearer" as an authorization type, e.g., Bearer <credentials>. * Refer to Authorization Token for more details.
Path Parameters
cardId String(32)
(Required) Wallet card identifier issued from Partner portal when the partner manager signs up for partner services and registers the wallet card they want to service.
refId String(32)
(Required) Unique content identifier defined by the content provider
Query Parameter
N/A
Payload
data String(3000)
(Required) JWT data encrypted with the public key information and card type. If decrypted this data is decoded, and it has the following format information. { “data”: “XXXXXXXXXXX”, “card”: { "type": "relyingparty", "subType": "others", "designType": "us-01" } }
[Example]
POST {Partner server URL}/rp/v1.0/{cardId}/{RefId}/key
Content-Type: application/json
{
“data”: “eyJjdHkiOiJBVVRIIiwidmVyIjoiMiIsInBhcnRuZXJJZCI6InRlc3QiLCJ1dGMiOjE3MTYyMDYzNjAxMTAsImFsZyI6IlJTMjU2In0.ZXlKbGJtTWlPaUpCTVRJNFIwTk5JaXdpWVd4bklqb2lVbE5CTFU5QlJWQXRNalUySW4wLlZ5aFAxS0FnMVJHbzBDN2NIX2pYdGtfODdQbnhrRmpfWkpPcnNSUUs4MnN0OWVxTjEyVzVMOEJaX1d5NGVzMzE3VDNad0pncmpwZWdZOEk3aVlCWWRlOGJ5LXFiMjBLU3RUc3JsSzlPSlFnN1FaM2xZaUxscXlTb0VlbERvd0FPaTRMRy1JUkZWdVlrbXRiNTg3UTd1ZWNuQ1lWWGZWalVEcG01YXBFbDV3SzM1UGZ3d0dkREM2TmowZ1AwbTZ3Nk1kdl9mdDBvZWc2MWZjaGdBYnY0emxMZjU2cVYzM0t6ZjdjbWVpbkJRNnpMSGUtYmFWYXhVZk5Ld2htZWVjUzFTV3laRm1NVlJ6MEFsMnBxa0dQLVJkT1Iza3VzaVo0VjFIdy1aQ2IyVWVwYVdZRU9nUEdrVW1mbTFuOWJWT1ZmZ1NUV1F0SE5pVTFJYVRHTG1DWlpVQS5PMzZrd1g4WmJnQ21wd3o2Ll9KZEhFVXNnbm13b1drdDRMcU4xMUNCaUNTSnUtbWpYV2ZrckxoS0ZVenBsS085ckdXbUdPZ0pqUkF1NTFsOTRYc2VIVWdfWU9NS2RGR1VOMWJhMHB3Y0tFNGtJMEt2dkFOWHprODN0azBjQzROT2F6VzlmOVNTT0RhMU9IMEFoaVFzQzdDeVFqNndNLWFlVk8waEJwSEJkMEdURUh1Z3Exc21vVmxRbjBlSnJqWHM4X3FwcnpLekwtaDFPcFk1aEs1ZUg5Q3NiSms0aEhCNGNmWUlKRUJFZ09BcGZxcGFuMGFSVGFmODhhdXlqSGZHdGRMa0tLWDV0Q0RTajIxSE5TT0FhWTJVWlZrR0hxU0wzNGJabTU5aEZMNVdHa0lJcE9BMHlWUE9tQzNWTFlKV2JsMm85LkFoeDBVYTVGeTZudkxKVXVkeTAzSHc.E07YYl7IoR3885VYKsS5_q1IcpX750uU2Ge5suJSedx3Dr_U0x4tSe9_0NxM46dyWnFUXrUAGfjDnjHIBc707Li9VI3XtyiHwnwEiFydgv1QB9oddkYyZuahXQmJHVUqNcdt6DF2CAqzF5QgMVqfMGSE_t7IPU8vQFXE34DO-sKzj8ftdusS2EcdANBqOKCHih3m39NouBPFhcX68PlPcW50diXlupxwEGniU2t3Co24YlIaKLGac669aCcXDQr34utVUqhTJt_FTXkahAlzoA34_Hj_s82FivIXh1ITD74UOjZSe7IBWya_kVysoZavnmzTz2tH9CbwyCvx8wA”
}
[Response]
Type
Value
Description
HTTP Status code
200 OK
Payload
data String(3000)
(Required) JWT data encrypted with the data field types requested to the client for authentication of the mDL.
[Result]
HTTP status code
Description
200 OK
Success
400 Bad Request
Requests cannot or will not be processed due to something that is perceived to be a client error.
401 Unauthorized
Authorization token is invalid or expired.
500 Internal Server Error
The server encountered an unexpected condition that prevented it from fulfilling the request.
503 Service Unavailable
The server is not ready to handle the request.
Send authentication data
The data is encrypted according to the requested data and then transmitted along with the data card information.
[Request]
Type
Value
Description
Method
POST
URL
{Partner server URL}/rp/v1.0/{cardId}/{RefId}/auth
Headers
Authorization String(1024)
(Required) Credential token. The token can have the prefix "Bearer" as an authorization type, e.g., Bearer <credentials>. * Refer to Authorization Token for more details.
PathParameters
cardId String(32)
(Required) Wallet card identifier issued from Partner portal when the partner manager signs up for partner services and registers the wallet card they want to service.
refId String(32)
(Required) Unique content identifier defined by the content provider
QueryParameter
N/A
Payload
data String(3000)
(Required) JWT data encrypted with the public key information and card type. If decrypted this data is decoded, it has the following format information. { “data”: “XXXXXXXXXXX”, “card”: { "type": "idcard", "subType": "drivers", "designType": "us-01" } }
[Example]
POST {Partner server URL}/rp/v1.0/{cardId}/{RefId}/auth
Content-Type: application/json
{
“data”: “eyJjdHkiOiJBVVRIIiwidmVyIjoiMiIsInBhcnRuZXJJZCI6InRlc3QiLCJ1dGMiOjE3MTYyMDYzNjAxMTAsImFsZyI6IlJTMjU2In0.ZXlKbGJtTWlPaUpCTVRJNFIwTk5JaXdpWVd4bklqb2lVbE5CTFU5QlJWQXRNalUySW4wLlZ5aFAxS0FnMVJHbzBDN2NIX2pYdGtfODdQbnhrRmpfWkpPcnNSUUs4MnN0OWVxTjEyVzVMOEJaX1d5NGVzMzE3VDNad0pncmpwZWdZOEk3aVlCWWRlOGJ5LXFiMjBLU3RUc3JsSzlPSlFnN1FaM2xZaUxscXlTb0VlbERvd0FPaTRMRy1JUkZWdVlrbXRiNTg3UTd1ZWNuQ1lWWGZWalVEcG01YXBFbDV3SzM1UGZ3d0dkREM2TmowZ1AwbTZ3Nk1kdl9mdDBvZWc2MWZjaGdBYnY0emxMZjU2cVYzM0t6ZjdjbWVpbkJRNnpMSGUtYmFWYXhVZk5Ld2htZWVjUzFTV3laRm1NVlJ6MEFsMnBxa0dQLVJkT1Iza3VzaVo0VjFIdy1aQ2IyVWVwYVdZRU9nUEdrVW1mbTFuOWJWT1ZmZ1NUV1F0SE5pVTFJYVRHTG1DWlpVQS5PMzZrd1g4WmJnQ21wd3o2Ll9KZEhFVXNnbm13b1drdDRMcU4xMUNCaUNTSnUtbWpYV2ZrckxoS0ZVenBsS085ckdXbUdPZ0pqUkF1NTFsOTRYc2VIVWdfWU9NS2RGR1VOMWJhMHB3Y0tFNGtJMEt2dkFOWHprODN0azBjQzROT2F6VzlmOVNTT0RhMU9IMEFoaVFzQzdDeVFqNndNLWFlVk8waEJwSEJkMEdURUh1Z3Exc21vVmxRbjBlSnJqWHM4X3FwcnpLekwtaDFPcFk1aEs1ZUg5Q3NiSms0aEhCNGNmWUlKRUJFZ09BcGZxcGFuMGFSVGFmODhhdXlqSGZHdGRMa0tLWDV0Q0RTajIxSE5TT0FhWTJVWlZrR0hxU0wzNGJabTU5aEZMNVdHa0lJcE9BMHlWUE9tQzNWTFlKV2JsMm85LkFoeDBVYTVGeTZudkxKVXVkeTAzSHc.E07YYl7IoR3885VYKsS5_q1IcpX750uU2Ge5suJSedx3Dr_U0x4tSe9_0NxM46dyWnFUXrUAGfjDnjHIBc707Li9VI3XtyiHwnwEiFydgv1QB9oddkYyZuahXQmJHVUqNcdt6DF2CAqzF5QgMVqfMGSE_t7IPU8vQFXE34DO-sKzj8ftdusS2EcdANBqOKCHih3m39NouBPFhcX68PlPcW50diXlupxwEGniU2t3Co24YlIaKLGac669aCcXDQr34utVUqhTJt_FTXkahAlzoA34_Hj_s82FivIXh1ITD74UOjZSe7IBWya_kVysoZavnmzTz2tH9CbwyCvx8wA”
}
[Response]
Type
Value
Description
HTTP Status code
200 OK 400 Bad Request
[Result]
HTTP status code
Description
200 OK
Success
400 Bad Request
Requests cannot or will not be processed due to something that is perceived to be a client error.
401 Unauthorized
Authorization token is invalid or expired.
500 Internal Server Error
The server encountered an unexpected condition that prevented it from fulfilling the request.
503 Service Unavailable
The server is not ready to handle the request.
Code explanation based on the sample code
JWT (JWS + JWE) decryption between the Wallet Backed Server and partner server
1. Verify by generateing a JWS using the body data
// Generate JWS by the body data
private static SignedJWT parseJWT(final String data) {
try {
return SignedJWT.parse(data);
} catch (ParseException e) {
log.error("parserJWT error class : {}, error message : {}", e.getClass(), e.getMessage());
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "parserJWT error");
}
}
// Verify JWS using Samsung Public key
public RequestBody getRequestBody(final KeyRing keyRing) {
final SignedJWT signedJWT = JWTUtils.verify(keyRing.getTargetPublicKey(), encryptedData, 60 * 10000); // Verify and generate JWS
try {
final String strBody = JWTUtils.getDecryptedPayloadFrom(keyRing.getSourcePrivateKey(), JWEObject.parse(signedJWT.getPayload().toString())); // Decryption JWE by the JWS
return objectMapper.readValue(strBody, RequestBody.class); // Convert to data format requested by client
} catch (ParseException | JsonProcessingException e) {
log.error("getRequestBody : {}", e.getMessage());
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "data body parse error");
}
}
public RequestBody getRequestBody(final KeyRing keyRing) {
final SignedJWT signedJWT = JWTUtils.verify(keyRing.getTargetPublicKey(), encryptedData, 60 * 10000); // Verify and generate JWS
try {
final String strBody = JWTUtils.getDecryptedPayloadFrom(keyRing.getSourcePrivateKey(), JWEObject.parse(signedJWT.getPayload().toString())); // Decryption JWE by the JWS
return objectMapper.readValue(strBody, RequestBody.class); // Convert to data format requested by client
} catch (ParseException | JsonProcessingException e) {
log.error("getRequestBody : {}", e.getMessage());
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "data body parse error");
}
}
Generate MdocEstablishment
1. Generate RSA Key per refId
public class TransactionContext {
private final KeyPair keyPair; // RSA Key
private final byte[] clientEngagement; // Body data received through key API, base64URL decoded value
@EqualsAndHashCode.Exclude
private int encryptMessageCounter = 0; // Count value when encrypted
@EqualsAndHashCode.Exclude
private int decryptMessageCounter = 0; // Count value when decrypted
}
private Cache<String, TransactionContext> contextCache; // RSA key management per refid with memory cache
// Generate and store RSA Key per refId only once upon first request
public TransactionContext setTransactionContext(final String key, final String base64EncodedClientEngagement) {
log.info("base64EncodedClientPublicKey : {}", base64EncodedClientEngagement);
this.contextCache.put(key, new TransactionContext(KeyUtils.generateKeyPair() , Base64Utils.decode(base64EncodedClientEngagement.getBytes())));
return this.getTransactionContextBy(key);
}
// Part of retrieving RAS Key based on refId
public TransactionContext getTransactionContextBy(final String key) {
return Optional.ofNullable(this.contextCache.getIfPresent(key)).orElseThrow(() -> {
log.info("{} is empty", key);
return new CustomException(HttpStatus.BAD_REQUEST, "No key matching the refId");
});
}
@AllArgsConstructor
public class Establishment {
private final TransactionContext context; // Info of client public key , partner private key, public key
private final List<String> strReqs; // Data field information required for authentication to the client
private final KeyRing keyRing; // RSA Key information for JWT(JWS + JWE) encryption and decryption between Wallet Backed Server and partner server.
}
protected CBORObject generate() {
final CBORObject sessionEstablishment = CBORObject.NewMap();
sessionEstablishment.set(E_READER_KEY, CBORObject.FromObjectAndTag(KeyUtils.getEReaderKey(context), TAG_SIZE)); // Generate oneKey by public key in transactionContext
sessionEstablishment.set(DATA, CBORObject.FromObject(CipherUtils.encrypt(context, generateRequestFormat(getRequestCBORObjectsFrom(strReqs))))); // Add request data field information for authentication.
return sessionEstablishment;
}
Generate the response value JWT(JWS + JWE)
1. Generate establishment with JWE
public static String encryptedStringJWE(final Key publicKey, final String data) { // Please enter Samsung Public Key and establishment data
final JWEObject jwe = new JWEObject(
new JWEHeader.Builder(JWEAlgorithm.RSA_OAEP_256, EncryptionMethod.A128GCM).build(), new Payload(data));
try {
jwe.encrypt(new RSAEncrypter((RSAPublicKey) publicKey));
return jwe.serialize();
} catch (JOSEException e) {
log.error("encryptedStringJWE Exception message : {}", e.getMessage());
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "encryptedStringJWE error");
}
}
2. Generate JWS by JWE
public static String generateSignedStringJWS(final Key privateKey, final Key publicKey, final String payload) { // Enter your partner’s public key, private key, and JWE data.
try {
final JWSObject jwsObj = new JWSObject(getDefaultJWSHeader(), new Payload(payload));
JWSSigner signer = new RSASSASigner(new RSAKey.Builder((RSAPublicKey) publicKey).privateKey((RSAPrivateKey) privateKey).build());
jwsObj.sign(signer);
return jwsObj.serialize();
} catch (JOSEException e) {
log.error("encryptedStringJWS Exception message : {}", e.getMessage());
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "generateSignedStringJWS error");
}
}
3. Generate JWT(JWS + JWE)
public PartnerOutputDTO toPartnerOutputDTO() {
final CBORObject generate = this.generate();
final String establishment = Base64.getUrlEncoder().encodeToString(generate.EncodeToBytes());
final String strJWE = JWTUtils.encryptedStringJWE(keyRing.getTargetPublicKey(), establishment);
final JWSHeader jwsHeader = JWTUtils.getDefaultJWSHeader(keyRing.getVersion(), keyRing.getCertificateId(), "partnerId");
return new PartnerOutputDTO(JWTUtils.generateSignedStringJWS(jwsHeader, keyRing.getSourcePrivateKey(), keyRing.getSourcePublicKey(),strJWE));
}
Authentication processing for values in data fields requested for authentication
1. Retrieve transactionContext value stored in cache using refId value
@Override
public Mono<TransactionContext> getContext(final PartnerInputDTO inputDTO) {
return Mono.just(this.transactionContextManager.getTransactionContextBy(inputDTO.getRefId()));
}
2. Processes the decryption process of the request body data like JWT (JWS + JWE) decryption between Wallet Backed Server and partner server.
3. Generate MdocResponse
public class MdocResponse {
private final TransactionContext context; // Managed tranactionContext by refId
private final byte[] data; // Base64URL decoded data after decrypting JWT (JWS + JWE) data.
public MdocResponse(final TransactionContext context, final String inputDTO) {
this.context = context;
this.data = Base64Utils.decode(inputDTO.getBytes(StandardCharsets.UTF_8));
}
}
4. Get the field values requested for authentication from the data in mDocResponse.
public String getData() {
// SessionData = {
// ? "data" : bstr ; Encrypted mdoc response or mdoc request
// ? "status" : uint ; Status code
// }
final CBORObject response = CBORObject.DecodeFromBytes(data);
checkType(response, CBORType.Map);
final CBORObject data = response.get(DATA);
checkType(data, CBORType.ByteString);
return CBORObject.DecodeFromBytes(isEncryptedMode ? CipherUtils.decrypt(this.context, data.GetByteString()) : data.GetByteString()).ToJSONString();
}
5. Create a Session value using the transactionContext value managed by refId and then decrypt it.
private static byte[] processCipher(final CipherMode cipherMode, final TransactionContext context, final byte[] bytes) { // CipherMode : encrypt or decrypt, bytes : Data passed by the client
try {
Cipher cipher = Cipher.getInstance("AES/GCM/NoPadding");
final int counter = CipherMode.ENCRYPT == cipherMode ? context.getEncryptMessageCounter() : context.getDecryptMessageCounter();
GCMParameterSpec parameterSpec = new GCMParameterSpec(128, getSessionKeyIv(cipherMode.identifier, counter));
cipher.init(cipherMode.cipherMode , getSecretKeySpec(context, cipherMode.info), parameterSpec);
return cipher.doFinal(bytes);
} catch (InvalidAlgorithmParameterException | NoSuchPaddingException | IllegalBlockSizeException |
NoSuchAlgorithmException | BadPaddingException | InvalidKeyException e) {
log.error("error type : {}, message :{}", e.getClass(), e.getMessage());
throw new CustomException(HttpStatus.INTERNAL_SERVER_ERROR, "processCipher error");
}
}
6. Examining data received from the client.
@Override
public Mono<Void> authentication(final String response) {
log.info("response info : {}", response);
return Mono.empty();
}
Manage Your Cookies
We use cookies to improve your experience on our website and to show you relevant
advertising. Manage you settings for our cookies below.
Essential Cookies
These cookies are essential as they enable you to move around the website. This
category cannot be disabled.
Company
Domain
Samsung Electronics
.samsungdeveloperconference.com
Analytical/Performance Cookies
These cookies collect information about how you use our website. for example which
pages you visit most often. All information these cookies collect is used to improve
how the website works.
Company
Domain
LinkedIn
.linkedin.com
Meta (formerly Facebook)
.samsungdeveloperconference.com
Google Inc.
.samsungdeveloperconference.com
Functionality Cookies
These cookies allow our website to remember choices you make (such as your user name, language or the region your are in) and
tailor the website to provide enhanced features and content for you.
Company
Domain
LinkedIn
.ads.linkedin.com, .linkedin.com
Advertising Cookies
These cookies gather information about your browser habits. They remember that
you've visited our website and share this information with other organizations such
as advertisers.
Company
Domain
LinkedIn
.linkedin.com
Meta (formerly Facebook)
.samsungdeveloperconference.com
Google Inc.
.samsungdeveloperconference.com
Preferences Submitted
You have successfully updated your cookie preferences.