Example of Passbase metadata encryption in Kotlin. Private key given by example (not a real key)
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// ... | |
dependencies { | |
implementation("com.fasterxml.jackson.core:jackson-annotations:2.13.1") | |
implementation("com.fasterxml.jackson.core:jackson-core:2.13.1") | |
implementation("com.fasterxml.jackson.core:jackson-databind:2.13.1") | |
implementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.1") | |
implementation("org.bouncycastle:bcprov-jdk15on:1.70") | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package com.passbase.sample | |
import com.fasterxml.jackson.databind.ObjectMapper | |
import com.fasterxml.jackson.databind.SerializationFeature | |
import com.fasterxml.jackson.module.kotlin.registerKotlinModule | |
import java.io.* | |
import java.nio.charset.StandardCharsets | |
import java.security.* | |
import java.security.spec.PKCS8EncodedKeySpec | |
import java.util.* | |
import java.util.stream.Collectors | |
/** Passbase encryption and serialization utility. */ | |
class PassbaseUtil { | |
companion object { | |
private const val signatureCipher = "NONEwithRSA" | |
private val mapper = ObjectMapper().registerKotlinModule() | |
init { | |
// note: if you're running in a DI container or on GraalVM, the following init code is safely | |
// placeable on the object rather than as a static initializer. | |
Security.insertProviderAt(org.bouncycastle.jce.provider.BouncyCastleProvider(), 1) | |
// sort map keys deterministically to ensure stable plaintext | |
mapper.configure( | |
SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS, | |
true | |
) | |
} | |
} | |
/** | |
* Load a PEM-encoded PKCS8 private key from JAR resources at the provided [filename], then decode it as a | |
* [PrivateKey] using the specified [algorithm] (which defaults to RSA). If the key decodes as expected, [callable] | |
* is then invoked with the key as the only parameter. | |
* | |
* The return value of [callable] (type [R]) is returned as the result of this method. If the key fails to decode | |
* properly as a private key using the named [algorithm], resulting exceptions are thrown to the caller. | |
* | |
* @param R Return type of the provided callable which uses the private key. | |
* @param filename Name of the JAR resource which should be loaded. | |
* @param algorithm Name of the keying algorithm to use. Defaults to RSA. | |
* @param callable Function which consumes the resulting private key, performs work, and returns. | |
* @return Value returned by the [callable] function passed to this method. | |
*/ | |
private fun <R> withPrivateKey(filename: String, algorithm: String = "RSA", callable: (PrivateKey) -> R): R { | |
return PassbaseUtil::class.java.getResourceAsStream("/$filename").use { stream -> | |
BufferedReader(InputStreamReader(Objects.requireNonNull( | |
stream, | |
"failed to find private key at path JAR resource path '$filename'" | |
))).use { buffer -> | |
callable.invoke(KeyFactory.getInstance(algorithm).generatePrivate( | |
PKCS8EncodedKeySpec(Base64.getDecoder().decode(buffer.lines().filter { | |
!it.startsWith("---") | |
}.collect(Collectors.joining("")))) | |
)) | |
} | |
} | |
} | |
/** | |
* Encrypt the provided [plainbytes] with the provided [key], using the [signatureCipher] defined for use with | |
* Passbase (PKCS#1 v1.5 with no padding at the time of this writing). | |
* | |
* @param key Private key which should be used to sign/encrypt the provided [plainbytes]. | |
* @param plainbytes Plaintext content to sign/encrypt, encoded in UTF-8. | |
* @return Raw encrypted bytes resulting from the encryption process. | |
*/ | |
private fun signEncryptData(key: PrivateKey, plainbytes: ByteArray): ByteArray { | |
val rsa: Signature = Signature.getInstance(signatureCipher) | |
rsa.initSign(key) | |
rsa.update(plainbytes) | |
return rsa.sign() | |
} | |
/** | |
* Encrypt the provided [plaintext] for use as Passbase metadata, using the key located in local JAR resources at | |
* the provided [keyfile] path. If the key loads as expected, encrypt [plaintext] for use as metadata, then invoke | |
* [callback] with the result. | |
* | |
* @param R Return type of the provided callable which uses the encrypted text. | |
* @param keyfile JAR resource path (without prefix slash) to load as the private key. | |
* @param plaintext UTF-8 encoded plaintext string to encrypt as metadata. | |
* @param callback Callback function to invoke with the encrypted metadata result. | |
* @return Return value from the provided [callback], if any. | |
*/ | |
private fun <R> encryptMetadata(keyfile: String, plaintext: String, callback: (String) -> R): R { | |
return withPrivateKey(keyfile) { key -> | |
callback(Base64.getEncoder().encodeToString(signEncryptData( | |
key, | |
plaintext.toByteArray(StandardCharsets.UTF_8) | |
))) | |
} | |
} | |
/** | |
* Serialize and encrypt the provided [pojo] for use as Passbase metadata. The [pojo] is first serialized into JSON | |
* using Jackson, then encrypted using [keyfile] and encoded into Base64, before being handed [callback] for further | |
* processing. | |
* | |
* @param I Intermediate JSON object type which should be encoded and then encrypted. | |
* @param R Ultimate return type of the callback that uses the encrypted data. | |
* @param keyfile Name of the private key file to use for the encryption step. | |
* @param pojo Java object to encode as JSON and then encrypt. | |
* @param callback Callback to invoke with the encrypted result. | |
* @return Whatever [callback] returns. | |
*/ | |
fun <I, R> encryptMetadataJSON(keyfile: String, pojo: I, callback: (String) -> R): R { | |
return encryptMetadata( | |
keyfile, | |
mapper.writeValueAsString(pojo), | |
callback | |
) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
data class MyPassbaseMetadata ( | |
var userId: String? = null, | |
var isVip: Boolean = false, | |
var nestedInfo: Map<String, Any>? = emptyMap() | |
) | |
fun runSample() { | |
// create a passbase object | |
val metadata = MyPassbaseMetadata( | |
userId = "sample123", | |
isVip = true, | |
nestedInfo = mapOf( | |
"nestedValue" to 5.5, | |
"anything" to "here" | |
) | |
) | |
PassbaseUtil().encryptMetadataJSON("sample-key.pem", metadata) { encryptedData -> | |
// do something with encrypted data here, like returning it to your frontend | |
// so it can be included on a `<PassbaseButton />` | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-----BEGIN RSA PRIVATE KEY----- | |
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 | |
-----END RSA PRIVATE KEY----- |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment