Skip to content

Instantly share code, notes, and snippets.

@sgammon
Last active March 2, 2022 15:10
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sgammon/e9a058431fde84fc66dddc3b869834da to your computer and use it in GitHub Desktop.
Save sgammon/e9a058431fde84fc66dddc3b869834da to your computer and use it in GitHub Desktop.
Example of Passbase metadata encryption in Kotlin. Private key given by example (not a real key)
// ...
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")
}
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
)
}
}
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 />`
}
}
-----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