Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,9 @@ Sensitive data as email and phone number encrypted using SHA256.
val ids = listOf(
OptableIdentifier.Email("john.doe+test@example.com"),
OptableIdentifier.PhoneNumber("+1(555)1234567"),
// Already hashed? Pass it directly, it will not be hashed again:

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: what about HashedPhoneNumbers, shouldn't it be here too?

OptableIdentifier.Hem("da146c4d301ce4a9bf01c8384a5b2f8966b3ed853adf1ee08746be5703ebeebf"),
OptableIdentifier.HashedPhoneNumber("e421d168fe94a7e385cf38608d8de870a5233cc6b14596dd307819b8491c4d5b"),
OptableIdentifier.PostalCode("12345"),
OptableIdentifier.IPv4("192.168.0.1"),
OptableIdentifier.IPv6("2001:db8::1"),
Expand All @@ -211,6 +214,9 @@ val ids = listOf(
ArrayList<OptableIdentifier> ids = Lists.newArrayList(
new OptableIdentifier.Email("john.doe+test@example.com"),
new OptableIdentifier.PhoneNumber("+1(555)1234567"),
// Already hashed? Pass it directly, it will not be hashed again:
new OptableIdentifier.Hem("da146c4d301ce4a9bf01c8384a5b2f8966b3ed853adf1ee08746be5703ebeebf"),
new OptableIdentifier.HashedPhoneNumber("e421d168fe94a7e385cf38608d8de870a5233cc6b14596dd307819b8491c4d5b"),
new OptableIdentifier.PostalCode("12345"),
new OptableIdentifier.IPv4("192.168.0.1"),
new OptableIdentifier.IPv6("2001:db8::1"),
Expand Down
14 changes: 14 additions & 0 deletions android_sdk/src/main/java/co/optable/sdk/OptableIdentifier.kt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,20 @@ sealed class OptableIdentifier {
*/
data class PhoneNumber(val value: String) : OptableIdentifier()

/**
* Already-hashed Email address (HEM).
* Encoding: normalized (whitespace removed, lowercased), never hashed again.
* Dropped if the value is not a SHA-256 digest, so a plaintext Email is never sent.
*/
data class Hem(val value: String) : OptableIdentifier()

/**
* Already-hashed Phone number.
* Encoding: normalized (whitespace removed, lowercased), never hashed again.
* Dropped if the value is not a SHA-256 digest.
*/
data class HashedPhoneNumber(val value: String) : OptableIdentifier()

/**
* Postal/ZIP code.
* Encoding: normalized (whitespace removed, lowercased).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ internal class IdentifiersEncoder(
when (identifier) {
is OptableIdentifier.Email -> result.addIfNotNull(EMAIL, identifier.value, ::encrypt)
is OptableIdentifier.PhoneNumber -> result.addIfNotNull(PHONE, identifier.value, ::encrypt)
is OptableIdentifier.Hem -> result.addIfValidHash(EMAIL, identifier.value)
is OptableIdentifier.HashedPhoneNumber -> result.addIfValidHash(PHONE, identifier.value)
is OptableIdentifier.PostalCode -> result.addIfNotNull(POSTAL, identifier.value, ::normalize)
is OptableIdentifier.IPv4 -> result.addIfNotNull(IPV4, identifier.value, ::removeWhitespaces)
is OptableIdentifier.IPv6 -> result.addIfNotNull(IPV6, identifier.value, ::normalize)
Expand Down Expand Up @@ -85,6 +87,19 @@ internal class IdentifiersEncoder(
this.add(result)
}

/**
* Adds an already-hashed value, skipping it entirely unless it is a SHA-256
* digest. This keeps a plaintext Email or Phone number off the wire.
*/
private fun MutableList<String>.addIfValidHash(key: String, value: String?) {
if (value == null) return

val hash = normalize(value)
if (hash.length != 64 || !hash.matches("^[a-f0-9]+$".toRegex())) return

this.add("$key:$hash")
}

/**
* Returns a type-prefixed ID based on the query string
* oeid=sha256value parameters in the specified uri, if one is found.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,52 @@ class OptableIdentifiersTest {
assertEquals(expected, actual)
}

@Test
fun `encode hem`() {
val email = " John.DOE+test@example.COM "
val hem = sha256(normalize(email))

// Supplying the hash must match hashing the plaintext ourselves.
assertEquals(
identifiersEncoder.encode(listOf(OptableIdentifier.Email(email))),
identifiersEncoder.encode(listOf(OptableIdentifier.Hem(hem)))
)
assertEquals(
listOf("e:$hem"),
identifiersEncoder.encode(listOf(OptableIdentifier.Hem(" ${hem.uppercase(Locale.ROOT)} ")))
)
}

@Test
fun `encode hashedPhoneNumber`() {
val phone = " +1 (555) 123 45 67 "
val hash = sha256(normalize(phone))

assertEquals(
identifiersEncoder.encode(listOf(OptableIdentifier.PhoneNumber(phone))),
identifiersEncoder.encode(listOf(OptableIdentifier.HashedPhoneNumber(hash)))
)
}

@Test
fun `encode drops hashed values that are not a SHA-256`() {
val hem = sha256("john.doe@example.com")

listOf(
"john.doe@example.com", // plaintext must never reach the wire
"",
hem.dropLast(1), // too short
hem + "a", // too long
hem.dropLast(1) + "z" // not hex
).forEach { value ->
assertEquals(
"expected $value to be dropped",
emptyList<String>(),
identifiersEncoder.encode(listOf(OptableIdentifier.Hem(value)))
)
}
}

@Test
fun `encode postalCode`() {
val postal = " 12 3 45 "
Expand Down
Loading