finally, minimal and straight registration schema and client implementation

This commit is contained in:
Sergey Chernov 2022-11-20 20:53:59 +01:00
parent 8f950991a3
commit 8b8724b0f1
7 changed files with 136 additions and 23 deletions

1
.gitignore vendored
View File

@ -4,3 +4,4 @@
/build/classes/kotlin/js/test/
/build/classes/kotlin/jvm/main/
/build/classes/kotlin/jvm/test/
/.idea

View File

@ -6,6 +6,10 @@ import net.sergeych.unikrypto.Passwords
import net.sergeych.unikrypto.SymmetricKey
import kotlin.random.Random
/**
* Common structure to keep PBKDF2 params to safely generate keys
* and a method to derive password accordingly.
*/
@Serializable
class PasswordDerivationParams(
val rounds: Int = 15000,
@ -13,8 +17,33 @@ class PasswordDerivationParams(
val salt: ByteArray = Random.nextBytes(32),
) {
/**
* Derive one or more keys in cryptographically-independent manner
* @param password password to derive keys from
* @param amount deisred number of keys (all of them will be mathematically and cryptographically independent)
* @return list of generated symmetric keys (with a proper ids that allow independent derivation later)
*/
suspend fun derive(password: String,amount: Int = 1): List<SymmetricKey> {
return Passwords.deriveKeys(password, amount, rounds, algorithm, salt, Passwords.KeyIdAlgorithm.Independent)
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || this::class != other::class) return false
other as PasswordDerivationParams
if (rounds != other.rounds) return false
if (algorithm != other.algorithm) return false
if (!salt.contentEquals(other.salt)) return false
return true
}
override fun hashCode(): Int {
var result = rounds
result = 31 * result + algorithm.hashCode()
result = 31 * result + salt.contentHashCode()
return result
}
}

View File

@ -1,14 +1,28 @@
package net.sergeych.superlogin
import net.sergeych.mp_logger.LogTag
import net.sergeych.mp_logger.error
import net.sergeych.mp_logger.exception
import net.sergeych.parsec3.Adapter
import net.sergeych.parsec3.WithAdapter
import net.sergeych.unikrypto.HashAlgorithm
import net.sergeych.unikrypto.SymmetricKey
import net.sergeych.unikrypto.digest
/**
* Registration instances are used to perform registration
* in steps to not no regemerate all keys on every attempt to
* say change login.
* Registration instances are used to perform superlogin-compatible registration in a smart way
* avoiding unnecessary time-consuming password derivation and key generation repetitions.
* once calculated these values are kept in the instance unless new password or different
* password derivation parameters are supplied. This is useful in the case the login in user error
* result - user can provide new login and re-register times faster than otherwise re-deriving
* and creating new keys.
*
* It is important to use _different_ instances for different registrations, and the same instance
* for consecutive attempts to register the same user varying login only.
*
* See [register] for more.
*/
class Registration<T: WithAdapter>(adapter: Adapter<T>) {
class Registration<T: WithAdapter>(val adapter: Adapter<T>,loginKeyStrength: Int = 4096): LogTag("SLREG") {
sealed class Result {
/**
@ -20,14 +34,69 @@ class Registration<T: WithAdapter>(adapter: Adapter<T>) {
* Operation failed for nknown reason, usually it means
* network or server downtime
*/
object NetworkFailure: Result()
class NetworkFailure(val exception: Throwable?=null): Result()
// class Success(val l)
class Success(val secret: String,val dataKey: SymmetricKey,loginToken: ByteArray): Result()
}
// fun register(
// login: String,
// password: String,
// )
private var lastPasswordHash: ByteArray? = null
private var lastDerivationParams: PasswordDerivationParams? = null
private var passwordKeys: DerivedKeys? = null
val api = SuperloginServerApi<T>()
private val deferredLoginKey = BackgroundKeyGenerator.getKeyAsync(loginKeyStrength)
private val dataKey = BackgroundKeyGenerator.randomSymmetricKey()
/**
* Smart attempt to register. It is ok to repeatedly call it if the result is not [Result.Success]: it will
* cache internal data and reuse time-consuming precalculated values for caches and derived keys if the
* password and derivarion parameters are not changed between calls.
*/
suspend fun register(
login: String,
password: String,
derivationParams: PasswordDerivationParams = PasswordDerivationParams()
): Result {
val newPasswordHash = HashAlgorithm.SHA3_256.digest(password)
if( lastPasswordHash?.contentEquals(newPasswordHash) != true ||
lastDerivationParams?.equals(derivationParams) != true ||
passwordKeys == null ) {
passwordKeys = DerivedKeys.derive(password,derivationParams)
lastDerivationParams = derivationParams
lastPasswordHash = newPasswordHash
}
val spl = SuperloginPayload(login, deferredLoginKey.await(), dataKey)
repeat(10) {
val (restoreKey, restoreData) = AccessControlObject.pack(passwordKeys!!.loginAccessKey,spl)
try {
val result = adapter.invokeCommand(
api.registerUser, RegistrationArgs(
login,
passwordKeys!!.loginId,
deferredLoginKey.await().publicKey,
derivationParams,
restoreKey.restoreId, restoreData
)
)
when (result) {
AuthenticationResult.RestoreIdUnavailable -> {
// rare situation but still possible: just repack the ACO
}
AuthenticationResult.LoginUnavailable -> return Result.InvalidLogin
is AuthenticationResult.Success -> return Result.Success(
restoreKey.secret,
dataKey,
result.loginToken
)
}
}
catch(x: Throwable) {
exception { "Failed to register" to x }
}
}
// If we still get there, its a strange error - 10 times we cant get suitable restoreId...
error { "Failed to register after 10 repetitions with no apparent reason" }
return Result.NetworkFailure(null)
}
}

View File

@ -1,7 +0,0 @@
package net.sergeych.superlogin
import kotlinx.serialization.Serializable
import net.sergeych.unikrypto.PrivateKey
import net.sergeych.unikrypto.SymmetricKey

View File

@ -3,12 +3,15 @@ package net.sergeych.superlogin
import net.sergeych.parsec3.Adapter
class SuperloginClient(adapter: Adapter<*>) {
// init {
// adapter.invokeCommand()
// }
fun register() {
}
}

View File

@ -0,0 +1,21 @@
package net.sergeych.superlogin
import kotlinx.serialization.Serializable
import net.sergeych.unikrypto.PrivateKey
import net.sergeych.unikrypto.SymmetricKey
/**
* The base payload class for superlogin dance, it contains basic information
* needed for an average superlogin system:
*
* - login (whatever string, say, email)
* - login private key (used only to sign in to a service)
* - storage key (used to safely keep stored data)
*/
@Serializable
data class SuperloginPayload(
val login: String,
val loginPrivateKey: PrivateKey,
val dataStorageKey: SymmetricKey
) {
}

View File

@ -12,10 +12,8 @@ data class RegistrationArgs(
val loginId: ByteArray,
val loginPublicKey: PublicKey,
val derivationParams: PasswordDerivationParams,
val loginData: ByteArray,
val restoreId: ByteArray,
val restoreData: ByteArray,
val extraData: ByteArray? = null
val restoreData: ByteArray
)
@Serializable
@ -23,7 +21,6 @@ sealed class AuthenticationResult {
@Serializable
data class Success(
val loginToken: ByteArray,
val extraData: ByteArray?
): AuthenticationResult()
@Serializable