Implement basic cryptography functions

feature/sync2
Alinson S. Xavier 4 years ago
parent e26b643423
commit 1567e2c0ad
No known key found for this signature in database
GPG Key ID: DCA0DAD4D2F58624

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

@ -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()

@ -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
}

@ -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()
}

@ -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"))
}
}
}

@ -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])
}
}
Loading…
Cancel
Save