Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose HMAC-SHA-256 interface #8230

Closed
wants to merge 18 commits into from
Closed
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
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package de.tutao.tutanota

import android.content.Context
import android.util.Log
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.platform.app.InstrumentationRegistry
import com.fasterxml.jackson.databind.ObjectMapper
Expand Down Expand Up @@ -246,6 +245,18 @@ class CompatibilityTest {
}
}

@Test
fun hmac_sha256() = runBlocking {
for (td in testData.hmacSha256Tests) {
val key = hexToBytes(td.keyHex)
val data = hexToBytes(td.dataHex)
val givenTag = hexToBytes(td.hmacSha256TagHex)

val computedTag = crypto.hmacSha256(DataWrapper(key), DataWrapper(data))
assertArrayEquals(givenTag, computedTag.data)
}
}

private fun hexToKyberPrivateKey(privateKey: String): KyberPrivateKey {
val keyComponents = bytesToByteArrays(hexToBytes(privateKey), 5)
return KyberPrivateKey(DataWrapper(keyComponents[0] + keyComponents[3] + keyComponents[4] + keyComponents[1] + keyComponents[2]))
Expand Down
55 changes: 55 additions & 0 deletions app-android/app/src/androidTest/java/de/tutao/tutanota/HmacTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package de.tutao.tutanota

import androidx.test.ext.junit.runners.AndroidJUnit4
import de.tutao.tutashared.CryptoError
import de.tutao.tutashared.crypto.Crypto
import org.junit.Assert.assertThrows
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.security.SecureRandom

@RunWith(AndroidJUnit4::class)
class HmacTest {
lateinit var randomizer: SecureRandom
lateinit var key: ByteArray
lateinit var data: ByteArray
lateinit var macTag: ByteArray

@Before
fun setup() {
randomizer = SecureRandom()
key = ByteArray(32)
data = ByteArray(256)

randomizer.nextBytes(key)
randomizer.nextBytes(data)

macTag = Crypto.hmacSha256(key, data)
}

@Test
fun roundTrip() {
Crypto.verifyHmacSha256(key, data, macTag)
}

@Test
fun badKey() {
val badKey = ByteArray(32)
randomizer.nextBytes(badKey)

assertThrows(CryptoError::class.java) {
Crypto.verifyHmacSha256(badKey, data, macTag)
}
}

@Test
fun badData() {
val badData = ByteArray(256)
randomizer.nextBytes(badData)

assertThrows(CryptoError::class.java) {
Crypto.verifyHmacSha256(key, badData, macTag)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package de.tutao.tutanota.testdata;

public class HmacSha256TestData {

String keyHex;
String dataHex;
String hmacSha256TagHex;

public HmacSha256TestData() {
}

public HmacSha256TestData(String keyHex, String dataHex, String hmacSha256TagHex) {
this.keyHex = keyHex;
this.dataHex = dataHex;
this.hmacSha256TagHex = hmacSha256TagHex;
}

public String getKeyHex() {
return keyHex;
}

public void setKeyHex(String keyHex) {
this.keyHex = keyHex;
}

public String getDataHex() {
return dataHex;
}

public void setDataHex(String dataHex) {
this.dataHex = dataHex;
}

public String getHmacSha256TagHex() {
return hmacSha256TagHex;
}

public void setHmacSha256TagHex(String hmacSha256TagHex) {
this.hmacSha256TagHex = hmacSha256TagHex;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public class TestData {
List<PQCryptTestData> pqCryptEncryptionTests = new LinkedList<>();
List<ByteArrayEncodingTestData> byteArrayEncodingTests = new LinkedList<>();
List<HkdfTestData> hkdfTests = new LinkedList<>();
List<HmacSha256TestData> hmacSha256Tests = new LinkedList<>();

public TestData addRsaEncryptionTest(EncryptionTestData test) {
this.rsaEncryptionTests.add(test);
Expand Down Expand Up @@ -99,6 +100,11 @@ public TestData addHkdfTest(HkdfTestData test) {
return this;
}

public TestData addHmacSha256Test(HmacSha256TestData test) {
this.hmacSha256Tests.add(test);
return this;
}

public List<EncryptionTestData> getRsaEncryptionTests() {
return rsaEncryptionTests;
}
Expand Down Expand Up @@ -159,5 +165,8 @@ public List<ByteArrayEncodingTestData> getByteArrayEncodingTests() {
public List<HkdfTestData> getHkdfTests() {
return hkdfTests;
}
public List<HmacSha256TestData> getHmacSha256Tests() {
return hmacSha256Tests;
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import androidx.annotation.VisibleForTesting
import de.tutao.tutasdk.KyberException
import de.tutao.tutasdk.kyberDecapsulateWithPrivKey
import de.tutao.tutasdk.kyberEncapsulateWithPubKey
import de.tutao.tutashared.crypto.Crypto
import de.tutao.tutashared.ipc.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
Expand Down Expand Up @@ -57,7 +58,7 @@ class AndroidNativeCryptoFacade(
const val AES256_KEY_LENGTH = 256
const val AES256_KEY_LENGTH_BYTES = AES256_KEY_LENGTH / 8

const val HMAC_256 = "HmacSHA256"
const val HMAC_SHA_256 = "HmacSHA256"

/**
* Converts the given byte array to a key.
Expand Down Expand Up @@ -115,12 +116,15 @@ class AndroidNativeCryptoFacade(

}

private fun hmac256(key: ByteArray, data: ByteArray): ByteArray {
val macKey = SecretKeySpec(key, HMAC_256)
val hmac = Mac.getInstance(HMAC_256)
hmac.init(macKey)
return hmac.doFinal(data)
}
}

override suspend fun hmacSha256(key: DataWrapper, data: DataWrapper): DataWrapper {
return DataWrapper(Crypto.hmacSha256(key.data, data.data))
}

@Throws(CryptoError::class)
override suspend fun verifyHmacSha256(key: DataWrapper, data: DataWrapper, tag: DataWrapper) {
Crypto.verifyHmacSha256(key.data, data.data, tag.data)
}

override suspend fun generateKyberKeypair(seed: DataWrapper): KyberKeyPair {
Expand Down Expand Up @@ -247,7 +251,7 @@ class AndroidNativeCryptoFacade(
val data = tempOut.toByteArray()
out.write(byteArrayOf(1))
out.write(data)
val macBytes = hmac256(subKeys.mKey!!, data)
val macBytes = Crypto.hmacSha256(subKeys.mKey!!, data)
out.write(macBytes)
} else {
out.write(tempOut.toByteArray())
Expand Down Expand Up @@ -409,10 +413,7 @@ class AndroidNativeCryptoFacade(
val cipherText = tempOut.toByteArray()
val cipherTextWithoutMac = cipherText.copyOfRange(1, cipherText.size - 32)
val providedMacBytes = cipherText.copyOfRange(cipherText.size - 32, cipherText.size)
val computedMacBytes = hmac256(subKeys.mKey!!, cipherTextWithoutMac)
if (!Arrays.equals(computedMacBytes, providedMacBytes)) {
throw CryptoError("invalid mac")
}
Crypto.verifyHmacSha256(subKeys.mKey!!, cipherTextWithoutMac, providedMacBytes)
inputWithoutMac = ByteArrayInputStream(cipherTextWithoutMac)
}
val iv = ByteArray(AES_BLOCK_SIZE_BYTES)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package de.tutao.tutashared.crypto

import de.tutao.tutashared.AndroidNativeCryptoFacade.Companion.HMAC_SHA_256
import de.tutao.tutashared.CryptoError
import javax.crypto.Mac
import javax.crypto.spec.SecretKeySpec

class Crypto {
Copy link
Contributor

Choose a reason for hiding this comment

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

is there a reason we package it into a class? that's not very idiomatic for Kotlin to collect some functions into statics of a class

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The reason is that we needed those functions with those signatures, which are different from what the NativeCryptoFacade wants, and it felt wrong to just stuff them into AndroidNativeCryptoFacade. Would that be better?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's fine to have them in this file, you can just leave them be as functions, without a wrapping class

companion object {
fun hmacSha256(key: ByteArray, data: ByteArray): ByteArray {
val macKey = SecretKeySpec(key, HMAC_SHA_256)
val hmac = Mac.getInstance(HMAC_SHA_256)
hmac.init(macKey)
return hmac.doFinal(data)
}

fun verifyHmacSha256(key: ByteArray, data: ByteArray, tag: ByteArray) {
val computedTag = hmacSha256(key, data)
if (!tag.contentEquals(computedTag)) {
throw CryptoError("invalid mac")
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -46,4 +46,13 @@ interface NativeCryptoFacade {
privateKey: KyberPrivateKey,
ciphertext: DataWrapper,
): DataWrapper
suspend fun hmacSha256(
key: DataWrapper,
data: DataWrapper,
): DataWrapper
suspend fun verifyHmacSha256(
key: DataWrapper,
data: DataWrapper,
tag: DataWrapper,
): Unit
}
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,26 @@ class NativeCryptoFacadeReceiveDispatcher(
)
return json.encodeToString(result)
}
"hmacSha256" -> {
val key: DataWrapper = json.decodeFromString(arg[0])
val data: DataWrapper = json.decodeFromString(arg[1])
val result: DataWrapper = this.facade.hmacSha256(
key,
data,
)
return json.encodeToString(result)
}
"verifyHmacSha256" -> {
val key: DataWrapper = json.decodeFromString(arg[0])
val data: DataWrapper = json.decodeFromString(arg[1])
val tag: DataWrapper = json.decodeFromString(arg[2])
val result: Unit = this.facade.verifyHmacSha256(
key,
data,
tag,
)
return json.encodeToString(result)
}
else -> throw Error("unknown method for NativeCryptoFacade: $method")
}
}
Expand Down
28 changes: 12 additions & 16 deletions app-ios/TutanotaSharedFramework/Crypto/IosNativeCryptoFacade.swift
Original file line number Diff line number Diff line change
@@ -1,41 +1,32 @@
import TutanotaSharedFramework
import CryptoKit
import tutasdk

/// High-level cryptographic operations API
/// Is an actor because we want to have serial execution for all the cryptogaphic operations, doing them in parallel is usually too
/// much for the device.
public actor IosNativeCryptoFacade: NativeCryptoFacade {
public init() {}

public func aesEncryptFile(_ key: DataWrapper, _ fileUri: String, _ iv: DataWrapper) async throws -> EncryptedFileInfo {

if !FileUtils.fileExists(atPath: fileUri) { throw CryptoError(message: "File to encrypt does not exist \(fileUri)") }
let encryptedFolder = try FileUtils.getEncryptedFolder()
let fileName = (fileUri as NSString).lastPathComponent
let encryptedFilePath = (encryptedFolder as NSString).appendingPathComponent(fileName)
let plainTextData = try Data(contentsOf: URL(fileURLWithPath: fileUri))
let outputData = try aesEncryptData(plainTextData, withKey: key.data, withIV: iv.data)
let result = EncryptedFileInfo(uri: encryptedFilePath, unencryptedSize: plainTextData.count)

try outputData.write(to: URL(fileURLWithPath: encryptedFilePath))

return result
}

public func aesDecryptFile(_ key: DataWrapper, _ fileUri: String) async throws -> String {
if !FileUtils.fileExists(atPath: fileUri) { throw CryptoError(message: "File to decrypt does not exist") }

let encryptedData = try Data(contentsOf: URL(fileURLWithPath: fileUri))
let plaintextData = try aesDecryptData(encryptedData, withKey: key.data)

let decryptedFolder = try FileUtils.getDecryptedFolder()
let fileName = (fileUri as NSString).lastPathComponent
let plaintextPath = (decryptedFolder as NSString).appendingPathComponent(fileName)
try plaintextData.write(to: URL(fileURLWithPath: plaintextPath), options: .atomic)

return plaintextPath
}

public func rsaEncrypt(_ publicKey: RsaPublicKey, _ data: DataWrapper, _ seed: DataWrapper) async throws -> DataWrapper {
try tutasdk.rsaEncryptWithPublicKeyComponents(
data: data.data,
Expand All @@ -45,7 +36,6 @@ public actor IosNativeCryptoFacade: NativeCryptoFacade {
)
.wrap()
}

public func rsaDecrypt(_ privateKey: RsaPrivateKey, _ data: DataWrapper) async throws -> DataWrapper {
try tutasdk.rsaDecryptWithPrivateKeyComponents(
ciphertext: data.data,
Expand All @@ -56,28 +46,34 @@ public actor IosNativeCryptoFacade: NativeCryptoFacade {
)
.wrap()
}

public func argon2idGeneratePassphraseKey(_ passphrase: String, _ salt: DataWrapper) async throws -> DataWrapper {
try tutasdk.argon2idGenerateKeyFromPassphrase(passphrase: passphrase, salt: salt.data).wrap()
}

public func generateKyberKeypair(_ seed: DataWrapper) async throws -> TutanotaSharedFramework.KyberKeyPair {
let keypair = tutasdk.generateKyberKeypair()
return KyberKeyPair(publicKey: KyberPublicKey(raw: keypair.publicKey.wrap()), privateKey: KyberPrivateKey(raw: keypair.privateKey.wrap()))
}

public func kyberEncapsulate(_ publicKey: KyberPublicKey, _ seed: DataWrapper) async throws -> TutanotaSharedFramework.KyberEncapsulation {
do {
let sdkEncapsulation = try tutasdk.kyberEncapsulateWithPubKey(publicKeyBytes: publicKey.raw.data)
return KyberEncapsulation(ciphertext: sdkEncapsulation.ciphertext.wrap(), sharedSecret: sdkEncapsulation.sharedSecret.wrap())
} catch { throw CryptoError(message: error.localizedDescription) }
}

public func kyberDecapsulate(_ privateKey: KyberPrivateKey, _ ciphertext: DataWrapper) async throws -> DataWrapper {
do { return try tutasdk.kyberDecapsulateWithPrivKey(privateKeyBytes: privateKey.raw.data, ciphertext: ciphertext.data).wrap() } catch {
throw CryptoError(message: error.localizedDescription)
}

}
public func hmacSha256(_ key: DataWrapper, _ data: DataWrapper) -> DataWrapper {
let symmetricKey = SymmetricKey(data: key.data)
Copy link
Contributor

Choose a reason for hiding this comment

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

It should probably be async if it's part of IPC

let macTag = HMAC<SHA256>.authenticationCode(for: data.data, using: symmetricKey)
var bytes: [UInt8] = []
bytes.append(contentsOf: macTag)
return DataWrapper(data: Data(bytes: bytes, count: bytes.count))
}
public func verifyHmacSha256(_ key: DataWrapper, _ data: DataWrapper, _ tag: DataWrapper) async throws {
let isValid = HMAC<SHA256>.isValidAuthenticationCode(tag.data, authenticating: data.data, using: SymmetricKey(data: key.data))
if !isValid { throw TUTErrorFactory.createError(withDomain: TUT_CRYPTO_ERROR, message: "invalid MAC: checksum and/or key is wrong") }
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,4 +43,13 @@ public protocol NativeCryptoFacade {
_ privateKey: KyberPrivateKey,
_ ciphertext: DataWrapper
) async throws -> DataWrapper
func hmacSha256(
_ key: DataWrapper,
_ data: DataWrapper
) async throws -> DataWrapper
func verifyHmacSha256(
_ key: DataWrapper,
_ data: DataWrapper,
_ tag: DataWrapper
) async throws -> Void
}
Loading
Loading