feat(train): the Jellyfin token in Train's own store, sealed by the Keystore (ADR 0030, D52)
The token moves from flutter_secure_storage into JellyfinTokens.kt: a file per person in the no-backup folder, sealed with AES-GCM under a key of Android's Keystore, the person's ID as associated data, so that the music job to come can read it as the app does. A token the keyring held is moved over as it is first read. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
This commit is contained in:
parent
64366d5420
commit
6bde74f0ba
2 changed files with 342 additions and 18 deletions
|
|
@ -426,7 +426,7 @@ Future<TrainPerson> _openPersonData(
|
|||
final music = Platform.isAndroid
|
||||
? MusicLibrary(
|
||||
MusicStore(File(p.join(folder.path, 'music.json'))),
|
||||
SecureJellyfinTokenStore(person.authUserId),
|
||||
AndroidJellyfinTokenStore(person.authUserId),
|
||||
version: appVersion,
|
||||
)
|
||||
: null;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import 'dart:io';
|
|||
import 'dart:math' show Random;
|
||||
|
||||
import 'package:equatable/equatable.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
/// Music from Jellyfin on this phone, for one person (ADR 0030, D47): never
|
||||
|
|
@ -137,32 +138,50 @@ abstract interface class JellyfinTokenStore() {
|
|||
Future<void> forgetOthers();
|
||||
}
|
||||
|
||||
/// [JellyfinTokenStore] in the system's keyring (Android's Keystore), under
|
||||
/// `train_jellyfin:` and the person's ID.
|
||||
class SecureJellyfinTokenStore(
|
||||
/// [JellyfinTokenStore] in Train's own store on Android (ADR 0030, D52),
|
||||
/// over `app.halkyon.train/music` (`JellyfinTokens.kt`: sealed under a key
|
||||
/// of the Keystore's, where the music job reads it too). A token the system's
|
||||
/// keyring kept before, under `train_jellyfin:` and the person's ID, is moved
|
||||
/// over as it is first read, and leaves the keyring.
|
||||
class AndroidJellyfinTokenStore(
|
||||
final String _person, {
|
||||
final FlutterSecureStorage _storage = const FlutterSecureStorage(),
|
||||
final MethodChannel _channel = const MethodChannel('app.halkyon.train/music'),
|
||||
final FlutterSecureStorage _keyring = const FlutterSecureStorage(),
|
||||
}) implements JellyfinTokenStore {
|
||||
static const _prefix = 'train_jellyfin:';
|
||||
static const _keyringPrefix = 'train_jellyfin:';
|
||||
|
||||
String get _key => '$_prefix$_person';
|
||||
String get _keyringKey => '$_keyringPrefix$_person';
|
||||
|
||||
@override
|
||||
Future<String?> read() => _storage.read(key: _key);
|
||||
Future<String?> read() async {
|
||||
final token = await _channel.invokeMethod<String>('tokenRead', {
|
||||
'person': _person,
|
||||
});
|
||||
if (token != null) {
|
||||
return token;
|
||||
}
|
||||
final before = await _keyring.read(key: _keyringKey);
|
||||
if (before != null) {
|
||||
await keep(before);
|
||||
}
|
||||
return before;
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> keep(String? token) async {
|
||||
await _storage.delete(key: _key);
|
||||
if (token != null) {
|
||||
await _storage.write(key: _key, value: token);
|
||||
}
|
||||
await _channel.invokeMethod<void>('tokenKeep', {
|
||||
'person': _person,
|
||||
'token': token,
|
||||
});
|
||||
await _keyring.delete(key: _keyringKey);
|
||||
}
|
||||
|
||||
@override
|
||||
Future<void> forgetOthers() async {
|
||||
for (final key in (await _storage.readAll()).keys) {
|
||||
if (key.startsWith(_prefix) && key != _key) {
|
||||
await _storage.delete(key: key);
|
||||
await _channel.invokeMethod<void>('tokenForgetOthers', {'person': _person});
|
||||
for (final key in [...(await _keyring.readAll()).keys]) {
|
||||
if (key.startsWith(_keyringPrefix) && key != _keyringKey) {
|
||||
await _keyring.delete(key: key);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue