Skip to content

Commit e2c681b

Browse files
committed
Inline issuance wasm function
1 parent 26cfacb commit e2c681b

File tree

2 files changed

+303
-0
lines changed

2 files changed

+303
-0
lines changed
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
package com.credman.cmwallet
2+
3+
import java.nio.ByteBuffer
4+
import java.security.GeneralSecurityException
5+
import java.security.KeyFactory
6+
import java.security.KeyPairGenerator
7+
import java.security.PrivateKey
8+
import java.security.PublicKey
9+
import java.security.spec.ECGenParameterSpec
10+
import java.security.spec.X509EncodedKeySpec
11+
import javax.crypto.Cipher
12+
import javax.crypto.KeyAgreement
13+
import javax.crypto.Mac
14+
import javax.crypto.spec.GCMParameterSpec
15+
import javax.crypto.spec.SecretKeySpec
16+
17+
/**
18+
* A Kotlin implementation of Hybrid Public Key Encryption (HPKE) as specified in RFC 9180.
19+
*
20+
* This implementation focuses on the base encryption mode and supports the ciphersuite
21+
* using DHKEM(X25519), HKDF-SHA256, and AES-128-GCM.
22+
*
23+
* @property suite The HPKE ciphersuite to be used for all operations.
24+
*/
25+
class Hpke(val suite: HpkeCipherSuite) {
26+
27+
/**
28+
* Data class to hold a public/private key pair.
29+
*/
30+
data class HpkeKeyPair(val privateKey: PrivateKey, val publicKey: PublicKey)
31+
32+
/**
33+
* Data class to hold the result of an HPKE seal (encryption) operation.
34+
* @param enc The encapsulated ephemeral public key.
35+
* @param ciphertext The encrypted message, including the authentication tag.
36+
*/
37+
data class HpkeSealedData(val enc: ByteArray, val ciphertext: ByteArray)
38+
39+
/**
40+
* Custom exception for HPKE-specific failures.
41+
*/
42+
class HpkeException(message: String, cause: Throwable? = null) : GeneralSecurityException(message, cause)
43+
44+
/**
45+
* Represents the supported HPKE ciphersuites.
46+
* Each suite defines the KEM, KDF, and AEAD algorithms.
47+
* Values are taken from RFC 9180, Section 7.
48+
*
49+
* @param kemId The IANA-registered value for the KEM.
50+
* @param kdfId The IANA-registered value for the KDF.
51+
* @param aeadId The IANA-registered value for the AEAD.
52+
* @param keyBitLength (`Nk`) The length of the AEAD key in bits.
53+
* @param nonceBitLength (`Nn`) The length of the AEAD nonce in bits.
54+
* @param hashBitLength (`Nh`) The length of the KDF hash output in bits.
55+
* @param dhKeyBitLength (`Npk`) The length of the public key in bits.
56+
* @param keyAlgorithm The JCA algorithm name for the KEM.
57+
* @param kdfAlgorithm The JCA algorithm name for the KDF MAC.
58+
* @param aeadAlgorithm The JCA algorithm name for the AEAD cipher.
59+
*/
60+
enum class HpkeCipherSuite(
61+
val kemId: Short,
62+
val kdfId: Short,
63+
val aeadId: Short,
64+
val keyBitLength: Int,
65+
val nonceBitLength: Int,
66+
val hashBitLength: Int,
67+
val dhKeyBitLength: Int,
68+
val keyAlgorithm: String,
69+
val kemAlgorithm: String,
70+
val kdfAlgorithm: String,
71+
val aeadAlgorithm: String,
72+
) {
73+
DHKEM_P256_HKDF_SHA256_AES_128_GCM(
74+
kemId = 0x0010,
75+
kdfId = 0x0001,
76+
aeadId = 0x0001,
77+
keyBitLength = 128,
78+
nonceBitLength = 96,
79+
hashBitLength = 256,
80+
dhKeyBitLength = 256,
81+
keyAlgorithm = "EC",
82+
kemAlgorithm = "ECDH",
83+
kdfAlgorithm = "HmacSHA256",
84+
aeadAlgorithm = "AES/GCM/NoPadding"
85+
);
86+
87+
val keyByteLength: Int get() = keyBitLength / 8
88+
val nonceByteLength: Int get() = nonceBitLength / 8
89+
val hashByteLength: Int get() = hashBitLength / 8
90+
val dhKeyByteLength: Int get() = dhKeyBitLength / 8
91+
}
92+
93+
// Internal container for the derived secrets from the key schedule.
94+
private data class HpkeContext(val key: ByteArray, val baseNonce: ByteArray, val exporterSecret: ByteArray)
95+
96+
private val suiteId: ByteArray = "HPKE".toByteArray(Charsets.UTF_8) + ByteBuffer.allocate(6)
97+
.putShort(suite.kemId)
98+
.putShort(suite.kdfId)
99+
.putShort(suite.aeadId)
100+
.array()
101+
102+
companion object {
103+
private const val HPKE_VERSION_LABEL = "HPKE-v1"
104+
private const val HPKE = "HPKE"
105+
private const val KEM = "KEM"
106+
107+
fun generateKeyPair(suite: HpkeCipherSuite): HpkeKeyPair {
108+
val kpg = KeyPairGenerator.getInstance(suite.keyAlgorithm)
109+
val spec = when (suite) {
110+
HpkeCipherSuite.DHKEM_P256_HKDF_SHA256_AES_128_GCM -> ECGenParameterSpec("secp256r1")
111+
}
112+
kpg.initialize(spec)
113+
val kp = kpg.generateKeyPair()
114+
return HpkeKeyPair(kp.private, kp.public)
115+
}
116+
}
117+
118+
/**
119+
* Encrypts a message for a recipient. This is the `Seal` operation.
120+
* See RFC 9180, Section 5.1.1.
121+
*
122+
* @param recipientPublicKey The public key of the recipient.
123+
* @param info Application-specific information (can be empty).
124+
* @param aad Additional Associated Data to be authenticated (can be empty).
125+
* @param plaintext The message to encrypt.
126+
* @return An HpkeSealedData object containing the encapsulated key and the ciphertext.
127+
*/
128+
fun seal(recipientPublicKey: PublicKey, info: ByteArray, aad: ByteArray, plaintext: ByteArray): HpkeSealedData {
129+
// Step 1: Ephemeral key generation and DH
130+
val ephemeralKeyPair = generateKeyPair(this.suite)
131+
val sharedSecret = dh(ephemeralKeyPair.privateKey, recipientPublicKey)
132+
val enc = ephemeralKeyPair.publicKey.encoded
133+
134+
// Step 2: Derive HPKE context
135+
// This combines SetupS from RFC 9180, Section 5.1.1
136+
val context = keySchedule(sharedSecret, info)
137+
138+
// Step 3: Encrypt the plaintext
139+
// This is the AeadSeal operation from RFC 9180, Section 5.2
140+
val ciphertext = aeadSeal(context.key, context.baseNonce, aad, plaintext)
141+
142+
return HpkeSealedData(enc, ciphertext)
143+
}
144+
145+
/**
146+
* Decrypts a message. This is the `Open` operation.
147+
* See RFC 9180, Section 5.1.1.
148+
*
149+
* @param sealedData The object containing the encapsulated key and ciphertext.
150+
* @param recipientKeyPair The key pair of the recipient.
151+
* @param info Application-specific information (must match the info used for sealing).
152+
* @param aad Additional Associated Data (must match the aad used for sealing).
153+
* @return The decrypted plaintext, or throws an exception if decryption fails.
154+
*/
155+
fun open(sealedData: HpkeSealedData, recipientKeyPair: PrivateKey, info: ByteArray, aad: ByteArray): ByteArray {
156+
val (enc, ciphertext) = sealedData
157+
158+
// Step 1: Perform DH with the encapsulated ephemeral public key
159+
val ephemeralPublicKey = KeyFactory.getInstance(suite.keyAlgorithm).generatePublic(X509EncodedKeySpec(enc))
160+
val sharedSecret = dh(recipientKeyPair, ephemeralPublicKey)
161+
162+
// Step 2: Derive HPKE context
163+
// This combines SetupR from RFC 9180, Section 5.1.1
164+
val context = keySchedule(sharedSecret, info)
165+
166+
// Step 3: Decrypt the ciphertext
167+
// This is the AeadOpen operation from RFC 9180, Section 5.2
168+
return aeadOpen(context.key, context.baseNonce, aad, ciphertext)
169+
}
170+
171+
/**
172+
* Performs the Diffie-Hellman key exchange.
173+
* Corresponds to `DH(sk, pk)` in the RFC.
174+
*/
175+
private fun dh(privateKey: PrivateKey, publicKey: PublicKey): ByteArray {
176+
val ka = KeyAgreement.getInstance(suite.kemAlgorithm)
177+
ka.init(privateKey)
178+
ka.doPhase(publicKey, true)
179+
return ka.generateSecret()
180+
}
181+
182+
/**
183+
* Implements the HPKE Key Schedule for base mode.
184+
* See RFC 9180, Section 5.1.
185+
*
186+
* @param sharedSecret The result of the Diffie-Hellman exchange.
187+
* @param info Application-specific information.
188+
* @return An HpkeContext containing the derived key, nonce, and exporter secret.
189+
*/
190+
private fun keySchedule(sharedSecret: ByteArray, info: ByteArray): HpkeContext {
191+
// In "base" mode, psk and psk_id are empty.
192+
val emptySalt = ByteArray(suite.hashByteLength)
193+
194+
// secret = LabeledExtract("", "secret", shared_secret)
195+
val secret = labeledExtract(emptySalt, "secret", sharedSecret)
196+
197+
// key = LabeledExpand(secret, "key", info, Nk)
198+
val key = labeledExpand(secret, "key", info, suite.keyByteLength)
199+
200+
// base_nonce = LabeledExpand(secret, "base_nonce", info, Nn)
201+
val baseNonce = labeledExpand(secret, "base_nonce", info, suite.nonceByteLength)
202+
203+
// exporter_secret = LabeledExpand(secret, "exp", info, Nh)
204+
val exporterSecret = labeledExpand(secret, "exp", info, suite.hashByteLength)
205+
206+
return HpkeContext(key, baseNonce, exporterSecret)
207+
}
208+
209+
/**
210+
* AEAD Encryption (AES-GCM). Corresponds to `AeadSeal(key, nonce, aad, pt)`.
211+
* See RFC 9180, Section 5.2.
212+
* The sequence number is always 0 for single-shot encryption.
213+
*/
214+
private fun aeadSeal(key: ByteArray, nonce: ByteArray, aad: ByteArray, pt: ByteArray): ByteArray {
215+
val cipher = Cipher.getInstance(suite.aeadAlgorithm)
216+
val keySpec = SecretKeySpec(key, "AES")
217+
// Tag length for AES-GCM is typically 128 bits (16 bytes).
218+
val gcmSpec = GCMParameterSpec(128, nonce)
219+
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec)
220+
cipher.updateAAD(aad)
221+
return cipher.doFinal(pt)
222+
}
223+
224+
/**
225+
* AEAD Decryption (AES-GCM). Corresponds to `AeadOpen(key, nonce, aad, ct)`.
226+
* See RFC 9180, Section 5.2.
227+
*/
228+
private fun aeadOpen(key: ByteArray, nonce: ByteArray, aad: ByteArray, ct: ByteArray): ByteArray {
229+
try {
230+
val cipher = Cipher.getInstance(suite.aeadAlgorithm)
231+
val keySpec = SecretKeySpec(key, "AES")
232+
val gcmSpec = GCMParameterSpec(128, nonce)
233+
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec)
234+
cipher.updateAAD(aad)
235+
return cipher.doFinal(ct)
236+
} catch (e: GeneralSecurityException) {
237+
// Catches AEADBadTagException and others, indicating a decryption failure.
238+
throw HpkeException("AEAD decryption failed, likely due to invalid authentication tag.", e)
239+
}
240+
}
241+
242+
/**
243+
* HKDF-Extract operation.
244+
* See RFC 5869.
245+
*/
246+
private fun hkdfExtract(salt: ByteArray, ikm: ByteArray): ByteArray {
247+
val mac = Mac.getInstance(suite.kdfAlgorithm)
248+
mac.init(SecretKeySpec(salt, suite.kdfAlgorithm))
249+
return mac.doFinal(ikm)
250+
}
251+
252+
/**
253+
* HKDF-Expand operation.
254+
* See RFC 5869.
255+
*/
256+
private fun hkdfExpand(prk: ByteArray, info: ByteArray, length: Int): ByteArray {
257+
val mac = Mac.getInstance(suite.kdfAlgorithm)
258+
mac.init(SecretKeySpec(prk, suite.kdfAlgorithm))
259+
260+
val result = ByteArray(length)
261+
var currentInfo = info
262+
var t = ByteArray(0)
263+
var i: Byte = 1
264+
var bytesCopied = 0
265+
266+
while (bytesCopied < length) {
267+
mac.update(t)
268+
mac.update(currentInfo)
269+
mac.update(i)
270+
t = mac.doFinal()
271+
272+
val toCopy = minOf(length - bytesCopied, t.size)
273+
System.arraycopy(t, 0, result, bytesCopied, toCopy)
274+
bytesCopied += toCopy
275+
276+
currentInfo = ByteArray(0) // Subsequent rounds use only the previous T value
277+
i++
278+
}
279+
return result
280+
}
281+
282+
/**
283+
* Implements the `LabeledExtract` function from RFC 9180, Section 4.
284+
*/
285+
private fun labeledExtract(salt: ByteArray, label: String, ikm: ByteArray): ByteArray {
286+
val labeledIkm = HPKE_VERSION_LABEL.toByteArray() + suiteId + label.toByteArray() + ikm
287+
return hkdfExtract(salt, labeledIkm)
288+
}
289+
290+
/**
291+
* Implements the `LabeledExpand` function from RFC 9180, Section 4.
292+
*/
293+
private fun labeledExpand(prk: ByteArray, label: String, info: ByteArray, length: Int): ByteArray {
294+
val lengthBytes = ByteBuffer.allocate(2).putShort(length.toShort()).array()
295+
val labeledInfo = lengthBytes + HPKE_VERSION_LABEL.toByteArray() + suiteId + label.toByteArray() + info
296+
return hkdfExpand(prk, labeledInfo, length)
297+
}
298+
}

matcher/credentialmanager.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,11 @@ __attribute__((import_module("credman"), import_name("AddPaymentEntry")))
5151
#endif
5252
void AddPaymentEntry(char *cred_id, char *merchant_name, char *payment_method_name, char *payment_method_subtitle, char* payment_method_icon, size_t payment_method_icon_len, char *transaction_amount, char* bank_icon, size_t bank_icon_len, char* payment_provider_icon, size_t payment_provider_icon_len);
5353

54+
#if defined(__wasm__)
55+
__attribute__((import_module("credman"), import_name("AddInlineIssuanceEntry")))
56+
#endif
57+
void AddInlineIssuanceEntry(char *cred_id, char* icon, size_t icon_len, char *title, char *subtitle);
58+
5459
#if defined(__wasm__)
5560
__attribute__((import_module("credman"), import_name("SetAdditionalDisclaimerAndUrlForVerificationEntry")))
5661
#endif

0 commit comments

Comments
 (0)