From 104413e9ba65cc8c0daad947a7a15f2f8ed45aaf Mon Sep 17 00:00:00 2001 From: giuseppe-s Date: Mon, 11 Nov 2024 11:02:47 +0100 Subject: [PATCH 1/6] Edited readme --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index dbe3853..7df01e4 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,6 @@ In this section we provide a description of the ```PolyProtect``` class. **NOTE 2**: The size of the protected templates depends on ```_POLYNOMIALDEGREE``` and ```_OVERLAP```. - # Workflow In this section we report the sequence diagrams are included for integrating PolyProtect in the opserations of enrolment, verification, and indentification. From 61a3fb16650d3a525973fd1284e59b095d786c69 Mon Sep 17 00:00:00 2001 From: giuseppe-s Date: Mon, 11 Nov 2024 16:25:09 +0100 Subject: [PATCH 2/6] Added version to build.gradle.kts --- build.gradle.kts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/build.gradle.kts b/build.gradle.kts index 807c5b6..f81d52c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,8 @@ plugins { id("org.jetbrains.kotlin.plugin.parcelize") version "2.1.0-RC" } +version = "2024.4.0" + android { namespace = "com.simprints.biometrics_simpolyprotect" compileSdk = 34 From 743fdcfecf2d650959fa389937c63a956e970761 Mon Sep 17 00:00:00 2001 From: gstrag Date: Mon, 18 Nov 2024 11:45:02 +0100 Subject: [PATCH 3/6] Improved unit tests, added integration test --- .gitignore | 1 + build.gradle.kts | 2 +- src/main/AndroidManifest.xml | 2 +- .../biometrics_simpolyprotect/PolyProtect.kt | 8 +- .../ExampleUnitTest.kt | 54 ---------- .../PolyProtectIntegrationTest.kt | 57 +++++++++++ .../PolyProtectUnitTest.kt | 98 +++++++++++++++++++ 7 files changed, 164 insertions(+), 58 deletions(-) delete mode 100644 src/test/java/com/simprints/biometrics_simpolyprotect/ExampleUnitTest.kt create mode 100644 src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt create mode 100644 src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt diff --git a/.gitignore b/.gitignore index 4dab7bb..e91d8d4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .idea .gradle +build local.properties \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index c72cbaa..1d0ef06 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { val projectGroupId = "com.simprints" val projectArtifactId = "biometrics_simpolyprotect" -val projectVersion = "0.0.2-SNAPSHOT" +val projectVersion = "2024.4.0" android { namespace = "$projectGroupId.$projectArtifactId" diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 9b65eb0..ae434a1 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1 +1 @@ - + \ No newline at end of file diff --git a/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt b/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt index 6c795a9..fbcd5c2 100644 --- a/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt +++ b/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt @@ -50,12 +50,14 @@ class PolyProtect { } fun transformTemplate(unprotectedTemplate: DoubleArray, - subjectSecretAuxData: secretAuxDataRecord + subjectSecretAuxData: secretAuxDataRecord ): protectedTemplateRecord { val (_, c, e) = subjectSecretAuxData // For convenience assert(e.size == c.size) { "Auxiliary data sizes must be equal." } + assert(e.size == POLYNOMIALDEGREE) { "Auxiliary data sizes must be equal to PolyProtect.POLYNOMIALDEGREE." } + val stepSize = e.size - OVERLAP val eIndices = e.indices @@ -108,7 +110,9 @@ class PolyProtect { // Calculate cosine similarity val cosineSimilarity = dotProduct / (magnitudeA * magnitudeB) // Calculate cosine distance - return (2.0-1.0 * cosineSimilarity)/2 + val cosineDistance = 1 - cosineSimilarity + + return (2.0-1.0*cosineDistance)/2 } } } diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/ExampleUnitTest.kt b/src/test/java/com/simprints/biometrics_simpolyprotect/ExampleUnitTest.kt deleted file mode 100644 index c39e1d6..0000000 --- a/src/test/java/com/simprints/biometrics_simpolyprotect/ExampleUnitTest.kt +++ /dev/null @@ -1,54 +0,0 @@ -package com.simprints.biometrics_simpolyprotect -import PolyProtect -import org.junit.Test -import kotlin.random.Random - - -class ExampleUnitTest { - @Test - - fun exampleUsage() { - - val subjectId1 = "myUniqueId1" - val subjectId2 = "myUniqueId2" - - // Creation of two unprotected templates (randomly generated) - val unprotectedTemplate1 = DoubleArray(512) { Random.nextDouble() - 0.5 } - val unprotectedTemplate2 = DoubleArray(512) { Random.nextDouble() - 0.5 } - - // Custom parameters - PolyProtect.POLYNOMIALDEGREE = 7 - PolyProtect.COEFFICIENTABSMAX = 100 - PolyProtect.OVERLAP = 2 - - // Generation of subject-specific secret parameters (auxiliary data) - val secrets1 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId1) - val secrets2 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId2) - - assert(secrets1::class.simpleName == "secretAuxDataRecord") - {"Secrets should be objects of the secretAuxDataRecord class."} - assert(secrets1.subjectId::class.simpleName == "String") - {"The subjectId should be a String."} - assert(secrets1.coefficients::class.simpleName == "IntArray") - {"The coefficients should be an IntArray."} - assert(secrets1.exponents::class.simpleName == "IntArray") - {"The exponents should be an IntArray."} - - assert(unprotectedTemplate1::class.simpleName == "DoubleArray") - {"The unprotected template should be a DoubleArray."} - - /* - PolyProtect tranformation: the unprotected templates are transformed using the - subject-specific secret auxiliary data - */ - val protectedTemplate1 = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate1, - subjectSecretAuxData = secrets1) - val protectedTemplate2 = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate2, - subjectSecretAuxData = secrets2) - - // Score in the [0, 1] range based on cosine similarity: 1 = perfect match - val score = PolyProtect.computeScore(protectedTemplate1.protectedTemplate, - protectedTemplate2.protectedTemplate) - - } -} \ No newline at end of file diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt new file mode 100644 index 0000000..b868a1e --- /dev/null +++ b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt @@ -0,0 +1,57 @@ +package com.simprints.biometrics_simpolyprotect +import PolyProtect +import org.junit.Test +import kotlin.random.Random + + +class PolyProtectIntegrationTest { + @Test + + fun `PolyProtect operations in sequence`() { + + val subjectId1 = "myUniqueId1" + val subjectId2 = "myUniqueId2" + + // Creation of two unprotected templates (randomly generated) + val unprotectedTemplate1 = DoubleArray(512) { Random.nextDouble() - 0.5 } + val unprotectedTemplate2 = DoubleArray(512) { Random.nextDouble() - 0.5 } + + val polynomialDegree = 7 + val coefficientAbsMax = 100 + val overlap = 2 + + assert((polynomialDegree <= 14) and (polynomialDegree >= 5)) + {"Assertion failed: the degree of the polynomial should be >=5 and <=14."} + + assert((coefficientAbsMax > 0)) + {"Assertion failed: the maximum coefficient value should be a positive integer."} + + assert((overlap >= 0) and (overlap <= polynomialDegree-1)) + {"Assertion failed: the overlap value should be >=0 and <= polynomial degree - 1."} + + // Custom parameters + PolyProtect.POLYNOMIALDEGREE = polynomialDegree + PolyProtect.COEFFICIENTABSMAX = coefficientAbsMax + PolyProtect.OVERLAP = overlap + + // Generation of subject-specific secret parameters (auxiliary data) + val secrets1 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId1) + val secrets2 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId2) + + /* + PolyProtect tranformation: the unprotected templates are transformed using the + subject-specific secret auxiliary data + */ + val protectedRecord1 = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate1, + subjectSecretAuxData = secrets1) + val protectedRecord2 = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate2, + subjectSecretAuxData = secrets2) + + // Score in the [0, 1] range based on cosine similarity: 1 = perfect match + val score = PolyProtect.computeScore(protectedRecord1.protectedTemplate, + protectedRecord2.protectedTemplate) + + assert(score in 0.0..1.0) { "Value $score should be between 0 and 1." } + + } +} \ No newline at end of file diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt new file mode 100644 index 0000000..2d6fe0a --- /dev/null +++ b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt @@ -0,0 +1,98 @@ +package com.simprints.biometrics_simpolyprotect +import PolyProtect +import org.junit.Test +import kotlin.random.Random + + +class PolyProtectUnitTest { + + @Test + fun `test generate secret auxiliary data record`() { + + val subjectId1 = "myUniqueId1" + + // Custom parameters (relevant to the generation of secret auxiliary data record) + PolyProtect.POLYNOMIALDEGREE = 7 + PolyProtect.COEFFICIENTABSMAX = 100 + + // Generation of subject-specific secret parameters (auxiliary data) + val secrets1 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId1) + + assert(secrets1::class.simpleName == "secretAuxDataRecord") + {"Secrets should be objects of the secretAuxDataRecord class."} + assert(secrets1.subjectId::class.simpleName == "String") + {"The subjectId should be a String."} + assert(secrets1.coefficients::class.simpleName == "IntArray") + {"The coefficients should be an IntArray."} + assert(secrets1.exponents::class.simpleName == "IntArray") + {"The exponents should be an IntArray."} + } + + @Test + fun `test transform a template`() { + + val subjectId = "myUniqueId" + + // Dummy coefficients + val coefficients = intArrayOf(18, -26, 30, 4, 59, 10, -91) + + // Check that all elements are within the appropriate range. + assert(coefficients.all { it in -PolyProtect.COEFFICIENTABSMAX..PolyProtect.COEFFICIENTABSMAX }) + { "The coefficient values should not be outside the allowed range." } + + // Check that the array does not contain 0 + assert(coefficients.none { it == 0 }) + { "The coefficient values must not be 0." } + + // Dummy exponents + val exponents = intArrayOf(1, 5, 3, 4, 6, 2, 7) + + assert(exponents.sortedArray().contentEquals(IntArray(exponents.size) { it + 1 })) + { "The exponents should include values from 1 to PolyProtect.POLYNOMIALDEGREE." } + + val subjectSecretAuxData = PolyProtect.secretAuxDataRecord(subjectId = subjectId, + coefficients = coefficients, + exponents = exponents) + + val overlapValue = 2 + + assert((0 < overlapValue) and (overlapValue < PolyProtect.POLYNOMIALDEGREE)) + { "The overlap value should be >=0 and <= polynomial degree - 1." } + + // Custom parameters (relevant to the generation of secret auxiliary data record) + PolyProtect.OVERLAP = overlapValue + + // Creation of two unprotected templates (randomly generated) + val unprotectedTemplate = DoubleArray(512) { Random.nextDouble() - 0.5 } + + /* + PolyProtect tranformation: the unprotected templates are transformed using the + subject-specific secret auxiliary data + */ + val protectedRecord = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate, + subjectSecretAuxData = subjectSecretAuxData) + + assert(protectedRecord.subjectId::class.simpleName == "String") + {"The coefficients should be an IntArray."} + + assert(protectedRecord.protectedTemplate::class.simpleName == "DoubleArray") + {"The coefficients should be an IntArray."} + + } + + @Test + fun `test compute scores`() { + + val randomArray1 = DoubleArray(512) { Random.nextDouble() - 0.5 } + val randomArray2 = DoubleArray(512) { Random.nextDouble() - 0.5 } + + assert(randomArray1.size == randomArray2.size) + { "The arrays should have the same size." } + + val score = PolyProtect.computeScore(randomArray1, randomArray2) + + assert(score in 0.0..1.0) { "Value $score should be between 0 and 1." } + + } + +} \ No newline at end of file From 403c5f211b063b6f9cd2c582d7858c5a1c9b4fd9 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Fri, 29 Nov 2024 12:31:19 +0200 Subject: [PATCH 4/6] Change source code to be idiomatic kotlin * Changed class and property names to follow the code style conventions * Removed unnecessary subjectId value that was being passed around for no apparent reason * Refactored global static variable use to be instance properties instead * Updated tests to do more meaningful assertions using correct functions --- README.md | 18 +- .../biometrics_simpolyprotect/AuxData.kt | 10 ++ .../biometrics_simpolyprotect/PolyProtect.kt | 170 +++++++----------- .../SimilarityScore.kt | 24 +++ .../ComputeSimilarityScoreTest.kt | 43 +++++ .../PolyProtectIntegrationTest.kt | 57 ------ .../PolyProtectUnitTest.kt | 109 +++++------ 7 files changed, 190 insertions(+), 241 deletions(-) create mode 100644 src/main/kotlin/biometrics_simpolyprotect/AuxData.kt create mode 100644 src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt create mode 100644 src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt delete mode 100644 src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt diff --git a/README.md b/README.md index 7df01e4..6b5423a 100644 --- a/README.md +++ b/README.md @@ -16,17 +16,15 @@ In this section we provide a description of the ```PolyProtect``` class. ## Data classes -- ```secretAuxDataRecord(val subjectId: String, val coefficients: IntArray, val exponents: IntArray)```: simple data class to handle the subject-specific secret parameters. The first element represents the subject's ID, the second and third element are subject-specific secret parameters used in the PolyProtect transformation, respectively corresponding to the coefficients and exponents of a polynomial function. - -- ```protectedTemplateRecord(val subjectId: String, val protectedTemplate: DoubleArray)```: simple data class to handle the protected templates. The first element represents the subject's ID, and the second element is the protected template, _i.e._ output of the PolyProtect transformation. +- ```AuxData(val coefficients: IntArray, val exponents: IntArray)```: simple data class to handle the subject-specific secret parameters. The first element represents the subject's ID, the second and third element are subject-specific secret parameters used in the PolyProtect transformation, respectively corresponding to the coefficients and exponents of a polynomial function. ## Methods -- ```transformTemplate(subjectId: String, unprotectedTemplate: DoubleArray, subjectSecretAuxData: secretAuxDataRecord): protectedTemplateRecord```: method to transform the unprotected template using the PolyProtect algorithm. Internally, the method uses the ```_OVERLAP``` variable. +- ```transformTemplate(unprotectedTemplate: DoubleArray, subjectSecretAuxData: AuxData): DoubleArray```: method to transform the unprotected template using the PolyProtect algorithm. Internally, the method uses the ```_OVERLAP``` variable. -- ```generateSecretAuxDataRecord(subjectId: String): secretAuxDataRecord```: method to randomly generate secret coefficients and exponents (auxiiliary data). Internally, the method uses the ```_POLYNOMIALDEGREE``` and ```_COEFFICIENTABSMAX``` variables. +- ```generateAuxData(): AuxData```: method to randomly generate secret coefficients and exponents (auxiiliary data). Internally, the method uses the ```_POLYNOMIALDEGREE``` and ```_COEFFICIENTABSMAX``` variables. -- ```computeScore(array1: DoubleArray, array2: DoubleArray): Double```: method to generate a score in the [0,1] range based on cosine distance. +- ```computeSimilarityScore(array1: DoubleArray, array2: DoubleArray): Double```: method to generate a score in the [0,1] range based on cosine distance. ## Variables @@ -48,7 +46,7 @@ In this section we report the sequence diagrams are included for integrating Pol ## Enrolment -During a subject's enrolment, firstly, a set of coefficients and exponents (```secretAuxDataRecord``` data class) are randomly generated by the ```generateSecretAuxDataRecord``` method. +During a subject's enrolment, firstly, a set of coefficients and exponents (```AuxData``` data class) are randomly generated by the ```generateAuxData``` method. Secondly, it is necessary to check that the same coefficients and exponents have not been assigned to previously enrolled subjects by reading the auxiliary data database, where they are stored. Once this check is completed, the auxiliary data can be saved in the database. Then, the unprotected template can be transformed with PolyProtect using the ```transformTemplate``` method. The output of this operation is the protected template, which can be saved in a separate database for protected templates. The unprotected template should NOT be stored. @@ -59,9 +57,9 @@ The unprotected template should NOT be stored. ## Verification -For verification, the ```subjectId``` of the subject whose identity is about to be verified is known. Consequently, we read the auxiliary data database to obtain the corresponding ```secretAuxDataRecord```. -Then, we transform the unprotected query template so that it can be compared directly with the protected reference (enrolment) template with the ```transformTemplate``` method. Consequently, we read the protected template database and we return the ```protectedTemplateRecord```. -The matching of the two templates is carried out using the ```computeScore``` method, which returns a score in the [0, 1] range, being 1 a perfect match. +For verification, the ```subjectId``` of the subject whose identity is about to be verified is known. Consequently, we read the auxiliary data database to obtain the corresponding ```AuxData```. +Then, we transform the unprotected query template so that it can be compared directly with the protected reference (enrolment) template with the ```transformTemplate``` method. Consequently, we read the protected template database and we return the protected template. +The matching of the two templates is carried out using the ```computeSimilarityScore``` method, which returns a score in the [0, 1] range, being 1 a perfect match. ![alt text](images/Verification.png) diff --git a/src/main/kotlin/biometrics_simpolyprotect/AuxData.kt b/src/main/kotlin/biometrics_simpolyprotect/AuxData.kt new file mode 100644 index 0000000..62f45a6 --- /dev/null +++ b/src/main/kotlin/biometrics_simpolyprotect/AuxData.kt @@ -0,0 +1,10 @@ +package biometrics_simpolyprotect + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +data class AuxData( + val coefficients: IntArray, + val exponents: IntArray, +) : Parcelable diff --git a/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt b/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt index fbcd5c2..43ac12f 100644 --- a/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt +++ b/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt @@ -1,118 +1,76 @@ -import kotlin.math.pow -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - - -class PolyProtect { - - @Parcelize - data class secretAuxDataRecord( - val subjectId: String, - val coefficients: IntArray, - val exponents: IntArray, - ) : Parcelable - - @Parcelize - data class protectedTemplateRecord( - val subjectId: String, - val protectedTemplate: DoubleArray - ) : Parcelable - - - companion object { - - private var _COEFFICIENTABSMAX: Int = 100 - private var _POLYNOMIALDEGREE: Int = 7 - private var _OVERLAP: Int = 2 - - var POLYNOMIALDEGREE: Int - get() = _POLYNOMIALDEGREE - set(value) { - assert((value <= 14) and (value >= 5)) - {"Assertion failed: the degree of the polynomial should be >=5 and <=14."} - _POLYNOMIALDEGREE = value - } - - var COEFFICIENTABSMAX: Int - get() = _COEFFICIENTABSMAX - set(value) { - assert((value > 0)) - {"Assertion failed: the maximum coefficient value should be a positive integer."} - _COEFFICIENTABSMAX = value - } - - var OVERLAP: Int - get() = _OVERLAP - set(value) { - assert((value >= 0) and (value <= _POLYNOMIALDEGREE-1)) - {"Assertion failed: the overlap value should be >=0 and <= polynomial degree - 1."} - _OVERLAP = value - } +package biometrics_simpolyprotect - fun transformTemplate(unprotectedTemplate: DoubleArray, - subjectSecretAuxData: secretAuxDataRecord - ): protectedTemplateRecord { - - val (_, c, e) = subjectSecretAuxData // For convenience - assert(e.size == c.size) { "Auxiliary data sizes must be equal." } - - assert(e.size == POLYNOMIALDEGREE) { "Auxiliary data sizes must be equal to PolyProtect.POLYNOMIALDEGREE." } +import kotlin.math.pow - val stepSize = e.size - OVERLAP - val eIndices = e.indices +class PolyProtect( + private val polynomialDegree: Int = POLYNOMIAL_DEGREE_DEFAULT, + private val coefficientAbsMax: Int = COEFFICIENT_ABS_MAX_DEFAULT, + private val overlap: Int = OVERLAP_DEFAULT, +) { - val protectedTemplate = mutableListOf() - for (templateIndex in 0..(unprotectedTemplate.lastIndex - OVERLAP) step stepSize) { - val s = eIndices - .map { i -> - // If the target element is out of bounds, consider it 0 since 0^n==0 - // This would be the same as padding the provided array up to certain size - if (templateIndex + i > unprotectedTemplate.lastIndex) { - 0.0 - } else { - unprotectedTemplate[templateIndex + i].pow(e[i]).times(c[i]) - } - } - .sum() - protectedTemplate.add(s) - } - return protectedTemplateRecord(subjectSecretAuxData.subjectId, protectedTemplate.toDoubleArray()) + init { + require(polynomialDegree in 5..14) { + "Assertion failed: the degree of the polynomial should be >=5 and <=14." } + require(coefficientAbsMax > 0) { + "Assertion failed: the maximum coefficient value should be a positive integer." + } + require(overlap in 0..(polynomialDegree - 1)) { + "Assertion failed: the overlap value should be >=0 and <= polynomial degree - 1." + } + } - fun generateSecretAuxDataRecord(subjectId: String): secretAuxDataRecord { - // Create a list that excludes 0, combining the ranges (-coefficientAbsMax to -1) and (1 to coefficientAbsMax) - val coefficientRange = (-COEFFICIENTABSMAX until 0).toList() + (1..COEFFICIENTABSMAX).toList() - // Shuffle the list randomly - val shuffledList = coefficientRange.shuffled() - // Return a sublist of the first 'polynomialDegree' elements - val coefficients = shuffledList.take(POLYNOMIALDEGREE).toIntArray() - - // Create a list of exponents (1 to polynomialDegree) - val exponentRange = (1..POLYNOMIALDEGREE) - // Shuffle the list randomly - val exponents = exponentRange.shuffled().toIntArray() + fun transformTemplate( + unprotectedTemplate: DoubleArray, + auxData: AuxData + ): DoubleArray { + val (coefficients, exponents) = auxData // For convenience + require(exponents.size == coefficients.size) { "Auxiliary data sizes must be equal." } + require(exponents.size == polynomialDegree) { + "Auxiliary data sizes must be equal to polynomial degree." + } - return secretAuxDataRecord(subjectId, coefficients, exponents) + val stepSize = exponents.size - overlap + + val protectedTemplate = mutableListOf() + for (templateIndex in 0..(unprotectedTemplate.lastIndex - overlap) step stepSize) { + val s = exponents.indices.map { i -> + // If the target element is out of bounds, consider it 0 since 0^n==0 + // This would be the same as padding the provided array up to certain size + if (templateIndex + i > unprotectedTemplate.lastIndex) { + 0.0 + } else { + unprotectedTemplate[templateIndex + i] + .pow(exponents[i]) + .times(coefficients[i]) + } + }.sum() + protectedTemplate.add(s) } + return protectedTemplate.toDoubleArray() + } - fun computeScore(array1: DoubleArray, array2: DoubleArray): Double { - require(array1.size == array2.size) { "Arrays must be of the same size." } + fun generateAuxData(): AuxData { + // Create a list that excludes 0, combining the ranges (-coefficientAbsMax to -1) and (1 to coefficientAbsMax) + val coefficientRange = + (-coefficientAbsMax until 0).toList() + (1..coefficientAbsMax).toList() + // Shuffle the list randomly + val shuffledList = coefficientRange.shuffled() + // Return a sublist of the first 'polynomialDegree' elements + val coefficients = shuffledList.take(polynomialDegree).toIntArray() + + // Create a list of exponents (1 to polynomialDegree) + val exponentRange = (1..polynomialDegree) + // Shuffle the list randomly + val exponents = exponentRange.shuffled().toIntArray() + + return AuxData(coefficients, exponents) + } - // Calculate the dot product of array1 and array2 - val dotProduct = array1.zip(array2) { x, y -> x * y }.sum() - // Calculate the magnitudes of array1 and array2 - val magnitudeA = Math.sqrt(array1.map { it * it }.sum()) - val magnitudeB = Math.sqrt(array2.map { it * it }.sum()) - // Check to avoid division by zero - if (magnitudeA == 0.0 || magnitudeB == 0.0) { - throw IllegalArgumentException("Arrays must not be zero vectors.") - } - // Calculate cosine similarity - val cosineSimilarity = dotProduct / (magnitudeA * magnitudeB) - // Calculate cosine distance - val cosineDistance = 1 - cosineSimilarity + companion object { - return (2.0-1.0*cosineDistance)/2 - } + private const val COEFFICIENT_ABS_MAX_DEFAULT: Int = 100 + private const val POLYNOMIAL_DEGREE_DEFAULT: Int = 7 + private const val OVERLAP_DEFAULT: Int = 2 } } diff --git a/src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt b/src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt new file mode 100644 index 0000000..4ab5cb1 --- /dev/null +++ b/src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt @@ -0,0 +1,24 @@ +package biometrics_simpolyprotect + +import kotlin.math.sqrt + +fun computeSimilarityScore( + array1: DoubleArray, + array2: DoubleArray, +): Double { + require(array1.size == array2.size) { "Arrays must be of the same size." } + + // Calculate the dot product of array1 and array2 + val dotProduct = array1.zip(array2) { x, y -> x * y }.sum() + // Calculate the magnitudes of array1 and array2 + val magnitudeA = sqrt(array1.map { it * it }.sum()) + val magnitudeB = sqrt(array2.map { it * it }.sum()) + // Check to avoid division by zero + check(magnitudeA > 0.0 && magnitudeB > 0.0) { "Arrays must not be zero vectors." } + // Calculate cosine similarity + val cosineSimilarity = dotProduct / (magnitudeA * magnitudeB) + // Calculate cosine distance + val cosineDistance = 1 - cosineSimilarity + + return (2.0 - 1.0 * cosineDistance) / 2 +} diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt b/src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt new file mode 100644 index 0000000..c23dc08 --- /dev/null +++ b/src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt @@ -0,0 +1,43 @@ +package com.simprints.biometrics_simpolyprotect + +import biometrics_simpolyprotect.PolyProtect +import biometrics_simpolyprotect.computeSimilarityScore +import org.junit.Assert.assertTrue +import org.junit.Test +import kotlin.random.Random + + +class ComputeSimilarityScoreTest { + + @Test + fun `similarity between two protected templates should be between 0 and 1`() { + // Creation of two unprotected templates (randomly generated) + val unprotectedTemplate1 = DoubleArray(512) { Random.nextDouble() - 0.5 } + val unprotectedTemplate2 = DoubleArray(512) { Random.nextDouble() - 0.5 } + + // Custom parameters + val polyProtect = PolyProtect( + polynomialDegree = 7, + coefficientAbsMax = 100, + overlap = 2, + ) + + // Generation of subject-specific secret parameters (auxiliary data) + val secrets1 = polyProtect.generateAuxData() + val secrets2 = polyProtect.generateAuxData() + + // PolyProtect transformation: the unprotected templates are transformed using the + // subject-specific secret auxiliary data + val protectedRecord1 = polyProtect.transformTemplate( + unprotectedTemplate = unprotectedTemplate1, auxData = secrets1 + ) + val protectedRecord2 = polyProtect.transformTemplate( + unprotectedTemplate = unprotectedTemplate2, auxData = secrets2 + ) + + // Score in the [0, 1] range based on cosine similarity: 1 = perfect match + val score = computeSimilarityScore(protectedRecord1, protectedRecord2) + + assertTrue("Score should be between 0 and 1.", score in 0.0..1.0) + } +} diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt deleted file mode 100644 index b868a1e..0000000 --- a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectIntegrationTest.kt +++ /dev/null @@ -1,57 +0,0 @@ -package com.simprints.biometrics_simpolyprotect -import PolyProtect -import org.junit.Test -import kotlin.random.Random - - -class PolyProtectIntegrationTest { - @Test - - fun `PolyProtect operations in sequence`() { - - val subjectId1 = "myUniqueId1" - val subjectId2 = "myUniqueId2" - - // Creation of two unprotected templates (randomly generated) - val unprotectedTemplate1 = DoubleArray(512) { Random.nextDouble() - 0.5 } - val unprotectedTemplate2 = DoubleArray(512) { Random.nextDouble() - 0.5 } - - val polynomialDegree = 7 - val coefficientAbsMax = 100 - val overlap = 2 - - assert((polynomialDegree <= 14) and (polynomialDegree >= 5)) - {"Assertion failed: the degree of the polynomial should be >=5 and <=14."} - - assert((coefficientAbsMax > 0)) - {"Assertion failed: the maximum coefficient value should be a positive integer."} - - assert((overlap >= 0) and (overlap <= polynomialDegree-1)) - {"Assertion failed: the overlap value should be >=0 and <= polynomial degree - 1."} - - // Custom parameters - PolyProtect.POLYNOMIALDEGREE = polynomialDegree - PolyProtect.COEFFICIENTABSMAX = coefficientAbsMax - PolyProtect.OVERLAP = overlap - - // Generation of subject-specific secret parameters (auxiliary data) - val secrets1 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId1) - val secrets2 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId2) - - /* - PolyProtect tranformation: the unprotected templates are transformed using the - subject-specific secret auxiliary data - */ - val protectedRecord1 = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate1, - subjectSecretAuxData = secrets1) - val protectedRecord2 = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate2, - subjectSecretAuxData = secrets2) - - // Score in the [0, 1] range based on cosine similarity: 1 = perfect match - val score = PolyProtect.computeScore(protectedRecord1.protectedTemplate, - protectedRecord2.protectedTemplate) - - assert(score in 0.0..1.0) { "Value $score should be between 0 and 1." } - - } -} \ No newline at end of file diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt index 2d6fe0a..ebe1560 100644 --- a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt +++ b/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt @@ -1,5 +1,10 @@ package com.simprints.biometrics_simpolyprotect -import PolyProtect + +import biometrics_simpolyprotect.AuxData +import biometrics_simpolyprotect.PolyProtect +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue import org.junit.Test import kotlin.random.Random @@ -8,91 +13,59 @@ class PolyProtectUnitTest { @Test fun `test generate secret auxiliary data record`() { - - val subjectId1 = "myUniqueId1" - // Custom parameters (relevant to the generation of secret auxiliary data record) - PolyProtect.POLYNOMIALDEGREE = 7 - PolyProtect.COEFFICIENTABSMAX = 100 + val polyProtect = PolyProtect( + polynomialDegree = 7, + coefficientAbsMax = 100, + ) // Generation of subject-specific secret parameters (auxiliary data) - val secrets1 = PolyProtect.generateSecretAuxDataRecord(subjectId = subjectId1) + val secrets1 = polyProtect.generateAuxData() + + assertEquals( + "Exponents count is same as provided polynomial degree", 7, secrets1.exponents.size + ) + assertTrue( + "The exponents should include values from 1 to PolyProtect.POLYNOMIALDEGREE.", + secrets1.exponents.sortedArray().contentEquals(IntArray(7) { it + 1 }) + ) + + assertEquals( + "Coefficient count is same as provided polynomial degree", 7, secrets1.coefficients.size + ) + assertTrue("The coefficient values should not be outside the allowed range", + secrets1.coefficients.all { it in -100..100 }) + assertTrue("The coefficient values must not be 0", secrets1.coefficients.none { it == 0 }) - assert(secrets1::class.simpleName == "secretAuxDataRecord") - {"Secrets should be objects of the secretAuxDataRecord class."} - assert(secrets1.subjectId::class.simpleName == "String") - {"The subjectId should be a String."} - assert(secrets1.coefficients::class.simpleName == "IntArray") - {"The coefficients should be an IntArray."} - assert(secrets1.exponents::class.simpleName == "IntArray") - {"The exponents should be an IntArray."} } @Test fun `test transform a template`() { - - val subjectId = "myUniqueId" - // Dummy coefficients val coefficients = intArrayOf(18, -26, 30, 4, 59, 10, -91) - - // Check that all elements are within the appropriate range. - assert(coefficients.all { it in -PolyProtect.COEFFICIENTABSMAX..PolyProtect.COEFFICIENTABSMAX }) - { "The coefficient values should not be outside the allowed range." } - - // Check that the array does not contain 0 - assert(coefficients.none { it == 0 }) - { "The coefficient values must not be 0." } - // Dummy exponents val exponents = intArrayOf(1, 5, 3, 4, 6, 2, 7) - assert(exponents.sortedArray().contentEquals(IntArray(exponents.size) { it + 1 })) - { "The exponents should include values from 1 to PolyProtect.POLYNOMIALDEGREE." } - - val subjectSecretAuxData = PolyProtect.secretAuxDataRecord(subjectId = subjectId, - coefficients = coefficients, - exponents = exponents) - - val overlapValue = 2 - - assert((0 < overlapValue) and (overlapValue < PolyProtect.POLYNOMIALDEGREE)) - { "The overlap value should be >=0 and <= polynomial degree - 1." } + val subjectSecretAuxData = AuxData( + coefficients = coefficients, exponents = exponents + ) // Custom parameters (relevant to the generation of secret auxiliary data record) - PolyProtect.OVERLAP = overlapValue + val polyProtect = PolyProtect( + polynomialDegree = 7, + coefficientAbsMax = 100, + overlap = 2, + ) // Creation of two unprotected templates (randomly generated) val unprotectedTemplate = DoubleArray(512) { Random.nextDouble() - 0.5 } - /* - PolyProtect tranformation: the unprotected templates are transformed using the - subject-specific secret auxiliary data - */ - val protectedRecord = PolyProtect.transformTemplate(unprotectedTemplate = unprotectedTemplate, - subjectSecretAuxData = subjectSecretAuxData) - - assert(protectedRecord.subjectId::class.simpleName == "String") - {"The coefficients should be an IntArray."} - - assert(protectedRecord.protectedTemplate::class.simpleName == "DoubleArray") - {"The coefficients should be an IntArray."} + // PolyProtect trandformation: the unprotected templates are transformed using the + // subject-specific secret auxiliary data + val protectedTemplate = polyProtect.transformTemplate( + unprotectedTemplate = unprotectedTemplate, auxData = subjectSecretAuxData + ) + assertFalse(unprotectedTemplate.contentEquals(protectedTemplate)) } - - @Test - fun `test compute scores`() { - - val randomArray1 = DoubleArray(512) { Random.nextDouble() - 0.5 } - val randomArray2 = DoubleArray(512) { Random.nextDouble() - 0.5 } - - assert(randomArray1.size == randomArray2.size) - { "The arrays should have the same size." } - - val score = PolyProtect.computeScore(randomArray1, randomArray2) - - assert(score in 0.0..1.0) { "Value $score should be between 0 and 1." } - - } - -} \ No newline at end of file +} From 4a55da7fe2036c671827a7ed251a9caf1bf4a269 Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Fri, 29 Nov 2024 12:42:46 +0200 Subject: [PATCH 5/6] Package and gradle setup * Changed the package to adhere to java/kotlin conventions to avoid build tooling issues in future * Cleaned up unnecessary android-specific cofnig from build gradle file --- build.gradle.kts | 10 ++-------- settings.gradle.kts | 8 +------- src/main/AndroidManifest.xml | 2 +- src/main/kotlin/biometrics_simpolyprotect/AuxData.kt | 10 ---------- .../com/simprints/biometrics/polyprotect/AuxData.kt | 6 ++++++ .../simprints/biometrics/polyprotect}/PolyProtect.kt | 2 +- .../biometrics/polyprotect}/SimilarityScore.kt | 2 +- .../polyprotect}/ComputeSimilarityScoreTest.kt | 4 +--- .../polyprotect}/PolyProtectUnitTest.kt | 4 +--- 9 files changed, 14 insertions(+), 34 deletions(-) delete mode 100644 src/main/kotlin/biometrics_simpolyprotect/AuxData.kt create mode 100644 src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt rename src/main/kotlin/{biometrics_simpolyprotect => com/simprints/biometrics/polyprotect}/PolyProtect.kt (98%) rename src/main/kotlin/{biometrics_simpolyprotect => com/simprints/biometrics/polyprotect}/SimilarityScore.kt (94%) rename src/test/java/com/simprints/{biometrics_simpolyprotect => biometrics/polyprotect}/ComputeSimilarityScoreTest.kt (90%) rename src/test/java/com/simprints/{biometrics_simpolyprotect => biometrics/polyprotect}/PolyProtectUnitTest.kt (94%) diff --git a/build.gradle.kts b/build.gradle.kts index 1d0ef06..d1facee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,12 +1,11 @@ plugins { alias(libs.plugins.android.library) alias(libs.plugins.kotlin.android) - id("org.jetbrains.kotlin.plugin.parcelize") version "2.1.0-RC" `maven-publish` } -val projectGroupId = "com.simprints" -val projectArtifactId = "biometrics_simpolyprotect" +val projectGroupId = "com.simprints.biometrics" +val projectArtifactId = "polyprotect" val projectVersion = "2024.4.0" android { @@ -25,12 +24,7 @@ android { } dependencies { - implementation(libs.androidx.core.ktx) - implementation(libs.androidx.appcompat) - implementation(libs.material) testImplementation(libs.junit) - androidTestImplementation(libs.androidx.junit) - androidTestImplementation(libs.androidx.espresso.core) } publishing { diff --git a/settings.gradle.kts b/settings.gradle.kts index 9c416e4..40add2a 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,12 +1,6 @@ pluginManagement { repositories { - google { - content { - includeGroupByRegex("com\\.android.*") - includeGroupByRegex("com\\.google.*") - includeGroupByRegex("androidx.*") - } - } + google() mavenCentral() gradlePluginPortal() } diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index ae434a1..f55de8c 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/src/main/kotlin/biometrics_simpolyprotect/AuxData.kt b/src/main/kotlin/biometrics_simpolyprotect/AuxData.kt deleted file mode 100644 index 62f45a6..0000000 --- a/src/main/kotlin/biometrics_simpolyprotect/AuxData.kt +++ /dev/null @@ -1,10 +0,0 @@ -package biometrics_simpolyprotect - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AuxData( - val coefficients: IntArray, - val exponents: IntArray, -) : Parcelable diff --git a/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt b/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt new file mode 100644 index 0000000..bad52ec --- /dev/null +++ b/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt @@ -0,0 +1,6 @@ +package com.simprints.biometrics.polyprotect + +data class AuxData( + val coefficients: IntArray, + val exponents: IntArray, +) diff --git a/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt b/src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt similarity index 98% rename from src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt rename to src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt index 43ac12f..9773e43 100644 --- a/src/main/kotlin/biometrics_simpolyprotect/PolyProtect.kt +++ b/src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt @@ -1,4 +1,4 @@ -package biometrics_simpolyprotect +package com.simprints.biometrics.polyprotect import kotlin.math.pow diff --git a/src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt b/src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt similarity index 94% rename from src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt rename to src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt index 4ab5cb1..cb8d3fa 100644 --- a/src/main/kotlin/biometrics_simpolyprotect/SimilarityScore.kt +++ b/src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt @@ -1,4 +1,4 @@ -package biometrics_simpolyprotect +package com.simprints.biometrics.polyprotect import kotlin.math.sqrt diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt b/src/test/java/com/simprints/biometrics/polyprotect/ComputeSimilarityScoreTest.kt similarity index 90% rename from src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt rename to src/test/java/com/simprints/biometrics/polyprotect/ComputeSimilarityScoreTest.kt index c23dc08..4cccb62 100644 --- a/src/test/java/com/simprints/biometrics_simpolyprotect/ComputeSimilarityScoreTest.kt +++ b/src/test/java/com/simprints/biometrics/polyprotect/ComputeSimilarityScoreTest.kt @@ -1,7 +1,5 @@ -package com.simprints.biometrics_simpolyprotect +package com.simprints.biometrics.polyprotect -import biometrics_simpolyprotect.PolyProtect -import biometrics_simpolyprotect.computeSimilarityScore import org.junit.Assert.assertTrue import org.junit.Test import kotlin.random.Random diff --git a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt b/src/test/java/com/simprints/biometrics/polyprotect/PolyProtectUnitTest.kt similarity index 94% rename from src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt rename to src/test/java/com/simprints/biometrics/polyprotect/PolyProtectUnitTest.kt index ebe1560..0354103 100644 --- a/src/test/java/com/simprints/biometrics_simpolyprotect/PolyProtectUnitTest.kt +++ b/src/test/java/com/simprints/biometrics/polyprotect/PolyProtectUnitTest.kt @@ -1,7 +1,5 @@ -package com.simprints.biometrics_simpolyprotect +package com.simprints.biometrics.polyprotect -import biometrics_simpolyprotect.AuxData -import biometrics_simpolyprotect.PolyProtect import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue From 42b49a7c3ffe901be7bcc71d91fed1583a561f0b Mon Sep 17 00:00:00 2001 From: Sergejs Luhmirins Date: Fri, 29 Nov 2024 14:13:53 +0200 Subject: [PATCH 6/6] Embed method and configuration documentation in KDocs in the sources instead of README --- README.md | 74 ++++++------------- .../biometrics/polyprotect/AuxData.kt | 7 ++ .../biometrics/polyprotect/PolyProtect.kt | 26 +++++++ .../biometrics/polyprotect/SimilarityScore.kt | 8 ++ 4 files changed, 65 insertions(+), 50 deletions(-) diff --git a/README.md b/README.md index 6b5423a..b79524c 100644 --- a/README.md +++ b/README.md @@ -4,41 +4,15 @@ In a nutshell, PolyProtect combines subject-specific secret information (also ca The auxiliary data consist of two parts: the coefficients (an array of signed integers excluding zero) and the exponents (an array of positive signed integers). The two arrays have the same length and they are used to perform a polynomial transformation on several slices of the input (unprotected) face template. The output of this transformation is the protected template. -# Repository tree directory +# PolyProtect configuration -The ```src``` folder contains two subfolders: -- ```src/main```, in which the ```PolyProtect``` class is defined. -- ```src/test/java/com/simprints/biometrics_simpolyprotect```, which contains a simple unit test with dummy templates. +- ```polynomialDegree: Int = 7```: degree of the polynomial used in the PolyProtect transformation. The minimum value which would provide any security is 5, since there is no closed form algebraic expression for solving polynomials of degree 5 or higher with arbitrary coefficients. + This variable has an important impact on the system design as it defines the maximum number of subjects that can be enrolled with different sets of exponents, which are *half* of the secret on which PolyProtect relies (the other half would be given by the polynomial coefficients). Such number is computed as the factorial of ```polynomialDegree```. For example, if ```polynomialDegree = 7```, then ```polynomialDegree!``` = 5040. That is, with ```polynomialDegree``` being 7 the maximum number of subjects that can be enrolled in the database is 5040. The maximum value for ```polynomialDegree``` is 14. Although, the upper limit of ```polynomialDegree``` is bound by the fact that the embeddings produced by NN-based feature extractors often consist of floating point values smaller than 1, which would cause large powers to effectively obliterate certain embedding elements during the PolyProtect transformation. However, 14! = 87,178,291,200 which would cover over ten times the entire world population. + At the bottom of this page it is possible to find a [table of factorials](#table-of-factorials) for the [5,14] range. In the original paper, ```polynomialDegree``` is named *m*. -# Class overview +- ```coefficientAbsMax: Int = 100```: range limit for the coefficients of the polynomial used in the PolyProtect transformation. The coefficients will be drawn from the [-```coefficientAbsMax```, ```coefficientAbsMax```] range excluding 0. -In this section we provide a description of the ```PolyProtect``` class. - -## Data classes - -- ```AuxData(val coefficients: IntArray, val exponents: IntArray)```: simple data class to handle the subject-specific secret parameters. The first element represents the subject's ID, the second and third element are subject-specific secret parameters used in the PolyProtect transformation, respectively corresponding to the coefficients and exponents of a polynomial function. - -## Methods - -- ```transformTemplate(unprotectedTemplate: DoubleArray, subjectSecretAuxData: AuxData): DoubleArray```: method to transform the unprotected template using the PolyProtect algorithm. Internally, the method uses the ```_OVERLAP``` variable. - -- ```generateAuxData(): AuxData```: method to randomly generate secret coefficients and exponents (auxiiliary data). Internally, the method uses the ```_POLYNOMIALDEGREE``` and ```_COEFFICIENTABSMAX``` variables. - -- ```computeSimilarityScore(array1: DoubleArray, array2: DoubleArray): Double```: method to generate a score in the [0,1] range based on cosine distance. - -## Variables - -- ```_POLYNOMIALDEGREE: Int = 7```: degree of the polynomial used in the PolyProtect transformation. The minimum value which would provide any security is 5, since there is no closed form algebraic expression for solving polynomials of degree 5 or higher with arbitrary coefficients. - This variable has an important impact on the system design as it defines the maximum number of subjects that can be enrolled with different sets of exponents, which are *half* of the secret on which PolyProtect relies (the other half would be given by the polynomial coefficients). Such number is computed as the factorial of ```_POLYNOMIALDEGREE```. For example, if ```_POLYNOMIALDEGREE = 7```, then ```_POLYNOMIALDEGREE!``` = 5040. That is, with ```_POLYNOMIALDEGREE``` being 7 the maximum number of subjects that can be enrolled in the database is 5040. The maximum value for ```_POLYNOMIALDEGREE``` is 14. Although required with an ```assert``` in the setter, the upper limit of ```_POLYNOMIALDEGREE``` is bound by the fact that the embeddings produced by NN-based feature extractors often consist of floating point values smaller than 1, which would cause large powers to effectively obliterate certain embedding elements during the PolyProtect transformation. However, 14! = 87,178,291,200 which would cover over ten times the entire world population. - At the bottom of this page it is possible to find a [table of factorials](#table-of-factorials) for the [5,14] range. In the original paper, ```_POLYNOMIALDEGREE``` is named *m*. - -- ```_COEFFICIENTABSMAX: Int = 100```: range limit for the coefficients of the polynomial used in the PolyProtect transformation. The coefficients will be drawn from the [-```_COEFFICIENTABSMAX```, ```_COEFFICIENTABSMAX```] range excluding 0. - -- ```_OVERLAP: Int = 2```: overlap of the intervals of the unprotected template to transform using the PolyProtect polynomial transformation. It has an impact on the security _vs._ recognition accuracy tradeoff. Specifically, as you increase the ```_OVERLAP```, the accuracy of the system improves and the security of protected template decreases. It can go from 0 to ```_POLYNOMIALDEGREE``` - 1. The safest options would be: {0, 1, 2, 3}. - -**NOTE 1**: All three variables are provided with getter/setter methods. - -**NOTE 2**: The size of the protected templates depends on ```_POLYNOMIALDEGREE``` and ```_OVERLAP```. +- ```overlap: Int = 2```: overlap of the intervals of the unprotected template to transform using the PolyProtect polynomial transformation. It has an impact on the security _vs._ recognition accuracy tradeoff. Specifically, as you increase the ```overlap```, the accuracy of the system improves and the security of protected template decreases. It can go from 0 to ```polynomialDegree``` - 1. The safest options would be: {0, 1, 2, 3}. # Workflow @@ -57,33 +31,33 @@ The unprotected template should NOT be stored. ## Verification -For verification, the ```subjectId``` of the subject whose identity is about to be verified is known. Consequently, we read the auxiliary data database to obtain the corresponding ```AuxData```. +For verification, the ```AuxData``` of the subject whose identity is about to be verified is known. Consequently, we read the auxiliary data database to obtain the corresponding ```AuxData```. Then, we transform the unprotected query template so that it can be compared directly with the protected reference (enrolment) template with the ```transformTemplate``` method. Consequently, we read the protected template database and we return the protected template. -The matching of the two templates is carried out using the ```computeSimilarityScore``` method, which returns a score in the [0, 1] range, being 1 a perfect match. +The matching of the two templates is carried out using the ```computeSimilarityScore``` function, which returns a score in the [0, 1] range, being 1 a perfect match. ![alt text](images/Verification.png) ## Identification -For identification, the ```subjectId``` of the subject whose identity is about to be verified is not known. Consequently, it is necessary to consider all ```secretAuxDataRecord``` stored. For this reason, we implement a for loop in the sequence diagram below. First, we initialise a list in which the scores and the corresponding ```subjectIds``` can be stored. In each iteration of the for loop, one enrolled subject per time is considered. -Their ```secretAuxDataRecord``` is obtained from the database and used to transform the unprotected query template. -Then, the ```protectedTemplateRecord``` corresponding to the ```secretAuxDataRecord``` of this iteration is retrieved so that it can be compared directly with the protected query template. -The matching of the two templates is carried out using the ```computeScore``` method, which returns a score in the [0, 1] range, being 1 a perfect match. In each iteration, the score is appended to the previously created list together with the ```subjectId```. +For identification, the ```AuxData``` of the subject whose identity is about to be verified is not known. Consequently, it is necessary to consider all ```AuxData``` stored. For this reason, we implement a for loop in the sequence diagram below. First, we initialise a list in which the scores and the corresponding ```subjectIds``` can be stored. In each iteration of the for loop, one enrolled subject per time is considered. +Their ```AuxData``` is obtained from the database and used to transform the unprotected query template. +Then, the protected template corresponding to the ```AuxData``` of this iteration is retrieved so that it can be compared directly with the protected query template. +The matching of the two templates is carried out using the ```computeSimilarityScore``` method, which returns a score in the [0, 1] range, being 1 a perfect match. In each iteration, the score is appended to the previously created list together with the ```subjectId```. Finally, the list is sorted by the score and the N most similar matches are considered. ![alt text](images/Identification.png) # Table of factorials -| *x* (corresponding to ```_POLYNOMIALDEGREE```) | *x!* (corresponding to maximum number of enrolled subjects) | -| :------: | :----: | -| 5 | 120 | -| 6 | 720 | -| 7 | 5,040 | -| 8 | 40,320 | -| 9 | 362,880 | -| 10 | 3,628,800 | -| 11 | 39,916,800 | -| 12 | 479,001,600 | -| 13 | 6,227,020,800 | -| 14 | 87,178,291,200 | +| *x* (corresponding to ```polynomialDegree```) | *x!* (corresponding to maximum number of enrolled subjects) | +|:---------------------------------------------:|:-----------------------------------------------------------:| +| 5 | 120 | +| 6 | 720 | +| 7 | 5,040 | +| 8 | 40,320 | +| 9 | 362,880 | +| 10 | 3,628,800 | +| 11 | 39,916,800 | +| 12 | 479,001,600 | +| 13 | 6,227,020,800 | +| 14 | 87,178,291,200 | diff --git a/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt b/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt index bad52ec..11138a1 100644 --- a/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt +++ b/src/main/kotlin/com/simprints/biometrics/polyprotect/AuxData.kt @@ -1,5 +1,12 @@ package com.simprints.biometrics.polyprotect +/** + * Simple data class to handle the template-specific secret parameters used in the PolyProtect + * transformation, namely the coefficients and exponents of a polynomial function. + * + * @param coefficients of a polynomial function + * @param exponents of a polynomial function + */ data class AuxData( val coefficients: IntArray, val exponents: IntArray, diff --git a/src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt b/src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt index 9773e43..c0ba861 100644 --- a/src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt +++ b/src/main/kotlin/com/simprints/biometrics/polyprotect/PolyProtect.kt @@ -2,6 +2,17 @@ package com.simprints.biometrics.polyprotect import kotlin.math.pow +/** + * The main entry point and configuration holder for the PolyProtect algorithm. + * + * @param polynomialDegree degree of the polynomial used in the PolyProtect transformation. Must be in `[5;14]` range. + * + * @param coefficientAbsMax range limit for the coefficients of the polynomial used in the PolyProtect transformation. + * The coefficients will be drawn from the `[-coefficientAbsMax, coefficientAbsMax]` range excluding 0. + * + * @param overlap of the intervals of the unprotected template to transform using the PolyProtect polynomial transformation. + * It can go from 0 to `polynomialDegree - 1`. The safest options would be: {0, 1, 2, 3}. + */ class PolyProtect( private val polynomialDegree: Int = POLYNOMIAL_DEGREE_DEFAULT, private val coefficientAbsMax: Int = COEFFICIENT_ABS_MAX_DEFAULT, @@ -20,6 +31,17 @@ class PolyProtect( } } + /** + * Applies PolyProtect transformation to the provided unprotected template + * using the accompanying `AuxData` values + * + * **NOTE**: The size of the protected templates depends on `polynomialDegree` and `overlap`. + * + * @param unprotectedTemplate + * @param auxData a set of coefficients and exponents associated with specific biometric record + * + * @return protected template + */ fun transformTemplate( unprotectedTemplate: DoubleArray, auxData: AuxData @@ -50,6 +72,10 @@ class PolyProtect( return protectedTemplate.toDoubleArray() } + /** + * @return Randomly generated coefficients and exponents (auxiliary data) according + * to the configured `polynomialDegree` and `coefficientAbsMax` values. + */ fun generateAuxData(): AuxData { // Create a list that excludes 0, combining the ranges (-coefficientAbsMax to -1) and (1 to coefficientAbsMax) val coefficientRange = diff --git a/src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt b/src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt index cb8d3fa..ec207ca 100644 --- a/src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt +++ b/src/main/kotlin/com/simprints/biometrics/polyprotect/SimilarityScore.kt @@ -2,6 +2,14 @@ package com.simprints.biometrics.polyprotect import kotlin.math.sqrt +/** + * Calculates a similarity score based on cosine distance. + * + * @param array1 first biometric template + * @param array2 another biometric template + * + * @return a value in in the `[0,1]` range + */ fun computeSimilarityScore( array1: DoubleArray, array2: DoubleArray,