Skip to content

Commit

Permalink
Фdd exposed-crypt (JetBrains#1365)
Browse files Browse the repository at this point in the history
  • Loading branch information
moonchanyong authored Feb 19, 2022
1 parent aec4ee5 commit e71370e
Show file tree
Hide file tree
Showing 12 changed files with 323 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -660,7 +660,7 @@ open class BasicBinaryColumnType : ColumnType() {
/**
* Binary column for storing binary strings of a specific [length].
*/
class BinaryColumnType(
open class BinaryColumnType(
/** Returns the length of the column- */
val length: Int
) : BasicBinaryColumnType() {
Expand Down
12 changes: 12 additions & 0 deletions exposed-crypt/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
kotlin("jvm") apply true
}

repositories {
mavenCentral()
}

dependencies {
api(project(":exposed-core"))
api("org.springframework.security", "spring-security-crypto", "5.5.3")
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package org.jetbrains.exposed.crypt

import org.springframework.security.crypto.encrypt.AesBytesEncryptor
import org.springframework.security.crypto.keygen.KeyGenerators
import org.springframework.security.crypto.util.EncodingUtils
import java.util.*
import javax.crypto.Cipher
import javax.crypto.spec.IvParameterSpec
import javax.crypto.spec.SecretKeySpec
import kotlin.math.ceil

object Algorithms {
private fun base64EncodedLength(byteSize: Int): Int = ceil(byteSize.toDouble() / 3).toInt() * 4
private fun paddingLen(len: Int, blockSize: Int): Int = if (len % blockSize == 0) 0 else blockSize - len % blockSize
private val base64Decoder = Base64.getDecoder()
private val base64Encoder = Base64.getEncoder()

private const val AES_256_GCM_BLOCK_LENGTH = 16
private const val AES_256_GCM_TAG_LENGTH = 16
@Suppress("FunctionNaming")
fun AES_256_PBE_GCM(password: CharSequence, salt: CharSequence): Encryptor {

return AesBytesEncryptor(password.toString(),
salt,
KeyGenerators.secureRandom(AES_256_GCM_BLOCK_LENGTH),
AesBytesEncryptor.CipherAlgorithm.GCM)
.run {
Encryptor({ base64Encoder.encodeToString(encrypt(it.toByteArray())) },
{ String(decrypt(base64Decoder.decode(it))) },
{ inputLen ->
base64EncodedLength(AES_256_GCM_BLOCK_LENGTH + inputLen + AES_256_GCM_TAG_LENGTH) })
}
}

private const val AES_256_CBC_BLOCK_LENGTH = 16
@Suppress("FunctionNaming")
fun AES_256_PBE_CBC(password: CharSequence, salt: CharSequence): Encryptor {

return AesBytesEncryptor(password.toString(),
salt,
KeyGenerators.secureRandom(AES_256_CBC_BLOCK_LENGTH))
.run {
Encryptor({ base64Encoder.encodeToString(encrypt(it.toByteArray())) },
{ String(decrypt(base64Decoder.decode(it))) },
{ inputLen ->
val paddingSize = (AES_256_CBC_BLOCK_LENGTH - inputLen % AES_256_CBC_BLOCK_LENGTH)
base64EncodedLength(AES_256_CBC_BLOCK_LENGTH + inputLen + paddingSize) })
}
}

private const val BLOW_FISH_BLOCK_LENGTH = 8
@Suppress("FunctionNaming")
fun BLOW_FISH(key: CharSequence): Encryptor {
val ks = SecretKeySpec(key.toString().toByteArray(), "Blowfish")

return Encryptor(
{ plainText ->
val cipher = Cipher.getInstance("Blowfish")
cipher.init(Cipher.ENCRYPT_MODE, ks)

val encryptedBytes = cipher.doFinal(plainText.toByteArray())
base64Encoder.encodeToString(encryptedBytes)
},
{ encryptedText ->
val cipher = Cipher.getInstance("Blowfish")
cipher.init(Cipher.DECRYPT_MODE, ks)

val decryptedBytes = cipher.doFinal(base64Decoder.decode(encryptedText))
String(decryptedBytes)
},
{ base64EncodedLength(it + paddingLen(it, BLOW_FISH_BLOCK_LENGTH)) })
}

private const val TRIPLE_DES_KEY_LENGTH = 24
private const val TRIPLE_DES_BLOCK_LENGTH = 8
@Suppress("FunctionNaming")
fun TRIPLE_DES(secretKey: CharSequence): Encryptor {
if (secretKey.toString().toByteArray().size != TRIPLE_DES_KEY_LENGTH) {
throw IllegalArgumentException("secretKey must have 24 bytes")
}
val ks = SecretKeySpec(secretKey.toString().toByteArray(), "TripleDES")

val ivGenerator = KeyGenerators.secureRandom(TRIPLE_DES_BLOCK_LENGTH)

return Encryptor(
{ plainText ->
val cipher = Cipher.getInstance("TripleDES/CBC/PKCS5Padding")
val iv = ivGenerator.generateKey()
cipher.init(Cipher.ENCRYPT_MODE, ks, IvParameterSpec(iv))

val encryptedBytes = cipher.doFinal(plainText.toByteArray())
base64Encoder.encodeToString(EncodingUtils.concatenate(iv, encryptedBytes))
},
{ encryptedText ->
val cipher = Cipher.getInstance("TripleDES/CBC/PKCS5Padding")
val decodedBytes = base64Decoder.decode(encryptedText.toByteArray())

val iv = EncodingUtils.subArray(decodedBytes, 0, TRIPLE_DES_BLOCK_LENGTH)
cipher.init(Cipher.DECRYPT_MODE, ks, IvParameterSpec(iv))

val decryptedBytes = cipher.doFinal(EncodingUtils.subArray(decodedBytes, TRIPLE_DES_BLOCK_LENGTH, decodedBytes.size))
String(decryptedBytes)
},
{ base64EncodedLength(TRIPLE_DES_BLOCK_LENGTH + it + paddingLen(it, TRIPLE_DES_BLOCK_LENGTH)) })
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package org.jetbrains.exposed.crypt

import org.jetbrains.exposed.sql.BinaryColumnType

class EncryptedBinaryColumnType(
private val encryptor: Encryptor,
length: Int
) : BinaryColumnType(length) {
override fun notNullValueToDB(value: Any): Any {
if (value !is ByteArray) {
error("Unexpected value of type Byte: $value of ${value::class.qualifiedName}")
}

return encryptor.encrypt(String(value)).toByteArray()
}

override fun valueFromDB(value: Any): Any {
val encryptedByte = super.valueFromDB(value)

if (encryptedByte !is ByteArray) {
error("Unexpected value of type Byte: $value of ${value::class.qualifiedName}")
}

return encryptor.decrypt(String(encryptedByte)).toByteArray()
}

override fun validateValueBeforeUpdate(value: Any?) {
if (value != null) {
super.validateValueBeforeUpdate(notNullValueToDB(value))
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false

other as EncryptedBinaryColumnType

if (encryptor != other.encryptor) return false

return true
}

override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + encryptor.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package org.jetbrains.exposed.crypt

import org.jetbrains.exposed.sql.VarCharColumnType

class EncryptedVarCharColumnType(
private val encryptor: Encryptor,
colLength: Int,
) : VarCharColumnType(colLength) {
override fun notNullValueToDB(value: Any): Any {
return encryptor.encrypt(value.toString())
}

override fun valueFromDB(value: Any): Any {
val encryptedStr = super.valueFromDB(value)

if (encryptedStr !is String) {
error("Unexpected value of type String: $value of ${value::class.qualifiedName}")
}

return encryptor.decrypt(encryptedStr)
}

override fun validateValueBeforeUpdate(value: Any?) {
if (value != null) {
super.validateValueBeforeUpdate(notNullValueToDB(value))
}
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
if (!super.equals(other)) return false

other as EncryptedVarCharColumnType

if (encryptor != other.encryptor) return false

return true
}

override fun hashCode(): Int {
var result = super.hashCode()
result = 31 * result + encryptor.hashCode()
return result
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package org.jetbrains.exposed.crypt

class Encryptor(
val encryptFn: (String) -> String,
val decryptFn: (String) -> String,
val maxColLengthFn: (Int) -> Int
) {
fun encrypt(str: String) = encryptFn(str)
fun decrypt(str: String) = decryptFn(str)
fun maxColLength(inputByteSize: Int) = maxColLengthFn(inputByteSize)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.jetbrains.exposed.crypt

import org.jetbrains.exposed.sql.Column
import org.jetbrains.exposed.sql.Table

fun Table.encryptedVarchar(name: String, cipherTextLength: Int, encryptor: Encryptor): Column<String> =
registerColumn(name, EncryptedVarCharColumnType(encryptor, cipherTextLength))

fun Table.encryptedBinary(name: String, cipherByteLength: Int, encryptor: Encryptor): Column<ByteArray> =
registerColumn(name, EncryptedBinaryColumnType(encryptor, cipherByteLength))
1 change: 1 addition & 0 deletions exposed-tests/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ dependencies {
implementation(project(":exposed-core"))
implementation(project(":exposed-jdbc"))
implementation(project(":exposed-dao"))
implementation(project(":exposed-crypt"))
implementation(kotlin("test-junit"))
implementation("org.slf4j", "slf4j-api", Versions.slf4j)
implementation("org.apache.logging.log4j", "log4j-slf4j-impl", Versions.log4j2)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.jetbrains.exposed.sql.tests.shared.dml

import org.jetbrains.exposed.crypt.Algorithms
import org.jetbrains.exposed.crypt.encryptedBinary
import org.jetbrains.exposed.crypt.encryptedVarchar
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.tests.DatabaseTestsBase
Expand Down Expand Up @@ -255,4 +258,30 @@ class SelectTests : DatabaseTestsBase() {
assertEquals(0, stringTable.select { stringTable.name eq veryLongString }.count())
}
}

@Test
fun `test encryptedColumnType with a string`() {
val stringTable = object : IntIdTable("StringTable") {
val name = encryptedVarchar("name", 80, Algorithms.AES_256_PBE_CBC("passwd", "5c0744940b5c369b"))
val city = encryptedBinary("city", 80, Algorithms.AES_256_PBE_GCM("passwd", "5c0744940b5c369b"))
val address = encryptedVarchar("address", 100, Algorithms.BLOW_FISH("key"))
val age = encryptedVarchar("age", 100, Algorithms.TRIPLE_DES("1".repeat(24)))
}

withTables(stringTable) {
val id1 = stringTable.insertAndGetId {
it[name] = "testName"
it[city] = "testCity".toByteArray()
it[address] = "testAddress"
it[age] = "testAge"
}

assertEquals(1L, stringTable.selectAll().count())

assertEquals("testName", stringTable.select { stringTable.id eq id1 }.first()[stringTable.name])
assertEquals("testCity", String(stringTable.select { stringTable.id eq id1 }.first()[stringTable.city]))
assertEquals("testAddress", stringTable.select { stringTable.id eq id1 }.first()[stringTable.address])
assertEquals("testAge", stringTable.select { stringTable.id eq id1 }.first()[stringTable.age])
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package org.jetbrains.exposed.sql.tests.shared.dml

import org.jetbrains.exposed.crypt.Algorithms
import org.jetbrains.exposed.crypt.encryptedBinary
import org.jetbrains.exposed.crypt.encryptedVarchar
import org.jetbrains.exposed.dao.id.IntIdTable
import org.jetbrains.exposed.exceptions.UnsupportedByDialectException
import org.jetbrains.exposed.sql.*
Expand Down Expand Up @@ -111,7 +114,36 @@ class UpdateTests : DatabaseTestsBase() {
// empty
}
}
}
}

@Test
fun `update encryptedColumnType`() {
val stringTable = object : IntIdTable("StringTable") {
val name = encryptedVarchar("name", 100, Algorithms.AES_256_PBE_GCM("passwd", "12345678"))
val city = encryptedBinary("city", 100, Algorithms.AES_256_PBE_CBC("passwd", "12345678"))
val address = encryptedVarchar("address", 100, Algorithms.BLOW_FISH("key"))
}

withTables(stringTable) {
val id = stringTable.insertAndGetId {
it[name] = "TestName"
it[city] = "TestCity".toByteArray()
it[address] = "TestAddress"
}

val updatedName = "TestName2"
val updatedCity = "TestCity2"
val updatedAddress = "TestAddress2"
stringTable.update({ stringTable.id eq id }) {
it[name] = updatedName
it[city] = updatedCity.toByteArray()
it[address] = updatedAddress
}

assertEquals(updatedName, stringTable.select { stringTable.id eq id }.single()[stringTable.name])
assertEquals(updatedCity, String(stringTable.select { stringTable.id eq id }.single()[stringTable.city]))
assertEquals(updatedAddress, stringTable.select { stringTable.id eq id }.single()[stringTable.address])
}
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package org.jetbrains.exposed.sql.tests.shared.functions

import org.jetbrains.exposed.crypt.Algorithms
import org.jetbrains.exposed.crypt.Encryptor
import org.jetbrains.exposed.sql.*
import org.jetbrains.exposed.sql.Function
import org.jetbrains.exposed.sql.SqlExpressionBuilder.concat
Expand All @@ -13,6 +15,7 @@ import org.jetbrains.exposed.sql.tests.shared.dml.withCitiesAndUsers
import org.jetbrains.exposed.sql.vendors.OracleDialect
import org.jetbrains.exposed.sql.vendors.SQLServerDialect
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotNull

class FunctionsTests : DatabaseTestsBase() {
Expand Down Expand Up @@ -446,4 +449,25 @@ class FunctionsTests : DatabaseTestsBase() {
}
}
}

private val encryptors = arrayOf("AES_256_PBE_GCM" to Algorithms.AES_256_PBE_GCM("passwd", "12345678"),
"AES_256_PBE_CBC" to Algorithms.AES_256_PBE_CBC("passwd", "12345678"),
"BLOW_FISH" to Algorithms.BLOW_FISH("sadsad"),
"TRIPLE_DES" to Algorithms.TRIPLE_DES("1".repeat(24)))
private val testStrings = arrayOf("1", "2".repeat(10), "3".repeat(31), "4".repeat(1001), "5".repeat(5391))

@Test
fun `test output length of encryption`() {
fun testSize(algorithm: String, encryptor: Encryptor, str: String) =
assertEquals(
encryptor.maxColLength(str.toByteArray().size),
encryptor.encrypt(str).toByteArray().size,
"Failed to calculate length of $algorithm's output.")

for ((algorithm, encryptor) in encryptors) {
for (testStr in testStrings) {
testSize(algorithm, encryptor, testStr)
}
}
}
}
1 change: 1 addition & 0 deletions settings.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ include("exposed-tests")
include("exposed-money")
include("exposed-bom")
include("exposed-kotlin-datetime")
include("exposed-crypt")

pluginManagement {
plugins {
Expand Down

0 comments on commit e71370e

Please sign in to comment.