serialize model credentials as List

This commit is contained in:
Adam Velebil 2022-05-06 14:27:33 +02:00
parent b7ee31c70c
commit 0a65ad6a73
No known key found for this signature in database
GPG Key ID: AC6D6B9D715FC084
8 changed files with 83 additions and 66 deletions

View File

@ -3,8 +3,6 @@ package com.yubico.authenticator.oath
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
val jsonSerializer = Json { val jsonSerializer = Json {
// allows us to use Credential as a key
allowStructuredMapKeys = true
// creates properties for default values // creates properties for default values
encodeDefaults = true encodeDefaults = true
} }

View File

@ -93,36 +93,41 @@ class Model {
} }
private var _credentials = mutableMapOf<Credential, Code?>(); private set
var session = Session() var session = Session()
var credentials = mutableMapOf<Credential, Code?>(); private set val credentials: List<CredentialWithCode>
get() = _credentials.map {
CredentialWithCode(it.key, it.value)
}
// resets the model to initial values // resets the model to initial values
// used when a usb key has been disconnected // used when a usb key has been disconnected
fun reset() { fun reset() {
this.credentials.clear() this._credentials.clear()
this.session = Session() this.session = Session()
} }
fun update(deviceId: String, credentials: Map<Credential, Code?>) { fun update(deviceId: String, credentials: Map<Credential, Code?>) {
if (this.session.deviceId != deviceId) { if (this.session.deviceId != deviceId) {
// device was changed, we use the new list // device was changed, we use the new list
this.credentials.clear() this._credentials.clear()
this.credentials.putAll(from = credentials) this._credentials.putAll(from = credentials)
this.session = Session(deviceId) this.session = Session(deviceId)
} else { } else {
// update codes for non interactive keys // update codes for non interactive keys
for ((credential, code) in credentials) { for ((credential, code) in credentials) {
if (!credential.isInteractive()) { if (!credential.isInteractive()) {
this.credentials[credential] = code this._credentials[credential] = code
} }
} }
// remove obsolete credentials // remove obsolete credentials
this.credentials.filter { entry -> this._credentials.filter { entry ->
// get only keys which are not present in the input map // get only keys which are not present in the input map
!credentials.contains(entry.key) !credentials.contains(entry.key)
}.forEach(action = { }.forEach(action = {
this.credentials.remove(it.key) this._credentials.remove(it.key)
}) })
} }
} }
@ -132,7 +137,7 @@ class Model {
return null return null
} }
credentials[credential] = code _credentials[credential] = code
return CredentialWithCode(credential, code) return CredentialWithCode(credential, code)
} }
@ -150,15 +155,15 @@ class Model {
return null return null
} }
if (!credentials.contains(oldCredential)) { if (!_credentials.contains(oldCredential)) {
return null return null
} }
// preserve code // preserve code
val code = credentials[oldCredential] val code = _credentials[oldCredential]
credentials.remove(oldCredential) _credentials.remove(oldCredential)
credentials[newCredential] = code _credentials[newCredential] = code
return newCredential return newCredential
} }
@ -168,11 +173,11 @@ class Model {
return null return null
} }
if (!credentials.contains(credential)) { if (!_credentials.contains(credential)) {
return null return null
} }
credentials[credential] = code _credentials[credential] = code
return code return code
} }

View File

@ -105,9 +105,9 @@ class ModelTest {
assertEquals("device1", model.session.deviceId) assertEquals("device1", model.session.deviceId)
assertEquals(3, model.credentials.size) assertEquals(3, model.credentials.size)
assertTrue(model.credentials.containsKey(cred1)) assertTrue(model.credentials.find { it.credential == cred1 } != null)
assertTrue(model.credentials.containsKey(cred2)) assertTrue(model.credentials.find { it.credential == cred2 } != null)
assertTrue(model.credentials.containsKey(cred3)) assertTrue(model.credentials.find { it.credential == cred3 } != null)
} }
@Test @Test
@ -117,13 +117,13 @@ class ModelTest {
val m1 = mapOf(cred to code) val m1 = mapOf(cred to code)
model.update(cred.deviceId, m1) model.update(cred.deviceId, m1)
assertTrue(model.credentials.containsValue(code)) assertTrue(model.credentials.find { it.code == code } != null)
val updatedCode = code(value = "121212") val updatedCode = code(value = "121212")
val m2 = mapOf(cred to updatedCode) val m2 = mapOf(cred to updatedCode)
model.update(cred.deviceId, m2) model.update(cred.deviceId, m2)
assertTrue(model.credentials.containsValue(updatedCode)) assertTrue(model.credentials.find { it.code == updatedCode } != null)
} }
@Test @Test
@ -138,16 +138,16 @@ class ModelTest {
val m1 = mapOf(hotp to hotpCode, totp to totpCode) val m1 = mapOf(hotp to hotpCode, totp to totpCode)
model.update(d, m1) model.update(d, m1)
assertTrue(model.credentials.containsValue(hotpCode)) assertTrue(model.credentials.find { it.code == hotpCode } != null)
val updatedTotpCode = code(value = "121212") val updatedTotpCode = code(value = "121212")
val updatedHotpCode = code(value = "098765") val updatedHotpCode = code(value = "098765")
val m2 = mapOf(hotp to updatedHotpCode, totp to updatedTotpCode) val m2 = mapOf(hotp to updatedHotpCode, totp to updatedTotpCode)
model.update(d, m2) model.update(d, m2)
assertTrue(model.credentials.containsValue(updatedTotpCode)) assertTrue(model.credentials.find { it.code == updatedTotpCode } != null)
assertTrue(model.credentials.containsValue(hotpCode)) assertTrue(model.credentials.find { it.code == hotpCode } != null)
assertFalse(model.credentials.containsValue(updatedHotpCode)) assertFalse(model.credentials.find { it.code == updatedHotpCode } != null)
} }
@Test @Test
@ -166,7 +166,7 @@ class ModelTest {
model.update(d, mapOf(totp to newCode)) model.update(d, mapOf(totp to newCode))
assertEquals(1, model.credentials.size) assertEquals(1, model.credentials.size)
assertEquals("00000", model.credentials[totp]?.value) assertEquals("00000", model.credentials.find { it.credential == totp }?.code?.value)
} }
@Test @Test
@ -203,7 +203,7 @@ class ModelTest {
// only t1 is part of credentials // only t1 is part of credentials
assertEquals(1, model.credentials.size) assertEquals(1, model.credentials.size)
assertTrue(model.credentials.containsKey(t1)) assertTrue(model.credentials.find { it.credential == t1 } != null)
} }
@Test @Test

View File

@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.encodeToJsonElement import kotlinx.serialization.json.encodeToJsonElement
import org.junit.Assert.assertEquals import org.junit.Assert.*
import org.junit.Assert.assertTrue
import org.junit.Test import org.junit.Test
class SerializationTest { class SerializationTest {
@ -16,7 +15,7 @@ class SerializationTest {
@Test @Test
fun `serialization settings`() { fun `serialization settings`() {
assertTrue(jsonSerializer.configuration.encodeDefaults) assertTrue(jsonSerializer.configuration.encodeDefaults)
assertTrue(jsonSerializer.configuration.allowStructuredMapKeys) assertFalse(jsonSerializer.configuration.allowStructuredMapKeys)
} }
@Test @Test
@ -94,32 +93,25 @@ class SerializationTest {
@Test @Test
fun `credentials json type`() { fun `credentials json type`() {
val m = mapOf(totp() to code(), hotp() to code()) val l = listOf(
Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
)
val jsonElement = jsonSerializer.encodeToJsonElement(m) val jsonElement = jsonSerializer.encodeToJsonElement(l)
assertTrue(jsonElement is JsonArray) assertTrue(jsonElement is JsonArray)
} }
@Test @Test
fun `credentials json size`() { fun `credentials json size`() {
val m1 = mapOf<Model.Credential, Model.Code?>() val l1 = listOf<Model.CredentialWithCode>()
val jsonElement1 = jsonSerializer.encodeToJsonElement(m1) as JsonArray val jsonElement1 = jsonSerializer.encodeToJsonElement(l1) as JsonArray
assertEquals(0, jsonElement1.size) assertEquals(0, jsonElement1.size)
val m2 = mapOf(totp() to code(), hotp() to code()) val l2 = listOf(
val jsonElement2 = jsonSerializer.encodeToJsonElement(m2) as JsonArray Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
assertEquals(4, jsonElement2.size) )
val jsonElement2 = jsonSerializer.encodeToJsonElement(l2) as JsonArray
assertEquals(2, jsonElement2.size)
} }
@Test
fun `credentials json content`() {
val m = mapOf(totp() to code())
val jsonElement = jsonSerializer.encodeToJsonElement(m) as JsonArray
// the first element is Credential which has device_id property
assertTrue((jsonElement[0] as JsonObject).containsKey("device_id"))
// the second element is Credential which has value property
assertTrue((jsonElement[1] as JsonObject).containsKey("value"))
}
} }

View File

@ -29,23 +29,10 @@ class _CredentialsProvider extends StateNotifier<List<OathPair>?> {
void setFromString(String input) { void setFromString(String input) {
var result = jsonDecode(input); var result = jsonDecode(input);
/// structure of data in the json object is: if (result is List) {
/// [credential1, code1, credential2, code2, ...] state = result.map((e) => OathPair.fromJson(e)).toList();
} else {
final List<OathPair> pairs = []; state = [];
if (result is List<dynamic>) {
for (var index = 0; index < result.length / 2; index++) {
final credential = result[index * 2];
final code = result[index * 2 + 1];
pairs.add(
OathPair(
OathCredential.fromJson(credential),
code == null ? null : OathCode.fromJson(code),
),
);
} }
} }
state = pairs;
}
} }

View File

@ -53,6 +53,9 @@ class OathCode with _$OathCode {
@freezed @freezed
class OathPair with _$OathPair { class OathPair with _$OathPair {
factory OathPair(OathCredential credential, OathCode? code) = _OathPair; factory OathPair(OathCredential credential, OathCode? code) = _OathPair;
factory OathPair.fromJson(Map<String, dynamic> json) =>
_$OathPairFromJson(json);
} }
@freezed @freezed

View File

@ -474,6 +474,10 @@ abstract class _OathCode implements OathCode {
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
} }
OathPair _$OathPairFromJson(Map<String, dynamic> json) {
return _OathPair.fromJson(json);
}
/// @nodoc /// @nodoc
class _$OathPairTearOff { class _$OathPairTearOff {
const _$OathPairTearOff(); const _$OathPairTearOff();
@ -484,6 +488,10 @@ class _$OathPairTearOff {
code, code,
); );
} }
OathPair fromJson(Map<String, Object?> json) {
return OathPair.fromJson(json);
}
} }
/// @nodoc /// @nodoc
@ -494,6 +502,7 @@ mixin _$OathPair {
OathCredential get credential => throw _privateConstructorUsedError; OathCredential get credential => throw _privateConstructorUsedError;
OathCode? get code => throw _privateConstructorUsedError; OathCode? get code => throw _privateConstructorUsedError;
Map<String, dynamic> toJson() => throw _privateConstructorUsedError;
@JsonKey(ignore: true) @JsonKey(ignore: true)
$OathPairCopyWith<OathPair> get copyWith => $OathPairCopyWith<OathPair> get copyWith =>
throw _privateConstructorUsedError; throw _privateConstructorUsedError;
@ -594,10 +603,13 @@ class __$OathPairCopyWithImpl<$Res> extends _$OathPairCopyWithImpl<$Res>
} }
/// @nodoc /// @nodoc
@JsonSerializable()
class _$_OathPair implements _OathPair { class _$_OathPair implements _OathPair {
_$_OathPair(this.credential, this.code); _$_OathPair(this.credential, this.code);
factory _$_OathPair.fromJson(Map<String, dynamic> json) =>
_$$_OathPairFromJson(json);
@override @override
final OathCredential credential; final OathCredential credential;
@override @override
@ -628,11 +640,18 @@ class _$_OathPair implements _OathPair {
@override @override
_$OathPairCopyWith<_OathPair> get copyWith => _$OathPairCopyWith<_OathPair> get copyWith =>
__$OathPairCopyWithImpl<_OathPair>(this, _$identity); __$OathPairCopyWithImpl<_OathPair>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$$_OathPairToJson(this);
}
} }
abstract class _OathPair implements OathPair { abstract class _OathPair implements OathPair {
factory _OathPair(OathCredential credential, OathCode? code) = _$_OathPair; factory _OathPair(OathCredential credential, OathCode? code) = _$_OathPair;
factory _OathPair.fromJson(Map<String, dynamic> json) = _$_OathPair.fromJson;
@override @override
OathCredential get credential; OathCredential get credential;
@override @override

View File

@ -46,6 +46,19 @@ Map<String, dynamic> _$$_OathCodeToJson(_$_OathCode instance) =>
'valid_to': instance.validTo, 'valid_to': instance.validTo,
}; };
_$_OathPair _$$_OathPairFromJson(Map<String, dynamic> json) => _$_OathPair(
OathCredential.fromJson(json['credential'] as Map<String, dynamic>),
json['code'] == null
? null
: OathCode.fromJson(json['code'] as Map<String, dynamic>),
);
Map<String, dynamic> _$$_OathPairToJson(_$_OathPair instance) =>
<String, dynamic>{
'credential': instance.credential,
'code': instance.code,
};
_$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState( _$_OathState _$$_OathStateFromJson(Map<String, dynamic> json) => _$_OathState(
json['device_id'] as String, json['device_id'] as String,
hasKey: json['has_key'] as bool, hasKey: json['has_key'] as bool,