The short reading's H1: open(2) takes its mode among variable arguments, which Apple's Arm passes on the stack, so bound with three fixed ones it read the mode from whatever the stack held: a phrase or recovery codes saved on an Apple silicon Mac could be readable by everyone. It is bound as variadic now (VarArgs), and every file written is made 0600 through its descriptor (fchmod), checked. M1: macOS's sandbox lets an app reach the file the person chose and nothing beside it, so the file made beside it was refused and nothing could be saved; Learn's sandbox allowed reading chosen files alone, so its exports never saved on macOS either. There, writePrivately now writes the chosen file itself (decided by the developer on 21 Oct): opened without following a link or waiting on a pipe and without truncating, refused with nothing written unless fstat finds a file of the own user's, then made 0600 and emptied. Both apps' entitlements grant reading and writing the files the person chooses. Linux keeps the new file renamed over it. The reading's nits: the buffer is wiped whatever happens, an interrupted call is made again (EINTR), the file made beside a chosen one is named apart from it so a name as long as a folder takes still saves, the folder is flushed after the rename, and Linux reads a file's type and owner with statx, alike on every architecture, where __lxstat's version differed (riscv64's was wrong). The test of another user's file is skipped as root. The in-place path is tested on Linux; none of it has run on a Mac. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Tizian 「ティツィアーン」 <raisondetredev@pm.me>
199 lines
12 KiB
Markdown
199 lines
12 KiB
Markdown
# halkyon_app
|
|
|
|
What every Halkyon app (Learn, Todo, Train) is built around, on an instance's
|
|
`halkyon_base` module: the instances a device keeps an account on, signing in,
|
|
each person's data opened and closed, and syncing it. Taken from Halkyon Learn,
|
|
which it was written for, so it behaves as Learn's own did; the texts are in
|
|
British English, American English and German, and where they name the app,
|
|
they name yours.
|
|
|
|
Written as the apps are: Dart 3.13's primary constructors, Material from
|
|
`package:material_ui`, Lucide icons, blocs (`flutter_bloc`), LeanCode's lints.
|
|
|
|
## What it holds
|
|
|
|
- **Instances** (`lib/src/instance/`): `Instance` (an address as people type
|
|
it; `isSecure`: over HTTPS, or over plain HTTP on this machine alone),
|
|
`InstanceChoice` (those the device keeps an account on, one in use;
|
|
`FileInstanceStore`; `allows` an instance reached securely, or in a
|
|
development build the build's own over HTTP, and `leave`s one it no longer
|
|
allows, which `InstanceGate` never opens: it says why and offers another or
|
|
removing it), `InstanceStorage` (each instance's people's folders and
|
|
session key), `InstanceChecker` (does an address answer, where does it keep
|
|
people's data; `ServerpodInstanceChecker` asks its `instance.basics()`),
|
|
`InstanceScope`. The screens: `FirstStartScreen` (the build's instance and
|
|
another, as tiles, `InstanceTile`), `InstanceStep` (an address, checked),
|
|
`showAccountSwitcher` and `AddAccountScreen`, and where an instance keeps
|
|
people's data, as a flag and words (`InstanceLocationBuilder`,
|
|
`InstanceFlag`, `instanceLocationWords`): `EU`, a country or a subdivision
|
|
(ISO 3166-2, `US-CA`) as its operator says, "Germany (EU)" for a member of
|
|
the European Union (`europeanUnionMembers`) and "California, United States"
|
|
for a subdivision, with the most specific flag there is (a few subdivisions
|
|
have their own, such as Scotland; else the country's). Countries and
|
|
subdivisions are named by Unicode CLDR's names in English and German, which
|
|
come with the package (`AssetCountryNames`, `AssetSubdivisionNames`; the
|
|
package's `tool/country_names.py` and `tool/subdivision_names.py` write
|
|
them, from one release); tests give names of their own (`CountryNames`,
|
|
`SubdivisionNames`).
|
|
- **Gates** (`lib/src/app/`): `InstanceGate` opens the app's dependencies on
|
|
the instance in use (`AppDependencies`, handed down by `AppScope`), anew on
|
|
a switch, the previous ones closed first; `SessionGate` keeps a person
|
|
signed in, signing in anonymously at a first start and asking how to start
|
|
once signed out (`StartScreen`); `PersonScope` opens each person's own
|
|
dependencies and closes them when the person goes. `resolveAppLocale`
|
|
picks the app's language as every Halkyon app does: the first preferred
|
|
language it speaks, never by country alone, British English otherwise.
|
|
- **About** (`showAppAbout`): the app's name (its frame's), its version,
|
|
icon and legalese where it gives them, where the source of what the
|
|
instance runs is (`InstanceSourceCode`, from `AppDependencies.sourceCodeUrl`;
|
|
both licences require offering it), the app's own parts (its credits), and
|
|
the licence list.
|
|
- **The account** (`AccountTiles`): whose account on which instance,
|
|
switching to the account on another, signing out (which of an anonymous
|
|
account deletes it, so asks first) and deleting it (after asking), for an
|
|
app's settings, in the app's words where only it can say them.
|
|
- **Signing in** (`lib/src/auth/`): `AuthRepository` on an `AuthGateway`;
|
|
`ServerpodAuthGateway` signs in anonymously and with GitHub (the system
|
|
browser and a loopback address on Linux and Windows, `LoopbackWebAuth`),
|
|
through the instance's `halkyon_base`, and puts a session granted elsewhere
|
|
in place (`useSession`); `SessionBloc`, `GitHubSignInBloc` and
|
|
`SignInWaysCubit`; `SignedOutMark`, `SignInAddresses`, `PersonFolders`.
|
|
- **A password and TOTP** (`lib/src/password/`, ADR 0017): `PasswordRepository`
|
|
on a `PasswordGateway` (`ServerpodPasswordGateway`), an `AccountCrypto`
|
|
(`SodiumAccountCrypto`: halkyon_keys and SRP's client half, each step on an
|
|
isolate of its own, whose results are copied before they cross back:
|
|
sodium's are views of memory freed with the isolate that made them) and an
|
|
`AccountKeyStore` (`SecureAccountKeyStore`: the account's unlocked keys in
|
|
the system's keyring, per instance and person, gone once another person
|
|
signs in or nobody; on iOS and macOS `unlocked_this_device`, so readable
|
|
only while the device is unlocked, never in a backup restored elsewhere nor
|
|
in iCloud Keychain, each write deleting the entry before it). Signing in (SRP's M2 checked and the keys opened before
|
|
anything is trusted), signing up (the recovery phrase shown once and three of
|
|
its words typed back before anything reaches the instance), a setup never
|
|
finished, the authenticators (QR codes drawn with pub.dev's `qr`), recovery
|
|
codes, changing the password, a new phrase, and getting back in. The
|
|
screens: the start screen's Sign in and Create an account,
|
|
`showPasswordSignInSheet`, `showAccountSetupSheet`,
|
|
`showAccountRecoverySheet`, and `AccountSecuritySection` for an app's
|
|
account part. Every change of how the account is secured asks for the
|
|
password with the code, one answer per sheet (`OwnerAnswer`, the owner's
|
|
proof: a session alone gets no code checked), each answer bound to the
|
|
instance's base URL and to the change it approves (its purpose and the
|
|
digest of what it sets, `proof_digests.dart`), so nobody between the device
|
|
and the instance swaps anything in; deleting by a code by mail
|
|
follows `secondFactorWorks`, as the instance tells it, and asks for the
|
|
address only where the device doesn't know it. Secrets shown stay the
|
|
person's: nothing secret is copied (no app can mark a copy as sensitive
|
|
through Flutter, so clipboard histories and cloud clipboards would keep
|
|
it): the recovery phrase and codes are saved or written down, an
|
|
authenticator's key scanned, opened or typed; all of them show in a
|
|
`SecretScreen` (the phrase typed back too), kept out of screenshots,
|
|
screen recordings and recents where the app's window answers
|
|
`app.halkyon/screen_guard`: Android's `FLAG_SECURE` (as Learn's and Todo's
|
|
`ScreenGuard.kt` in their `MainActivity`), macOS's `sharingType = .none`
|
|
(`MainFlutterWindow.swift`) and Windows' `WDA_EXCLUDEFROMCAPTURE`
|
|
(`flutter_window.cpp`); covered on iOS while the app isn't in front;
|
|
nothing on Linux; keyboards learn nothing typed into a password, a code or
|
|
the phrase; no state's text (which a debug build's bloc observer logs) holds
|
|
a secret, a code or the address; only an `otpauth:` URI is shown as a QR
|
|
code or opened; and anything a step didn't expect (bytes of the wrong
|
|
length from an instance, libsodium's or the keyring's own errors) fails it
|
|
rather than leaving its sheet busy. `register_interop_test.dart` holds the
|
|
real cryptography to the register's own checks. Anonymous people get no
|
|
keys until something is encrypted (step 4). Every build of an app needs a C
|
|
compiler and make: sodium's build hook compiles libsodium.
|
|
- **Files** (`FileSaver`, `PlatformFileSaver`): keeping a file the app made
|
|
where the person chooses (a recovery phrase, recovery codes, an app's
|
|
exports): on Linux and macOS the own user's alone (`writePrivately`). On
|
|
Linux a new file beside it, made 0600 with `O_EXCL`, written through its
|
|
descriptor, renamed over the one chosen, never written into, and the
|
|
folder flushed. On macOS, whose sandbox lets an app reach the file chosen
|
|
and nothing beside it, the file itself, opened without following a link
|
|
or waiting on a pipe, refused unless it is a file of the own user's, made
|
|
0600 and emptied before anything is written; whoever held it open reads
|
|
what is saved. Each app's macOS entitlements grant reading and writing
|
|
the files the person chooses (`com.apple.security.files.user-selected.read-write`),
|
|
without which nothing could be saved there, Learn's exports neither. On
|
|
both, a symbolic link or another user's file is refused with nothing
|
|
written. On Windows written in place with its folder's rights, on iOS
|
|
without the copy file_picker leaves in the temporary folder
|
|
(`forgetPickerCopy`). None of it has run on a Mac yet.
|
|
- **Sync** (`lib/src/sync/`): `SyncService` (once at start, shortly after
|
|
local changes, on resume and every few minutes, one sync at a time, counting
|
|
the changes not on the server yet, `PendingChangesStore`), `AppCompatibility`
|
|
and `compatibleSync` (the instance's minimum app build, the engine's table
|
|
check), `SyncStatusBloc`, `syncAndWait`, and the widgets: `SyncStatusCard`,
|
|
`AppMismatchBanner`, `SyncTroubleBadge`.
|
|
- **A client of `halkyon_base` alone** (`BaseClient`): to ask an instance
|
|
about itself, and to sign in as another person than the app's client holds.
|
|
- **Testing** (`package:halkyon_app/testing.dart`): fakes of every boundary
|
|
(`FakeAuthGateway`, `FakePasswordGateway`, `FakeAccountCrypto`,
|
|
`FakeInstanceChecker`, the stores in memory), a frame
|
|
(`testFrame`), `testApp`, and `TestAppDependencies`.
|
|
|
|
## Using it in an app
|
|
|
|
1. **The server** includes `halkyon_base` with the nickname `base` (its
|
|
README), so the app's generated client has `client.modules.base`.
|
|
2. **Depend on it** by path, within the repository's workspace, as on
|
|
`halkyon_ui`:
|
|
|
|
```yaml
|
|
dependencies:
|
|
halkyon_app:
|
|
path: ../../../packages/halkyon_app
|
|
```
|
|
|
|
3. **Texts**: add `HalkyonAppLocalizations.delegate` to every `MaterialApp`'s
|
|
delegates, beside `HalkyonUiLocalizations.delegate` and material_ui's
|
|
`GlobalMaterialLocalizations.delegates`.
|
|
4. **The frame** (`AppFrame`): the app's name and wordmark word, its icon, an
|
|
example address, its theme, delegates, languages (`resolveAppLocale` as
|
|
its resolution) and launch screen, and the three sentences only it can
|
|
say: what the first start's instance keeps of the person's, what a switch
|
|
to another account leaves behind, and where GitHub is added later.
|
|
5. **Android**: the app's `MainActivity` answers `app.halkyon/screen_guard`
|
|
(copy Learn's or Todo's `ScreenGuard.kt` and call `ScreenGuard.attach` in
|
|
`configureFlutterEngine`), or secrets show in screenshots and recents.
|
|
6. **The dependencies**: the app's composition root implements
|
|
`AppDependencies<P>` (`serverName`, `auth`, `signedOut`, `passwords`,
|
|
`fileSaver`, `sourceCodeUrl`, `openPerson`, `dispose`), its person's
|
|
dependencies `Disposable`. Its `passwords` is a `PasswordRepository` on
|
|
`ServerpodPasswordGateway(client, base: client.modules.base)`,
|
|
`SodiumAccountCrypto()`, `SecureAccountKeyStore(storage.keysOf(instance))`,
|
|
the auth gateway and the app's `SignInAddresses`. Its `auth` is an
|
|
`AuthRepository` on `ServerpodAuthGateway(client, base:
|
|
client.modules.base, callbackScheme: …, appName: …)`; the scheme is the
|
|
app's own (`app.halkyon.learn`), registered with each platform and in the
|
|
instance's GitHub OAuth app, whose loopback address on Linux and Windows is
|
|
`http://127.0.0.1/github-callback` (port 43823 unless given).
|
|
7. **The tree**:
|
|
|
|
```dart
|
|
InstanceGate(
|
|
frame: frame,
|
|
choice: choice,
|
|
checker: ServerpodInstanceChecker(app: 'todo'),
|
|
readLook: …, // the device's theme mode and language
|
|
open: Dependencies.create,
|
|
app: SessionGate(
|
|
look: (context) => …,
|
|
app: (person) => PersonApp(person: person), // with PersonScope inside
|
|
),
|
|
)
|
|
```
|
|
|
|
A person's widgets find their dependencies with
|
|
`PersonScope.of<PersonDependencies>(context)` and the app's with
|
|
`AppScope.of<Dependencies>(context)`.
|
|
8. **Licences**: call `registerHalkyonAppLicenses()` once at startup (the
|
|
flags' and Unicode CLDR's notices), beside halkyon_ui's
|
|
`registerFontLicenses()`.
|
|
|
|
## Licence
|
|
|
|
GNU Affero General Public License 3.0 or later (`LICENSE`). The flags are
|
|
lipis' flag-icons, drawn by the `flag` package, under the MIT licence
|
|
(`assets/flags/LICENSE-flag-icons.txt`). The subdivisions' names are Unicode
|
|
CLDR's (release 48.2), under the Unicode License v3
|
|
(`assets/subdivisions/LICENSE-CLDR.txt`, `assets/subdivisions/ATTRIBUTION.md`).
|