diff --git a/.gitignore b/.gitignore index 3f62a6e..ff4cc94 100644 --- a/.gitignore +++ b/.gitignore @@ -3,4 +3,5 @@ /build/ /build/classes/kotlin/js/test/ /build/classes/kotlin/jvm/main/ -/build/classes/kotlin/jvm/test/ \ No newline at end of file +/build/classes/kotlin/jvm/test/ +/.idea \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt b/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt index 0703678..537c27a 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/PasswordDerivationParams.kt @@ -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 { 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 + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt b/src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt index 87fc578..e5dd074 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/Registration.kt @@ -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(adapter: Adapter) { +class Registration(val adapter: Adapter,loginKeyStrength: Int = 4096): LogTag("SLREG") { sealed class Result { /** @@ -20,14 +34,69 @@ class Registration(adapter: Adapter) { * 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() + 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) + } } \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/RestoreAccessData.kt b/src/commonMain/kotlin/net.sergeych.superlogin/RestoreAccessData.kt deleted file mode 100644 index d61ba9c..0000000 --- a/src/commonMain/kotlin/net.sergeych.superlogin/RestoreAccessData.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.sergeych.superlogin - -import kotlinx.serialization.Serializable -import net.sergeych.unikrypto.PrivateKey -import net.sergeych.unikrypto.SymmetricKey - - diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt b/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt index 963db57..cbdea3a 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginClient.kt @@ -3,12 +3,15 @@ package net.sergeych.superlogin import net.sergeych.parsec3.Adapter - class SuperloginClient(adapter: Adapter<*>) { // init { // adapter.invokeCommand() // } + fun register() { + + } + } \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginPayload.kt b/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginPayload.kt new file mode 100644 index 0000000..3eb3d86 --- /dev/null +++ b/src/commonMain/kotlin/net.sergeych.superlogin/SuperloginPayload.kt @@ -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 +) { +} \ No newline at end of file diff --git a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt index 3530ed2..55eca68 100644 --- a/src/commonMain/kotlin/net.sergeych.superlogin/api.kt +++ b/src/commonMain/kotlin/net.sergeych.superlogin/api.kt @@ -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