Initial commit
This commit is contained in:
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())
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user