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
val jsonSerializer = Json {
// allows us to use Credential as a key
allowStructuredMapKeys = true
// creates properties for default values
encodeDefaults = true
}

View File

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

View File

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

View File

@ -7,8 +7,7 @@ import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.encodeToJsonElement
import org.junit.Assert.assertEquals
import org.junit.Assert.assertTrue
import org.junit.Assert.*
import org.junit.Test
class SerializationTest {
@ -16,7 +15,7 @@ class SerializationTest {
@Test
fun `serialization settings`() {
assertTrue(jsonSerializer.configuration.encodeDefaults)
assertTrue(jsonSerializer.configuration.allowStructuredMapKeys)
assertFalse(jsonSerializer.configuration.allowStructuredMapKeys)
}
@Test
@ -94,32 +93,25 @@ class SerializationTest {
@Test
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)
}
@Test
fun `credentials json size`() {
val m1 = mapOf<Model.Credential, Model.Code?>()
val jsonElement1 = jsonSerializer.encodeToJsonElement(m1) as JsonArray
val l1 = listOf<Model.CredentialWithCode>()
val jsonElement1 = jsonSerializer.encodeToJsonElement(l1) as JsonArray
assertEquals(0, jsonElement1.size)
val m2 = mapOf(totp() to code(), hotp() to code())
val jsonElement2 = jsonSerializer.encodeToJsonElement(m2) as JsonArray
assertEquals(4, jsonElement2.size)
val l2 = listOf(
Model.CredentialWithCode(totp(), code()), Model.CredentialWithCode(hotp(), code()),
)
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) {
var result = jsonDecode(input);
/// structure of data in the json object is:
/// [credential1, code1, credential2, code2, ...]
final List<OathPair> pairs = [];
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),
),
);
if (result is List) {
state = result.map((e) => OathPair.fromJson(e)).toList();
} else {
state = [];
}
}
state = pairs;
}
}

View File

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

View File

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

View File

@ -46,6 +46,19 @@ Map<String, dynamic> _$$_OathCodeToJson(_$_OathCode instance) =>
'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(
json['device_id'] as String,
hasKey: json['has_key'] as bool,