Implement basic cryptography functions

This commit is contained in:
2021-11-06 06:18:59 -05:00
parent e26b643423
commit 1567e2c0ad
15 changed files with 21077 additions and 0 deletions

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,76 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.crypto
class Bip39(private val wordlist: List<String>, private val crypto: Crypto) {
private fun computeChecksum(entropy: List<Boolean>): List<Boolean> {
val sha256 = crypto.sha256()
var byte = 0
entropy.forEachIndexed { i, bit ->
byte = byte shl 1
if (bit) byte += 1
if (i.rem(8) == 7) {
sha256.update(byte.toByte())
byte = 0
}
}
return sha256.finalize().toBits().subList(0, entropy.size / 32)
}
fun encode(entropy: ByteArray): List<String> {
val entropyBits = entropy.toBits()
val msg = entropyBits + computeChecksum(entropyBits)
var wordIndex = 0
val mnemonic = mutableListOf<String>()
msg.forEachIndexed { i, bit ->
wordIndex = wordIndex shl 1
if (bit) wordIndex += 1
if (i.rem(11) == 10) {
mnemonic.add(wordlist[wordIndex])
wordIndex = 0
}
}
return mnemonic
}
fun decode(mnemonic: List<String>): ByteArray {
val bits = mutableListOf<Boolean>()
mnemonic.forEach { word ->
val wordBits = mutableListOf<Boolean>()
var wordIndex = wordlist.binarySearch(word)
if (wordIndex < 0) throw InvalidWordException(word)
for (it in 0..10) {
wordBits.add(wordIndex.rem(2) == 1)
wordIndex = wordIndex shr 1
}
bits.addAll(wordBits.reversed())
}
if (bits.size.rem(33) != 0) throw InvalidMnemonicLength()
val checksumSize = bits.size / 33
val checksum = bits.subList(bits.size - checksumSize, bits.size)
val entropy = bits.subList(0, bits.size - checksumSize)
if (computeChecksum(entropy) != checksum) throw InvalidChecksumException()
return byteArray(entropy)
}
}
class InvalidChecksumException : Exception()
class InvalidWordException(word: String) : Exception(word)
class InvalidMnemonicLength : Exception()

View File

@@ -0,0 +1,78 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.crypto
class Key(val bytes: ByteArray)
interface Crypto {
fun sha256(): Sha256
fun hmacSha256(): HmacSha256
fun aesGcm(key: Key): AesGcm
fun secureRandomBytes(numBytes: Int): ByteArray
fun generateKey(): Key {
return Key(secureRandomBytes(32))
}
fun deriveKey(master: Key, subkeyName: String): Key {
val mac = hmacSha256()
mac.init(master.bytes)
mac.update(subkeyName)
return Key(mac.finalize())
}
}
interface Sha256 {
fun update(byte: Byte)
fun finalize(): ByteArray
}
interface HmacSha256 {
fun init(key: ByteArray)
fun update(byte: Byte)
fun finalize(): ByteArray
fun update(msg: String) {
for (b in msg.encodeToByteArray()) {
update(b)
}
}
}
interface AesGcm {
fun encrypt(msg: ByteArray, iv: ByteArray): ByteArray
fun decrypt(cipherText: ByteArray): ByteArray
}
fun Byte.toBits(): List<Boolean> = (7 downTo 0).map { (toInt() and (1 shl it)) != 0 }
fun ByteArray.toBits(): List<Boolean> = flatMap { it.toBits() }
fun byteArrayOfInts(vararg b: Int) = b.map { it.toByte() }.toByteArray()
fun byteArray(bits: List<Boolean>): ByteArray {
var byte = 0
val bytes = ByteArray(bits.size / 8)
bits.forEachIndexed { i, b ->
byte = byte shl 1
if (b) byte += 1
if (i.rem(8) == 7) {
bytes[i / 8] = byte.toByte()
}
}
return bytes
}

View File

@@ -0,0 +1,83 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.crypto
import java.nio.ByteBuffer
import java.security.MessageDigest
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.Mac
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.SecretKeySpec
class JavaCrypto : Crypto {
override fun sha256() = JavaSha256()
override fun hmacSha256() = JavaHmacSha256()
override fun aesGcm(key: Key) = JavaAesGcm(key)
override fun secureRandomBytes(numBytes: Int): ByteArray {
val sr = SecureRandom()
val bytes = ByteArray(numBytes)
sr.nextBytes(bytes)
return bytes
}
}
class JavaSha256 : Sha256 {
private val md = MessageDigest.getInstance("SHA-256")
override fun update(byte: Byte) = md.update(byte)
override fun finalize(): ByteArray = md.digest()
}
class JavaHmacSha256 : HmacSha256 {
private val mac = Mac.getInstance("HmacSHA256")
override fun init(key: ByteArray) = mac.init(SecretKeySpec(key, "HmacSHA256"))
override fun update(byte: Byte) = mac.update(byte)
override fun finalize(): ByteArray = mac.doFinal()
}
class JavaAesGcm(val key: Key) : AesGcm {
override fun encrypt(msg: ByteArray, iv: ByteArray): ByteArray {
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, SecretKeySpec(key.bytes, "AES"), GCMParameterSpec(128, iv))
val encrypted = cipher.doFinal(msg)
return ByteBuffer
.allocate(iv.size + encrypted.size)
.put(iv)
.put(encrypted)
.array()
}
override fun decrypt(cipherText: ByteArray): ByteArray {
val buffer = ByteBuffer.wrap(cipherText)
val iv = ByteArray(12)
buffer.get(iv)
val encrypted = ByteArray(buffer.remaining())
buffer.get(encrypted)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, SecretKeySpec(key.bytes, "AES"), GCMParameterSpec(128, iv))
return cipher.doFinal(encrypted)
}
}
fun ByteArray.toHexString(): String {
val sb = StringBuilder()
for (b in this) sb.append(String.format("%02x", b))
return sb.toString()
}

View File

@@ -0,0 +1,126 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.crypto
import kotlinx.coroutines.runBlocking
import org.isoron.platform.io.JavaFileOpener
import org.junit.Assert.assertEquals
import org.junit.Before
import org.junit.Test
import kotlin.test.assertFailsWith
class Bip39Test {
private lateinit var bip39: Bip39
private val phrases = listOf(
listOf(
"gather",
"capable",
"since",
),
listOf(
"exit",
"churn",
"hazard",
"garage",
"hint",
"great",
),
listOf(
"exile",
"blouse",
"athlete",
"dinner",
"chef",
"home",
"destroy",
"disagree",
"select",
"eight",
"slim",
"talent",
),
)
private val entropies = listOf(
byteArrayOfInts(0x60, 0x64, 0x3f, 0x24),
byteArrayOfInts(0x4f, 0xe5, 0x19, 0xa7, 0xaf, 0xb6, 0xbc, 0xcc),
byteArrayOfInts(
0x4f,
0xa3,
0x04,
0x38,
0x9f,
0x22,
0x74,
0xda,
0x0f,
0x09,
0xf6,
0xc3,
0x48,
0xdf,
0x2f,
0x6e,
)
)
@Before
fun setUp() = runBlocking {
bip39 = Bip39(JavaFileOpener().openResourceFile("bip39/en_US.txt").lines(), JavaCrypto())
}
@Test
fun test_encode_decode() {
phrases.zip(entropies).forEach { (phrase, entropy) ->
assertEquals(phrase, bip39.encode(entropy))
assertEquals(entropy.toHexString(), bip39.decode(phrase).toHexString())
}
}
@Test
fun test_decode_invalid_checksum() {
assertFailsWith<InvalidChecksumException> {
bip39.decode(
listOf(
"lawn",
"dirt",
"work",
"mountain",
"depth",
"loyal",
"citizen",
"theory",
"cram",
"trip",
"boil",
"about",
)
)
}
}
@Test
fun test_decode_invalid_word() {
assertFailsWith<InvalidWordException> {
bip39.decode(listOf("dirt", "bee", "work"))
}
}
}

View File

@@ -0,0 +1,234 @@
/*
* Copyright (C) 2016-2021 Álinson Santos Xavier <git@axavier.org>
*
* This file is part of Loop Habit Tracker.
*
* Loop Habit Tracker is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by the
* Free Software Foundation, either version 3 of the License, or (at your
* option) any later version.
*
* Loop Habit Tracker is distributed in the hope that it will be useful, but
* WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
* or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package org.isoron.platform.crypto
import org.junit.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotEquals
class CryptoTest {
private val crypto = JavaCrypto()
@Test
fun test_sha256() {
val sha256 = crypto.sha256()
sha256.update(0x10.toByte())
sha256.update(0x20.toByte())
sha256.update(0x30.toByte())
val digest = sha256.finalize()
assertEquals(32, digest.size)
assertEquals(0x8e.toByte(), digest[0])
assertEquals(0x13.toByte(), digest[1])
assertEquals(0x36.toByte(), digest[2])
assertEquals(0xb9.toByte(), digest[31])
}
@Test
fun test_hmacsha256() {
val hmac = crypto.hmacSha256()
hmac.init(byteArrayOfInts(0x01, 0x02, 0x03))
hmac.update(0x40.toByte())
hmac.update("AB")
val checksum = hmac.finalize()
assertEquals(32, checksum.size)
assertEquals(0x6d.toByte(), checksum[0])
assertEquals(0xc9.toByte(), checksum[1])
assertEquals(0x05.toByte(), checksum[2])
assertEquals(0xa1.toByte(), checksum[31])
}
@Test
fun test_aes_gcm() {
val msg = byteArrayOfInts(
0x2f,
0xdc,
0xaa,
0x41,
0xfa,
0xb8,
0x5e,
0xe8,
0xa3,
0x12,
0x69,
0x68,
0x14,
0x31,
0xd8,
0x59,
0x74,
0x29,
0x2e,
0xae,
0xed,
0x76,
0x0a,
0x56,
0x46,
0x90,
0xb6,
0xcb,
0x9f,
0x37,
0xbe,
0xae,
)
val key = Key(
byteArrayOfInts(
0xed,
0xa8,
0xc3,
0xc6,
0x44,
0x1e,
0xa1,
0xd5,
0x71,
0x8c,
0x71,
0x45,
0xbe,
0x2d,
0xf7,
0xa4,
0x81,
0x2e,
0x0a,
0x0b,
0xa8,
0xe4,
0x20,
0x49,
0x94,
0x8a,
0x71,
0x1a,
0x15,
0xf5,
0x29,
0x78,
)
)
val iv = byteArrayOfInts(
0xa7,
0xef,
0xe1,
0xba,
0xdf,
0x4f,
0x85,
0xca,
0xc3,
0x81,
0xc1,
0x93,
)
val expected = byteArrayOfInts(
// iv
0xa7,
0xef,
0xe1,
0xba,
0xdf,
0x4f,
0x85,
0xca,
0xc3,
0x81,
0xc1,
0x93,
// msg
0x24,
0xe7,
0x26,
0x9b,
0xb8,
0x59,
0xf0,
0xe0,
0x4f,
0xda,
0xc0,
0x85,
0xc6,
0x23,
0x21,
0x61,
0x80,
0x59,
0xd6,
0x18,
0xee,
0xa0,
0xd8,
0x00,
0xe3,
0xdf,
0x6e,
0xcf,
0x89,
0x82,
0xfd,
0x63,
// verification tag
0xe9,
0xe9,
0xac,
0x92,
0xdc,
0xb1,
0x7c,
0x2d,
0x9a,
0x73,
0xda,
0x25,
0x6d,
0xda,
0xc0,
0x83,
)
val cipher = crypto.aesGcm(key)
val actual = cipher.encrypt(msg, iv)
assertEquals(actual.toHexString(), expected.toHexString())
val recovered = cipher.decrypt(actual)
assertEquals(msg.toHexString(), recovered.toHexString())
}
@Test
fun test_rand() {
val r1 = crypto.secureRandomBytes(8)
val r2 = crypto.secureRandomBytes(8)
assertEquals(8, r1.size)
assertNotEquals(r1.toBits(), r2.toBits())
}
@Test
fun test_derive_key() {
val k1 = Key(byteArrayOfInts(0x01, 0x02, 0x03))
val k2 = crypto.deriveKey(k1, "TEST")
assertEquals(0x44.toByte(), k2.bytes[0])
assertEquals(0xd3.toByte(), k2.bytes[31])
}
}