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: 5 additions & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,18 @@ dependencies {
// Isolated Projects forbids. The module lists come from `settings.gradle.kts`; the
// task depends on each module's test task by *path* — a lazy, cross-project-safe
// dependency that never configures the other project at configuration time.
// Android modules expose `testDebugUnitTest`; pure-JVM modules expose `test`.
// Android modules expose `testDebugUnitTest`; KMP modules expose `testAndroidHostTest`;
// pure-JVM modules expose `test`.
@Suppress("UNCHECKED_CAST")
val androidUnitTestModules = rootProject.extra["flipcash.androidUnitTestModules"] as List<String>
@Suppress("UNCHECKED_CAST")
val jvmUnitTestModules = rootProject.extra["flipcash.jvmUnitTestModules"] as List<String>
@Suppress("UNCHECKED_CAST")
val kmpUnitTestModules = rootProject.extra["flipcash.kmpUnitTestModules"] as List<String>

tasks.register("flipcashTestDebug") {
description = "Run testDebug for all Flipcash modules"
dependsOn(androidUnitTestModules.map { "$it:testDebugUnitTest" })
dependsOn(jvmUnitTestModules.map { "$it:test" })
dependsOn(kmpUnitTestModules.map { "$it:testAndroidHostTest" })
}
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,8 @@ timber = "5.0.1"

sodium-bindings = "0.9.5"
desugaring = "2.1.5"
kotlincrypto-hash = "0.7.0"
kotlincrypto-macs = "0.7.0"
event-bus = "0.1.0"

bugsnag = "6.26.1"
Expand Down Expand Up @@ -257,6 +259,8 @@ mixpanel = { module = "com.mixpanel.android:mixpanel-android", version.ref = "mi
# Crypto
sodium-bindings = { module = "com.ionspin.kotlin:multiplatform-crypto-libsodium-bindings-android", version.ref = "sodium-bindings" }
eddsa = { module = "net.i2p.crypto:eddsa", version = "0.3.0" }
kotlincrypto-hash-sha2 = { module = "org.kotlincrypto.hash:sha2", version.ref = "kotlincrypto-hash" }
kotlincrypto-macs-hmac-sha2 = { module = "org.kotlincrypto.macs:hmac-sha2", version.ref = "kotlincrypto-macs" }

# Misc
fingerprint-pro = { module = "com.fingerprint.android:pro", version = "2.4.0" }
Expand Down
6 changes: 6 additions & 0 deletions kmp/shared-core/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -21,13 +21,19 @@ kotlin {
baseName = "SharedCore"
isStatic = true
export(project(":libs:encryption:base58"))
export(project(":libs:encryption:sha256"))
export(project(":libs:encryption:sha512"))
export(project(":libs:encryption:hmac"))
}
}

sourceSets {
commonMain {
dependencies {
api(project(":libs:encryption:base58"))
api(project(":libs:encryption:sha256"))
api(project(":libs:encryption:sha512"))
api(project(":libs:encryption:hmac"))
}
}
}
Expand Down
32 changes: 25 additions & 7 deletions libs/encryption/hmac/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,30 @@
plugins {
alias(libs.plugins.flipcash.android.library)
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
}

android {
namespace = "${Gradle.codeNamespace}.encryption.hmac"
}
kotlin {
android {
namespace = "com.getcode.encryption.hmac"
compileSdk = 37
minSdk = 29
withHostTest {}
}

iosArm64()
iosSimulatorArm64()
iosX64()

dependencies {
implementation(libs.grpc.okhttp)
implementation(libs.grpc.kotlin)
sourceSets {
commonMain {
dependencies {
implementation(libs.kotlincrypto.macs.hmac.sha2)
}
}
commonTest {
dependencies {
implementation(kotlin("test"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.getcode.crypt

import org.kotlincrypto.macs.hmac.sha2.HmacSHA256
import org.kotlincrypto.macs.hmac.sha2.HmacSHA512

/** Multiplatform HMAC utilities backed by kotlincrypto. */
object Hmac {

/**
* Computes an HMAC over [message] using [key] with the given [algorithm].
*
* Supported algorithm strings: `"HmacSHA512"`, `"HmacSHA256"`.
*/
fun hmac(algorithm: String, key: ByteArray, message: ByteArray): ByteArray {
return when (algorithm) {
"HmacSHA512" -> {
val mac = HmacSHA512(key)
mac.update(message)
mac.doFinal()
}
"HmacSHA256" -> {
val mac = HmacSHA256(key)
mac.update(message)
mac.doFinal()
}
else -> throw IllegalArgumentException("Unsupported HMAC algorithm: $algorithm")
}
}

/** Encodes [bytes] as a lowercase hex string. */
fun bytesToHex(bytes: ByteArray): String {
val hexChars = CharArray(bytes.size * 2)
for (j in bytes.indices) {
val v = bytes[j].toInt() and 0xFF
hexChars[j * 2] = "0123456789abcdef"[v ushr 4]
hexChars[j * 2 + 1] = "0123456789abcdef"[v and 0x0F]
}
return hexChars.concatToString()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ class HmacTest {
@Test
fun hmacSha512Rfc4231TestCase1() {
val key = ByteArray(20) { 0x0b.toByte() }
val message = "Hi There".toByteArray(Charsets.UTF_8)
val message = "Hi There".encodeToByteArray()
val result = Hmac.hmac("HmacSHA512", key, message)
assertEquals(64, result.size)
assertEquals(
Expand All @@ -25,8 +25,8 @@ class HmacTest {

@Test
fun hmacSha512Rfc4231TestCase2() {
val key = "Jefe".toByteArray(Charsets.UTF_8)
val message = "what do ya want for nothing?".toByteArray(Charsets.UTF_8)
val key = "Jefe".encodeToByteArray()
val message = "what do ya want for nothing?".encodeToByteArray()
val result = Hmac.hmac("HmacSHA512", key, message)
assertEquals(
"164b7a7bfcf819e2e395fbe73b56e0a3" +
Expand All @@ -39,8 +39,8 @@ class HmacTest {

@Test
fun hmacSha256KnownVector() {
val key = "key".toByteArray(Charsets.UTF_8)
val message = "The quick brown fox jumps over the lazy dog".toByteArray(Charsets.UTF_8)
val key = "key".encodeToByteArray()
val message = "The quick brown fox jumps over the lazy dog".encodeToByteArray()
val result = Hmac.hmac("HmacSHA256", key, message)
assertEquals(
"f7bc83f430538424b13298e6aa6fb143ef4d59a14946175997479dbc2d1a3cd8",
Expand All @@ -50,8 +50,8 @@ class HmacTest {

@Test
fun deterministicOutput() {
val key = "k".toByteArray()
val msg = "m".toByteArray()
val key = "k".encodeToByteArray()
val msg = "m".encodeToByteArray()
val a = Hmac.hmac("HmacSHA512", key, msg)
val b = Hmac.hmac("HmacSHA512", key, msg)
assertContentEquals(a, b)
Expand Down
40 changes: 0 additions & 40 deletions libs/encryption/hmac/src/main/java/com/getcode/crypt/Hmac.java

This file was deleted.

35 changes: 28 additions & 7 deletions libs/encryption/sha256/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,12 +1,33 @@
plugins {
alias(libs.plugins.flipcash.android.library)
kotlin("multiplatform")
id("com.android.kotlin.multiplatform.library")
}

android {
namespace = "${Gradle.codeNamespace}.encryption.sha256"
}
kotlin {
android {
namespace = "com.getcode.encryption.sha256"
compileSdk = 37
minSdk = 29
withHostTest {}
}

iosArm64()
iosSimulatorArm64()
iosX64()

dependencies {
implementation(libs.grpc.okhttp)
implementation(libs.grpc.kotlin)
sourceSets {
commonMain {
dependencies {
implementation(libs.kotlincrypto.hash.sha2)
}
}
androidMain {
// MessageDigest + BigInteger + File — JDK only; no extra Gradle deps.
}
commonTest {
dependencies {
implementation(kotlin("test"))
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.getcode.crypt

import kotlin.test.Test
import kotlin.test.assertTrue

class Sha256HashJvmTest {

/** [Sha256Hash.toBigInteger] must return a positive value for a non-zero hash. */
@Test
fun toBigIntegerPositive() {
val hash = Sha256Hash.wrap(ByteArray(32) { 0xff.toByte() })
assertTrue(hash.toBigInteger().signum() > 0)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.getcode.crypt

import java.io.File
import java.math.BigInteger
import java.security.MessageDigest

/** Returns a new SHA-256 [MessageDigest] instance (JVM/Android only). */
fun Sha256Hash.Companion.newDigest(): MessageDigest =
MessageDigest.getInstance("SHA-256")

/**
* Returns the wrapped bytes interpreted as a positive (unsigned) [BigInteger]
* (JVM/Android only).
*/
fun Sha256Hash.toBigInteger(): BigInteger = BigInteger(1, bytes)

/**
* Hashes the full contents of [file] and returns the result wrapped in a
* [Sha256Hash] (JVM/Android only).
*/
fun Sha256Hash.Companion.of(file: File): Sha256Hash = of(file.readBytes())
Loading
Loading