Initial commit

This commit is contained in:
sergeych committed 2022-11-11 00:22:26 +01:00
commit 8f950991a3
21 files changed
+3200

No files matched your search

@@ -0,0 +1,156 @@
package net.sergeych.superlogin
import kotlinx.serialization.Serializable
import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.unikrypto.Container
import net.sergeych.unikrypto.DecryptingKey
import net.sergeych.unikrypto.Safe58
import net.sergeych.unikrypto.SymmetricKey
import kotlin.reflect.KType
import kotlin.reflect.typeOf
/**
* Access Control Object (ACO) allows to decrypt vital application data using either some `passwordKey`, usually
* obtained from user password using [DerivedKeys], or a special randomly created `secret` string, which is
* hard to remember but cryptographically string and contains some protection against typing errors,
* see [RestoreKey] for details. Once the access control object is generated, it can be decrypted using:
*
* - regenerated password key, usually [DerivedKeys.loginAccessKey]
* - `restoreSecret`, that should only be known to a user
*
* It is possible to change data and/or `passwordKey` of a decrypted object. Use [packed] property on new instance
* to save encrypted updated data.
*
* AceesControlObject holds also application-specific `data`.
*
* _Important note. While the primary constructor is public, we strongly recommend never using it directly.
* To construct it please use one of:_
*
* - [AccessControlObject.pack] to generate new
* - [AccessControlObject.unpackWithPasswordKey] to decrypt it with a password key
* - [AccessControlObject.unpackWithSecret] to decrypt it with a `secret`
*
* @param payloadType used to properly serialize application=specific data for [payload]
* @param packed encrypted and packed representation of the object, could be stored in public.
* @param passwordKey the password-derived key that could be used to unpack the [packed] data
* @param data contains necessary data to keep and operate the ACO
*/
class AccessControlObject<T>(
val payloadType: KType,
val packed: ByteArray,
val passwordKey: SymmetricKey,
val data: Data<T>,
) {
/**
* The application-specific data stored in the ACO. Should be @Serializable or a simple type
*/
val payload: T by data::payload
/**
* The ID that can be used to index/find the stored ACO, restoreId is derived from the secret
* with [RestoreKey] and could be obtanied by application software independently. The recommended
* use is to identify stored login record that should include a copy of most recent
* packed AccessControlObject.
*/
val restoreId: ByteArray by data::restoreId
/**
* Internal use only. The data ACO needs to operate, including the application's [payload]. This is
* the content of encrypted packed ACO.
*/
@Serializable
class Data<T>(
val restoreId: ByteArray,
val derivedRestoreKey: SymmetricKey,
val passwordKey: SymmetricKey,
val payload: T,
)
/**
* Update payload and return new instance of te ACO. The `secret` and `passwordKey` are not changed.
* Use [packed] property on the result to save updated copy.
*/
inline fun <reified R>updatePayload(newPayload: R): AccessControlObject<R> {
val data = Data(restoreId, data.derivedRestoreKey, passwordKey, newPayload)
return AccessControlObject<R>(
typeOf<Data<R>>(),
Container.encrypt(
data,
passwordKey, data.derivedRestoreKey
),
passwordKey, data
)
}
/**
* Update the password key and return new instance. It could be decrypted with the same `secret` and
* [newPasswordKey] only. The [payload] is not changed.
*/
fun updatePasswordKey(newPasswordKey: SymmetricKey): AccessControlObject<T> {
val data = Data(restoreId, data.derivedRestoreKey, newPasswordKey, payload)
return AccessControlObject(
payloadType,
Container.encryptData(
BossEncoder.encode(payloadType,data),
newPasswordKey, data.derivedRestoreKey
),
newPasswordKey, data
)
}
class WrongKeyException : Exception("can't decrypt AccessControlObject")
companion object {
/**
* Create and pack brand-new access control object with new random `secret`. It returns [RestoreKey]
* instance which contains not only `secret` but also `restoreId` which are often needed to save (and identify)
* encrypted object in the cloud, and also the key used to encrypt it. _Never save the key to disk or
* transmit it over the network_. If need, derive it from s `secret` again, see [RestoreKey.parse].
* @param passwordKey the second key that is normally used to decrypt ACO
* @param payload application-specific data to keep inside the ACO
* @return pair where first is a [RestoreKey] instance containing new `secret` and so on, and packed encrypted
* ACO that could be restored using `secret` or a [passwordKey]
*/
inline suspend fun <reified T> pack(
passwordKey: SymmetricKey,
payload: T,
): Pair<RestoreKey, ByteArray> {
val restoreKey = RestoreKey.generate()
return restoreKey to Container.encrypt(
Data(restoreKey.restoreId, restoreKey.key, passwordKey,payload),
passwordKey, restoreKey.key
)
}
/**
* Unpack and decrypt ACO with a password key
* @return decrypted ACO or null if the key is wrong.
*/
inline fun <reified T> unpackWithPasswordKey(packed: ByteArray, passwordKey: SymmetricKey): AccessControlObject<T>? =
Container.decrypt<Data<T>>(packed, passwordKey)?.let {
println(it)
AccessControlObject(typeOf<Data<T>>(), packed, passwordKey, it)
}
/**
* Unpack and decrypt ACO with a [secret]. If you want to check [secret] integrity, use
* [RestoreKey.checkSecretIntegrity].
* @return deccrypted ACO or null if the secret is wrong
*/
suspend inline fun <reified T> unpackWithSecret(packed: ByteArray, secret: String): AccessControlObject<T>? {
try {
val (id, key) = RestoreKey.parse(secret)
return Container.decrypt<Data<T>>(packed, key)?.let { data ->
AccessControlObject(typeOf<Data<T>>(), packed, data.passwordKey, data)
}
}
catch(_: RestoreKey.InvalidSecretException) {
return null
}
}
}
}
@@ -0,0 +1,101 @@
package net.sergeych.superlogin
import kotlinx.coroutines.Deferred
import kotlinx.datetime.Clock
import net.sergeych.mp_tools.globalDefer
import net.sergeych.unikrypto.*
import kotlin.random.Random
import kotlin.time.Duration.Companion.seconds
object BackgroundKeyGenerator {
const val DefaultStrength = 4096
class EntropyLowException : Exception("entropy level is below requested")
private var keyStrength = DefaultStrength
private var nextKey: Deferred<PrivateKey>? = null
var entropy = 0
private set
private val hashAlgorithm = HashAlgorithm.SHA3_256
private var entropyHash: ByteArray? = null
fun startPrivateKeyGeneration(strength: Int = DefaultStrength) {
if (keyStrength != strength) {
nextKey?.cancel()
nextKey = null
keyStrength = strength
}
nextKey = globalDefer { AsymmetricKeys.generate(keyStrength) }
}
fun getKeyAsync(strength: Int = DefaultStrength, startNext: Boolean = true): Deferred<PrivateKey> {
if (keyStrength != strength || nextKey == null)
startPrivateKeyGeneration(strength)
return nextKey!!.also {
nextKey = null
if (startNext) startPrivateKeyGeneration(strength)
}
}
suspend fun getPrivateKey(strength: Int = DefaultStrength): PrivateKey = getKeyAsync(strength).await()
fun addEntropyString(s: String, count: Int = 10) {
entropy += count
entropyHash = entropyHash?.let { hashAlgorithm.digest(s.encodeToByteArray(), it) } ?: hashAlgorithm.digest(s)
}
fun addEntropyPointerMove(x: Int, y: Int) {
addEntropyString("x:$x,y:$y")
}
private var lastTime = Clock.System.now()
fun addEntropyKey(key: String?) {
val t = Clock.System.now()
val e = (if (t - lastTime < 1.seconds) 3 else 10) + (key?.length ?: 0) * 4
addEntropyString("${Clock.System.now().nanosecondsOfSecond}$key", e)
lastTime = t
}
fun addEntropyTimestamp() {
addEntropyKey(null)
}
fun randomSymmetricKey(minEntropy: Int = 0): SymmetricKey {
return SymmetricKeys.create(randomBytes(32, minEntropy), BytesId.random()).also {
entropy = 0
addEntropyTimestamp()
}
}
fun randomBytes(length: Int,minEntropy: Int): ByteArray {
entropyHash?.let {
if (minEntropy <= entropy) {
entropy -= minEntropy
addEntropyTimestamp()
return randomBytes(length, it)
}
}
throw EntropyLowException()
}
fun randomBytes(length: Int, IV: ByteArray? = null): ByteArray {
var result = byteArrayOf()
var iv2 = IV ?: Random.nextBytes(32)
entropyHash?.let { iv2 += it }
var seed = HashAlgorithm.SHA3_384.digest(Random.nextBytes(48), iv2)
while (result.size < length) {
if (result.size + seed.size <= length)
result += seed
else
result += seed.sliceArray(0 until (length - result.size))
seed = HashAlgorithm.SHA3_384.digest(Random.nextBytes(48), seed)
}
return result
}
}
@@ -0,0 +1,21 @@
package net.sergeych.superlogin
import net.sergeych.unikrypto.SymmetricKey
/**
* Super creds are derived from a password and given derivatino parameters.
* They should not being sent over the netowkr as is or stored so it is not serializable.
*/
class DerivedKeys(
val loginId: ByteArray,
val loginAccessKey: SymmetricKey,
val systemAccessKey: SymmetricKey
) {
companion object {
suspend fun derive(password: String, params: PasswordDerivationParams = PasswordDerivationParams()): DerivedKeys {
val kk = params.derive(password, 3)
return DerivedKeys(kk[0].keyBytes, kk[1], kk[2])
}
}
}
@@ -0,0 +1,20 @@
package net.sergeych.superlogin
import kotlinx.serialization.Serializable
import net.sergeych.unikrypto.HashAlgorithm
import net.sergeych.unikrypto.Passwords
import net.sergeych.unikrypto.SymmetricKey
import kotlin.random.Random
@Serializable
class PasswordDerivationParams(
val rounds: Int = 15000,
val algorithm: HashAlgorithm = HashAlgorithm.SHA3_256,
val salt: ByteArray = Random.nextBytes(32),
) {
suspend fun derive(password: String,amount: Int = 1): List<SymmetricKey> {
return Passwords.deriveKeys(password, amount, rounds, algorithm, salt, Passwords.KeyIdAlgorithm.Independent)
}
}
@@ -0,0 +1,33 @@
package net.sergeych.superlogin
import net.sergeych.parsec3.Adapter
import net.sergeych.parsec3.WithAdapter
/**
* Registration instances are used to perform registration
* in steps to not no regemerate all keys on every attempt to
* say change login.
*/
class Registration<T: WithAdapter>(adapter: Adapter<T>) {
sealed class Result {
/**
* Login is already in use or is somehow else invalid
*/
object InvalidLogin: Result()
/**
* Operation failed for nknown reason, usually it means
* network or server downtime
*/
object NetworkFailure: Result()
// class Success(val l)
}
// fun register(
// login: String,
// password: String,
// )
}
@@ -0,0 +1,7 @@
package net.sergeych.superlogin
import kotlinx.serialization.Serializable
import net.sergeych.unikrypto.PrivateKey
import net.sergeych.unikrypto.SymmetricKey
@@ -0,0 +1,69 @@
package net.sergeych.superlogin
import net.sergeych.boss_serialization_mp.BossEncoder
import net.sergeych.unikrypto.*
/**
* RestoreKey is a specially constructed highly random string that produces cryptographically
* independent ID and symmetric key tohether with some tolerance to human typing errors and minimal
* integrity check to simplify its entry.
*
* The restore key is too long to remember, it should better be copied to some safe place.It is intended
* as a last resort to restore access to something impprtant using [AccessControlObject] object.
*
* Construct new instance with [generate], check with [checkSecretIntegrity] and derive key and id
* with [parse]
*/
class RestoreKey private constructor(
val restoreId: ByteArray,
val key: SymmetricKey,
val secret: String,
) {
class InvalidSecretException: Exception("secret is not valid")
companion object {
/**
* Remove auxiliary characters from secret
*/
private fun p(secret: String): String = secret.trim().replace("-", "")
/**
* Perform internal integrity check against occasional mistypes. This check does
* not mean it the key are ok to decrypt anything, just that the secret was not mistyped.
*
* This method does not alter key strength which is 136 random bits.
*/
fun checkSecretIntegrity(secret: String): Boolean {
return kotlin.runCatching { Safe58.decodeWithCrc(p(secret)) }.isSuccess
}
/**
* Parse the secret (secret key combination) and provide corresponding id and key to
* find and decrupt [AccessControlObject] object.
* @return restoreId to restoreKey pair
* @throws Safe58.InvalidCrcException if the secret is not valid, see [checkSecretIntegrity].
*/
suspend fun parse(secret: String): Pair<ByteArray, SymmetricKey> {
// Just ensure secret is valid
val s = p(secret)
if( !checkSecretIntegrity(s) ) throw InvalidSecretException()
return Passwords.deriveKeys(
s,
2,
salt = HashAlgorithm.SHA3_256.digest(s + "RestoreKeySecret"),
rounds = 1000
).let { (a, b) -> a.keyBytes to b }
}
/**
* Generate brand new random secret and return it and parsed id and key.
*/
suspend fun generate(): RestoreKey {
val secret = Safe58.encodeWithCrc(BackgroundKeyGenerator.randomBytes(17))
val (id, key) = parse(secret)
return RestoreKey(id, key, secret.chunked(5).joinToString("-"))
}
}
}
@@ -0,0 +1,14 @@
package net.sergeych.superlogin
import net.sergeych.parsec3.Adapter
class SuperloginClient(adapter: Adapter<*>) {
// init {
// adapter.invokeCommand()
// }
}
@@ -0,0 +1,61 @@
package net.sergeych.superlogin
import kotlinx.serialization.Serializable
import net.sergeych.parsec3.CommandHost
import net.sergeych.parsec3.WithAdapter
import net.sergeych.unikrypto.PublicKey
@Serializable
data class RegistrationArgs(
val loginName: String,
val loginId: ByteArray,
val loginPublicKey: PublicKey,
val derivationParams: PasswordDerivationParams,
val loginData: ByteArray,
val restoreId: ByteArray,
val restoreData: ByteArray,
val extraData: ByteArray? = null
)
@Serializable
sealed class AuthenticationResult {
@Serializable
data class Success(
val loginToken: ByteArray,
val extraData: ByteArray?
): AuthenticationResult()
@Serializable
object LoginUnavailable: AuthenticationResult()
@Serializable
object RestoreIdUnavailable: AuthenticationResult()
}
@Serializable
data class LoginArgs(
val loginId: ByteArray,
val packedSignedRecord: ByteArray
)
@Serializable
data class LoginData(
val encryptedPrivateKey: ByteArray,
val loginNonce: ByteArray
)
class SuperloginServerApi<T: WithAdapter> : CommandHost<T>() {
val registerUser by command<RegistrationArgs,AuthenticationResult>()
val loginUserByToken by command<ByteArray,AuthenticationResult>()
val requestUserLoginParams by command<String,PasswordDerivationParams>()
/**
* Get resstoreData by restoreId: password reset procedure start.
*/
val requestUserLogin by command<ByteArray,ByteArray>()
// val performLogin by command<LoginArgs
}
@@ -0,0 +1,48 @@
package superlogin
import kotlinx.coroutines.test.runTest
import net.sergeych.superlogin.AccessControlObject
import net.sergeych.superlogin.RestoreKey
import net.sergeych.unikrypto.SymmetricKeys
import kotlin.test.*
internal class AccessControlObjectTest {
@Test
fun createRestoreTest() = runTest {
val pk1 = SymmetricKeys.random()
val pk2 = SymmetricKeys.random()
val (rk, packed1) = AccessControlObject.pack(pk1, 117)
println(rk.secret)
val ac1 = AccessControlObject.unpackWithPasswordKey<Int>(packed1,pk1)
assertNotNull(ac1)
assertEquals(117, ac1.payload)
val ac2 = AccessControlObject.unpackWithSecret<Int>(packed1,rk.secret)
assertNotNull(ac2)
assertEquals(117, ac2.payload)
assertNull(AccessControlObject.unpackWithPasswordKey<Int>(packed1,pk2))
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,"the_-wrong-secret-yess"))
val (rk2, packed2) = AccessControlObject.pack(pk2, 107)
assertNull(AccessControlObject.unpackWithSecret<Int>(packed1,rk2.secret))
var ac21 = AccessControlObject.unpackWithPasswordKey<Int>(packed2,pk2)
assertNotNull(ac21)
assertEquals(107, ac21.payload)
var packed3 = ac1.updatePayload(121).packed
ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk1)
assertNotNull(ac21)
assertEquals(121, ac21.payload)
packed3 = ac1.updatePasswordKey(pk2).packed
println("-------")
ac21 = AccessControlObject.unpackWithPasswordKey(packed3,pk2)
assertNotNull(ac21)
assertEquals(117, ac21.payload)
ac21 = AccessControlObject.unpackWithSecret(packed3,rk.secret)
assertNotNull(ac21)
assertEquals(117, ac21.payload)
}
}
@@ -0,0 +1,28 @@
package superlogin
import kotlinx.coroutines.test.runTest
import net.sergeych.superlogin.RestoreKey
import kotlin.test.*
internal class RestoreKeyTest {
@Test
fun checkRestoreKey() = runTest {
val rk = RestoreKey.generate()
println(rk.secret)
println(rk.restoreId)
println(rk.key)
val (id, k) = RestoreKey.parse(rk.secret)
assertContentEquals(rk.restoreId, id)
assertEquals(rk.key.id, k.id)
assertContentEquals(rk.key.keyBytes, k.keyBytes)
val x = StringBuilder(rk.secret)
x[0] = (x[0].code xor 0x75).toChar()
assertFails {
RestoreKey.parse(x.toString())
}
}
}