Skip to content

Commit

Permalink
Destructuring accessors for SecureCellData in Kotlin (#638)
Browse files Browse the repository at this point in the history
* Destructuring accessors for SecureCellData in Kotlin

Kotlin supports tuples and destructuring assignment which results in
much nicer APIs with multiple return values. Implement special accessor
methods for SecureCellData to enable that.

These methods are not for general purpose use. They are expected to be
implicitly used in Kotlin when working with Token Protect mode, that's
why they also have @NotNull annotations -- as in Token Protect mode the
encrypted data and authentication token are always available.

* Replace Arrays.copyOf() calls

@NotNull annotations remove a layer of optionals, so instead of
ByteArray? results we are now dealing with just ByteArray, and
that means we can call copyOf() method directly instead of using
java.utils.Arrays.copyOf(). Replace the calls to make linter happy.
  • Loading branch information
ilammy committed May 15, 2020
1 parent eac3721 commit 77739f4
Show file tree
Hide file tree
Showing 3 changed files with 100 additions and 106 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,19 @@ _Code:_
- Improved Token Protect API
([#634](https://github.com/cossacklabs/themis/pull/634)).
- Decryption no longer requires an intermediate `SecureCellData` object.
- `SecureCellData` can now be destructured in Kotlin
([#638](https://github.com/cossacklabs/themis/pull/638)).

```kotlin
// You can now write like this:
val (encrypted, authToken) = cellTP.encrypt(message, context)
// Instead of having to spell it out like this:
val result = cellTP.protect(context, message)
val encrypted = result.protectedData
val authToken = result.additionalData
```

- Secure Cell mode can now be selected by instantiating an appropriate interface:

| New API | Old API |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@

package com.cossacklabs.themis;

import org.jetbrains.annotations.NotNull;

/**
* Represents data protected by SecureCell
*/
Expand Down Expand Up @@ -58,4 +60,23 @@ public boolean hasAdditionalData() {
return null != this.additionalData;
}

/**
* Returns encrypted data for Token Protect mode.
* <p>
* This method is equivalent to {@link #getProtectedData()}.
* You are not expected to use it directly, it exists for improved Kotlin API.
*/
public @NotNull byte[] component1() {
return this.protectedData;
}

/**
* Returns authentication token for Token Protect mode.
* <p>
* This method is equivalent to {@link #getAdditionalData()}.
* You are not expected to use it directly, it exists for improved Kotlin API.
*/
public @NotNull byte[] component2() {
return this.additionalData;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
package com.cossacklabs.themis.test

import java.nio.charset.StandardCharsets
import java.util.*
import kotlin.experimental.inv

import com.cossacklabs.themis.*
Expand Down Expand Up @@ -61,11 +60,7 @@ class SecureCellTokenProtectTestKotlin {
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)
val context = "For great justice".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message, context)
assertNotNull(result)

val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message, context)
assertNotNull(encrypted)
assertNotNull(authToken)

Expand All @@ -79,9 +74,10 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
assertEquals(message.size.toLong(), result.protectedData.size.toLong())
assertTrue(result.additionalData.isNotEmpty())
val (encrypted, authToken) = cell.encrypt(message)

assertEquals(message.size, encrypted.size)
assertTrue(authToken.isNotEmpty())
}

@Test
Expand All @@ -91,12 +87,12 @@ class SecureCellTokenProtectTestKotlin {
val shortContext = ".".toByteArray(StandardCharsets.UTF_8)
val longContext = "You have no chance to survive make your time. Ha ha ha ha ...".toByteArray(StandardCharsets.UTF_8)

val resultShort = cell.encrypt(message, shortContext)
val resultLong = cell.encrypt(message, longContext)
val (encryptedShort, authTokenShort) = cell.encrypt(message, shortContext)
val (encryptedLong, authTokenLong) = cell.encrypt(message, longContext)

// Context is not (directly) included into encrypted message.
assertEquals(resultShort.protectedData.size.toLong(), resultLong.protectedData.size.toLong())
assertEquals(resultShort.additionalData.size.toLong(), resultLong.additionalData.size.toLong())
assertEquals(encryptedShort.size, encryptedLong.size)
assertEquals(authTokenShort.size, authTokenLong.size)
}

@Test
Expand All @@ -106,19 +102,19 @@ class SecureCellTokenProtectTestKotlin {
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

// Absent, empty, or nil context are all the same.
val result1 = cell.encrypt(message)
val result2 = cell.encrypt(message, null)
val result3 = cell.encrypt(message, byteArrayOf())

assertArrayEquals(message, cell.decrypt(result1.protectedData, result1.additionalData))
assertArrayEquals(message, cell.decrypt(result2.protectedData, result2.additionalData))
assertArrayEquals(message, cell.decrypt(result3.protectedData, result3.additionalData))
assertArrayEquals(message, cell.decrypt(result1.protectedData, result1.additionalData, null))
assertArrayEquals(message, cell.decrypt(result2.protectedData, result2.additionalData, null))
assertArrayEquals(message, cell.decrypt(result3.protectedData, result3.additionalData, null))
assertArrayEquals(message, cell.decrypt(result1.protectedData, result1.additionalData, byteArrayOf()))
assertArrayEquals(message, cell.decrypt(result2.protectedData, result2.additionalData, byteArrayOf()))
assertArrayEquals(message, cell.decrypt(result3.protectedData, result3.additionalData, byteArrayOf()))
val (encrypted1, authToken1) = cell.encrypt(message)
val (encrypted2, authToken2) = cell.encrypt(message, null)
val (encrypted3, authToken3) = cell.encrypt(message, byteArrayOf())

assertArrayEquals(message, cell.decrypt(encrypted1, authToken1))
assertArrayEquals(message, cell.decrypt(encrypted2, authToken2))
assertArrayEquals(message, cell.decrypt(encrypted3, authToken3))
assertArrayEquals(message, cell.decrypt(encrypted1, authToken1, null))
assertArrayEquals(message, cell.decrypt(encrypted2, authToken2, null))
assertArrayEquals(message, cell.decrypt(encrypted3, authToken3, null))
assertArrayEquals(message, cell.decrypt(encrypted1, authToken1, byteArrayOf()))
assertArrayEquals(message, cell.decrypt(encrypted2, authToken2, byteArrayOf()))
assertArrayEquals(message, cell.decrypt(encrypted3, authToken3, byteArrayOf()))
}

@Test
Expand All @@ -129,9 +125,7 @@ class SecureCellTokenProtectTestKotlin {
val correctContext = "We are CATS".toByteArray(StandardCharsets.UTF_8)
val incorrectContext = "Captain !!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message, correctContext)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message, correctContext)

// You cannot use a different context to decrypt data.
assertThrows(SecureCellException::class.java) {
Expand All @@ -149,13 +143,8 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result1 = cell.encrypt(message)
val encrypted1 = result1.protectedData
val authToken1 = result1.additionalData

val result2 = cell.encrypt(message)
val encrypted2 = result2.protectedData
val authToken2 = result2.additionalData
val (encrypted1, authToken1) = cell.encrypt(message)
val (encrypted2, authToken2) = cell.encrypt(message)

// You cannot use a different token to decrypt data.
assertThrows(SecureCellException::class.java) { cell.decrypt(encrypted1, authToken2) }
Expand All @@ -171,12 +160,10 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

// Invert every odd byte, this will surely break the message.
val corrupted = Arrays.copyOf(encrypted, encrypted.size)
val corrupted = encrypted.copyOf(encrypted.size)
for (i in corrupted.indices) {
if (i % 2 == 1) {
corrupted[i] = corrupted[i].inv()
Expand All @@ -193,11 +180,9 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

val truncated = Arrays.copyOf(encrypted, encrypted.size - 1)
val truncated = encrypted.copyOf(encrypted.size - 1)

assertThrows(SecureCellException::class.java) {
cell.decrypt(truncated, authToken)
Expand All @@ -209,11 +194,9 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

val extended = Arrays.copyOf(encrypted, encrypted.size + 1)
val extended = encrypted.copyOf(encrypted.size + 1)

assertThrows(SecureCellException::class.java) {
cell.decrypt(extended, authToken)
Expand All @@ -226,12 +209,10 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

// Invert every odd byte, this will surely break the token.
val corruptedToken = Arrays.copyOf(authToken, authToken.size)
val corruptedToken = authToken.copyOf(authToken.size)
for (i in corruptedToken.indices) {
if (i % 2 == 1) {
corruptedToken[i] = corruptedToken[i].inv()
Expand All @@ -253,11 +234,9 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

val truncatedToken = Arrays.copyOf(authToken, authToken.size - 1)
val truncatedToken = authToken.copyOf(authToken.size - 1)

assertThrows(SecureCellException::class.java) {
cell.decrypt(encrypted, truncatedToken)
Expand All @@ -270,11 +249,9 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

val extendedToken = Arrays.copyOf(authToken, authToken.size + 1)
val extendedToken = authToken.copyOf(authToken.size + 1)

// Current implementation of Secure Cell allows the token to be overlong.
// Extra data is simply ignored.
Expand All @@ -288,9 +265,7 @@ class SecureCellTokenProtectTestKotlin {
val cell = SecureCell.TokenProtectWithKey(SymmetricKey())
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

// FIXME(ilammy, 2020-05-05): improve Themis Core robustness (T1604)
// Currently this call throws OutOfMemoryError instead of SecureCellException
Expand All @@ -308,8 +283,7 @@ class SecureCellTokenProtectTestKotlin {
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)
val context = "We are CATS".toByteArray(StandardCharsets.UTF_8)

val result = cell.encrypt(message, context)
val encrypted = result.protectedData
val (encrypted, _) = cell.encrypt(message, context)

assertThrows(SecureCellException::class.java) {
cell.decrypt(encrypted, context)
Expand All @@ -325,9 +299,7 @@ class SecureCellTokenProtectTestKotlin {
assertThrows(NullArgumentException::class.java) { cell.encrypt(null) }
assertThrows(InvalidArgumentException::class.java) { cell.encrypt(byteArrayOf()) }

val result = cell.encrypt(message)
val encrypted = result.protectedData
val authToken = result.additionalData
val (encrypted, authToken) = cell.encrypt(message)

assertThrows(NullArgumentException::class.java) { cell.decrypt(encrypted, null) }
assertThrows(NullArgumentException::class.java) { cell.decrypt(null, authToken) }
Expand All @@ -346,26 +318,20 @@ class SecureCellTokenProtectTestKotlin {
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)
val context = "We are CATS".toByteArray(StandardCharsets.UTF_8)

var encrypted: ByteArray
var authToken: ByteArray
var decrypted: ByteArray?
var result = oldCell.protect(context, message)
assertNotNull(result)
encrypted = result.protectedData
authToken = result.additionalData
assertNotNull(encrypted)
assertNotNull(authToken)
decrypted = newCell.decrypt(encrypted, authToken, context)
assertArrayEquals(message, decrypted)

result = newCell.encrypt(message, context)
val result = oldCell.protect(context, message)
assertNotNull(result)
encrypted = result.protectedData
authToken = result.additionalData
assertNotNull(encrypted)
assertNotNull(authToken)
decrypted = oldCell.unprotect(context, SecureCellData(encrypted, authToken))
assertArrayEquals(message, decrypted)
val encryptedOld = result.protectedData
val authTokenOld = result.additionalData
assertNotNull(encryptedOld)
assertNotNull(authTokenOld)
val decryptedOld = newCell.decrypt(encryptedOld, authTokenOld, context)
assertArrayEquals(message, decryptedOld)

val (encryptedNew, authTokenNew) = newCell.encrypt(message, context)
assertNotNull(encryptedNew)
assertNotNull(authTokenNew)
val decryptedNew = oldCell.unprotect(context, SecureCellData(encryptedNew, authTokenNew))
assertArrayEquals(message, decryptedNew)
}

@Test
Expand All @@ -377,25 +343,19 @@ class SecureCellTokenProtectTestKotlin {
val oldCell = SecureCell(key.toByteArray(), SecureCell.MODE_TOKEN_PROTECT)
val message = "All your base are belong to us!".toByteArray(StandardCharsets.UTF_8)

var encrypted: ByteArray
var authToken: ByteArray
var decrypted: ByteArray?
var result = oldCell.protect(null as ByteArray?, message)
val result = oldCell.protect(null as ByteArray?, message)
assertNotNull(result)
encrypted = result.protectedData
authToken = result.additionalData
assertNotNull(encrypted)
assertNotNull(authToken)
decrypted = newCell.decrypt(encrypted, authToken)
assertArrayEquals(message, decrypted)

result = newCell.encrypt(message)
assertNotNull(result)
encrypted = result.protectedData
authToken = result.additionalData
assertNotNull(encrypted)
assertNotNull(authToken)
decrypted = oldCell.unprotect(null as ByteArray?, SecureCellData(encrypted, authToken))
assertArrayEquals(message, decrypted)
val encryptedOld = result.protectedData
val authTokenOld = result.additionalData
assertNotNull(encryptedOld)
assertNotNull(authTokenOld)
val decryptedOld = newCell.decrypt(encryptedOld, authTokenOld)
assertArrayEquals(message, decryptedOld)

val (encryptedNew, authTokenNew) = newCell.encrypt(message)
assertNotNull(encryptedNew)
assertNotNull(authTokenNew)
val decryptedNew = oldCell.unprotect(null as ByteArray?, SecureCellData(encryptedNew, authTokenNew))
assertArrayEquals(message, decryptedNew)
}
}

0 comments on commit 77739f4

Please sign in to comment.